Android 呼吸灯效果

android呼吸灯效果设计思路

  • 使用颜色渐变,使其边缘灯光区,与背景色无缝连接
  • 使用属性动画,控制呼吸灯的发光

代码文件

package com.tian.criminalintent;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Created by XuZhen on 2019/5/13 18:02
 */
public class BreatheView extends View {
    private int mInsideViewWidth = dp2px(getContext(), 70);
    private int mInsideViewHeight = dp2px(getContext(), 40);
    private int mLightDiffusionWidth = dp2px(getContext(), 20);  //扩散区宽度
    private int mTextColor = Color.parseColor("#ffffff"); //默认字体颜色为白色
    private int mTextSize = sp2px(getContext(), 22);
    private int mInsideColor = Color.parseColor("#9FB6FF");//内部图形颜色,为蓝色
    private boolean isLight = false; //控制是否开启动画

    private int[] colors = {Color.parseColor("#9FB6FF"), Color.parseColor("#A7BAFE"), Color.parseColor("#F26882"), Color.parseColor("#F8DF57")};//变动颜色

    private int mOutsideColor;
    private Paint mTextPaint;
    private Paint mInsideViewPaint;
    private Paint mOutsideRectPaint;
    private float mOutsideRectHeight;
    private Paint mOutsideCirclePaint;

    private int mRealWidth;
    private int mRealHeight;
    private LinearGradient mLinearGradient;
    private RadialGradient mRadialGradient;
    private String mBreatheViewText;
    private ValueAnimator mGoLightAnim;
    private ValueAnimator mBackLightAnim;
    private ValueAnimator mColorAnim;
    private int mColor;

    private int mEndGraidentColor = Color.parseColor("#2e2e3D");

    public BreatheView(Context context) {
        this(context, null);
    }

    public BreatheView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BreatheView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        obtainAttributes(attrs);
        initPaint();
    }

    /**
     * 步骤一:根据xml属性,设置控件的属性
     *
     * @param attrs
     */
    private void obtainAttributes(AttributeSet attrs) {
        TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.BreatheView);
        mInsideViewWidth = typedArray.getDimensionPixelSize(R.styleable.BreatheView_insideViewWidth, mInsideViewWidth);
        mInsideViewHeight = typedArray.getDimensionPixelSize(R.styleable.BreatheView_insideViewHeight, mInsideViewHeight);
        mLightDiffusionWidth = (int) typedArray.getDimension(R.styleable.BreatheView_lightDiffusionWidth, mLightDiffusionWidth);

        mTextSize = typedArray.getDimensionPixelSize(R.styleable.BreatheView_BreatheViewTextSize, mTextSize);
        mTextColor = typedArray.getColor(R.styleable.BreatheView_BreatheViewTextColor, mTextColor);
        //控件文字
        mBreatheViewText = typedArray.getString(R.styleable.BreatheView_BreatheViewText);
        //内部颜色
        mInsideColor = typedArray.getColor(R.styleable.BreatheView_BreatheViewInsideColor, mInsideColor);
        //是否开启灯光扩散,在xml中设置
        isLight = typedArray.getBoolean(R.styleable.BreatheView_BreatheViewIsLight, isLight);
        typedArray.recycle();//回收资源
    }

    /**
     * 步骤二:初始化绘制控件的笔触
     */
    private void initPaint() {
        //文本文字
        mTextPaint = new Paint();
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        //内部的图形
        mInsideViewPaint = new Paint();
        mInsideViewPaint.setStyle(Paint.Style.FILL);
        mInsideViewPaint.setAntiAlias(true);
        mInsideViewPaint.setColor(mInsideColor);


        //外部方区域图形
        mOutsideColor = mInsideColor;
        mOutsideRectPaint = new Paint();
        mOutsideRectHeight = mInsideViewHeight;
        mLinearGradient = new LinearGradient(0, 0, 0, mOutsideRectHeight,
                new int[]{Color.WHITE, mOutsideColor, Color.WHITE}, new float[]{0, 0.5f, 1}, Shader.TileMode.CLAMP);
        mOutsideRectPaint.setShader(mLinearGradient);
        mOutsideRectPaint.setStyle(Paint.Style.FILL);
        mOutsideRectPaint.setAntiAlias(true);
        mOutsideRectPaint.setColor(mOutsideColor);


        //外部的圆形区域
        mOutsideCirclePaint = new Paint();
        mOutsideCirclePaint.setStyle(Paint.Style.FILL);
        mOutsideCirclePaint.setAntiAlias(true);
        mOutsideCirclePaint.setColor(mOutsideColor);
        mRadialGradient = new RadialGradient(mOutsideRectHeight / 2, mOutsideRectHeight / 2,
                mOutsideRectHeight / 2, mOutsideColor, Color.WHITE, Shader.TileMode.CLAMP);
        mOutsideCirclePaint.setShader(mRadialGradient);

    }

    /**
     * 第三步,测试控件大小
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = getPaddingBottom() + getPaddingTop() + Math.abs(mLightDiffusionWidth * 2) + mInsideViewHeight;
        int width = getPaddingLeft() + getPaddingRight() + mInsideViewWidth + mInsideViewHeight / 2 + mLightDiffusionWidth;
        mRealWidth = resolveSize(width, widthMeasureSpec);
        mRealHeight = resolveSize(height, heightMeasureSpec);
        setMeasuredDimension(mRealWidth, mRealHeight);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

    /**
     * 第四步,绘制控件
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //变色
        mInsideViewPaint.setColor(mInsideColor);
        mInsideViewPaint.setAlpha(150);

        canvas.translate(mInsideViewHeight / 2 + mLightDiffusionWidth + getPaddingLeft(), mRealHeight >> 1);
        canvas.save();//保存图层

        if (isLight) {
            mOutsideCirclePaint.setColor(mOutsideColor);
            mOutsideRectPaint.setColor(mOutsideColor);
            mOutsideCirclePaint.setAlpha(150);
            mOutsideRectPaint.setAlpha(150);
            //绘制外部的长方形
            canvas.translate(0, -mOutsideRectHeight / 2);
            canvas.drawRect(0, 0, mInsideViewWidth, mOutsideRectHeight, mOutsideRectPaint);
            //绘制外部的半圆
            canvas.translate(-mOutsideRectHeight / 2, 0);
            canvas.drawArc(0, 0, (int) mOutsideRectHeight, (int) mOutsideRectHeight, 90, 180, true, mOutsideCirclePaint);
            canvas.restore();
            canvas.save();
        }

        //绘制内部的长方形与半圆
        canvas.translate(0, -(mInsideViewHeight >> 1));
        canvas.drawRect(0, 0, mInsideViewWidth, mInsideViewHeight, mInsideViewPaint);
        canvas.translate(-(mInsideViewHeight / 2), 0);
        canvas.drawArc(0, 0, mInsideViewHeight, mInsideViewHeight, 90, 180, true, mInsideViewPaint);
        canvas.restore();
        canvas.save();
        canvas.translate(0, -(mInsideViewHeight >> 1));

        //文字的x轴坐标
        float stringWidth = mTextPaint.measureText(mBreatheViewText);
        float x = (mInsideViewWidth - stringWidth) / 2;
        //文字的y轴坐标
        Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
        float y = (float) mInsideViewHeight / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2;
        canvas.drawText(mBreatheViewText, x, y, mTextPaint);
        canvas.restore();
    }

    /**
     * 这边是设置动画,动画分成两个部分,第一部分,是扩散出去,第二部分是扩散回来,同时颜色渐变
     */
    public void startLightAnim() {
        mGoLightAnim = ValueAnimator.ofFloat(0f, mLightDiffusionWidth);
        mGoLightAnim.setDuration(1500);
        //动态开始设置扩散出去动画
        mGoLightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //// TODO: 动画里面里面不应该不停的new对象,对内存会造成影响,,需要优化
                mOutsideRectHeight = (int) (mInsideViewHeight + (float) animation.getAnimatedValue() * 2);
                setGradient();
                invalidate();
            }
        });
        mGoLightAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                if (!isLight) {
                    return;
                }
                mGoLightAnim.setStartDelay(1500);
                mGoLightAnim.start();
          
            }
        });

        //扩散回来的颜色动画
        mBackLightAnim = ValueAnimator.ofFloat(mLightDiffusionWidth, 0f);
        mBackLightAnim.setDuration(1500);
        mBackLightAnim.setStartDelay(1500);
        mBackLightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mOutsideRectHeight = (int) (mInsideViewHeight + (float) animation.getAnimatedValue() * 2);
                setGradient();
                invalidate();
            }
        });
        mBackLightAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                if (!isLight) {
                    return;
                }
                mBackLightAnim.setDuration(1500);
                mBackLightAnim.start();

       
            }
        });

        //第二部分颜色渐变动画
        int i = (int) (Math.random() * 3);
        mColor = colors[i];
        mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), mInsideColor, mColor);
        mColorAnim.setDuration(1500);
        mColorAnim.setStartDelay(1500);
        mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mInsideColor = ((Integer) animation.getAnimatedValue());
                mOutsideColor = mInsideColor;
                invalidate();
            }
        });
        mColorAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                if (!isLight) {
                    return;
                }
          
                int i = (int) (Math.random() * 3);
                mColor = colors[i];
                Log.e("TAG", i + "");
                mColorAnim.setObjectValues(mInsideColor, mColor);
                mColorAnim.setStartDelay(1500);
                mColorAnim.start();
            }
        });

        mGoLightAnim.start();
        mBackLightAnim.start();
        mColorAnim.start();
        isLight = true;
    }

    /**
     * 重新设置Gradient
     */
    private void setGradient() {
        mLinearGradient = new LinearGradient(0, 0, 0, mOutsideRectHeight,
                new int[]{mEndGraidentColor, mOutsideColor,mEndGraidentColor}, new float[]{0, 0.5f, 1}, Shader.TileMode.CLAMP);
        mOutsideRectPaint.setShader(mLinearGradient);
        mRadialGradient = new RadialGradient(mOutsideRectHeight / 2, mOutsideRectHeight / 2,
                mOutsideRectHeight / 2, mOutsideColor, mEndGraidentColor, Shader.TileMode.CLAMP);
        mOutsideCirclePaint.setShader(mRadialGradient);
    }

    /**
     * 设置渐变底色
     * @param endGraidentColor
     */
    public void setEndGraidentColor(int endGraidentColor) {
        mEndGraidentColor = endGraidentColor;
    }

    /**
     * 关闭动画
     */
    public void cancelLightAnim() {
        isLight = false;//关闭动画
        if (mGoLightAnim != null) {
            mGoLightAnim.cancel();
        }
        if (mBackLightAnim != null) {
            mBackLightAnim.cancel();
        }
        if (mColorAnim != null) {
            mColorAnim.cancel();
        }
        mOutsideRectHeight = mInsideViewHeight;
        mInsideColor = mColor;
        mOutsideColor = mColor;
        invalidate();
    }

    /**
     * dp转换成px,scale为像素密度,density越高,分辨率越高
     *
     * @param context 获取屏幕
     * @param dpVal
     * @return
     */
    public static int dp2px(Context context, int dpVal) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpVal * scale + 0.5f);
    }

    /**
     * sp转换成px,fontSCale为文字像素密度,fontScale越高,分辨率越高
     *
     * @param context
     * @param spVal
     * @return
     */
    public static int sp2px(Context context, int spVal) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spVal * fontScale + 0.5f);
    }
}

attrs:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="BreatheView">
        <attr name="insideViewWidth" format="dimension" /> <!--内部TextView的宽度-->
        <attr name="insideViewHeight" format="dimension" /> <!--内部TextView的高度-->

        <attr name="lightDiffusionWidth" format="dimension" /> <!--光扩散的间距-->

        <attr name="BreatheViewTextSize" format="dimension" />  <!--设置文字的大小-->
        <attr name="BreatheViewTextColor" format="color" />  <!--设置文字的颜色-->
        <attr name="BreatheViewText" format="string" /> <!--控件的文字-->

        <attr name="BreatheViewInsideColor" format="color" />  <!--设置内部View的颜色-->

        <attr name="BreatheViewIsLight" format="boolean" /> <!--设置控件是否扩散-->

    </declare-styleable>
</resources>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 在小镇看着蓝天 听一首缓慢动人的歌曲 一不小心就入迷了
    伍月的晴空阅读 397评论 3 6
  • 今天,朋友圈“山竹”刷屏了。 Part 1 我是一个不看各种新闻的原始人,遇到各种江湖上的江湖事,基本都会自动屏蔽...
    转转小仙女阅读 361评论 3 2
  • 洞房 一个可以疯狂的炽热开张 生命在此刻热血升华 有一些人善始却不能善终 心事 这是一处隐秘的洞穴 阿里巴巴也不知...
    情也阅读 1,030评论 19 53
  • 天干:甲(jiǎ)、乙( yǐ)、 丙(bǐng)、丁(dīng)、戊(wù)、 己(jǐ)、庚(gēng)、辛(...
    金石明镜阅读 9,392评论 0 2
  • 2018.03.30.星期五,天气晴 昨晚大宝和我们一起睡的,弄的我一晚上没睡好,怕挤着小宝又担心大宝蹬被子,...
    任昱丞妈妈阅读 113评论 0 0