Android 手电筒照亮效果

2,931 阅读5分钟

前言

经常在掘金博客上看到使用前端技术实现的手电筒照亮效果,但是搜了一下android相关的,发现少得很,实际上这种效果在Android上实现会更简单,当然,条条大路通罗马,有很多技术手段去实现这种效果,今天我们选择一种相对比较好的方法来实现。

实现方法梳理

  • 第一种方法就是利用Path路径进行 Clip Outline,然后绘制不同的渐变效果即可,这种方法其实很适合蒙版切图,不过也能用于实现这种特效。
  • 第二种方法是利用Xfermode 进行中间图层镂空。
  • 第三种方法就是Shader,效率高且无锯齿。

效果

fire_61.gif

实现原理

其实本篇的核心就是Shader了,这次我们也用RadialGradient来实现,本篇几乎没有任何难度,关键技术难点就是Shader 的移动,其实最经典的效果是Facebook实现的光影文案,本质上时Matrix + Shader.setLocalMatrix 实现。

155007_4C1U_2256215.gif

Matrix涉及一些数学问题,Matrix初始化本身就是单位矩阵,几乎每个操作都是乘以另一个矩阵,属于线性代数的基本知识,难度其实并不高。

matrix.setTranslation(1,2) 可以看作,矩阵的乘法无非是行乘列,繁琐事繁琐,但是很容易理解


1,0,0,   1,0,1,
0,1,0, X 0,1,2,
0,0,1    0,0,1

我们来看看经典的facebook 出品代码

public class GradientShaderTextView extends View {

    private final String TAG = "GradientShaderTextView";
    private RadialGradient mGradient;
    private final Matrix mGradientMatrix = new Matrix();
    private Paint mPaint;
    private TextPaint mTextPaint;
    private int mTranslate = 0;
    private boolean mAnimating = true;
    private int delta = 5;
    private final Paint.FontMetrics fontMetrics = new Paint.FontMetrics();

    private final Rect textBound = new Rect();
    private int horizontalTextPadding = 0;
    private int verticalTextPadding = 0;

    public GradientShaderTextView(Context context) {
        super(context);
    }

    public GradientShaderTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

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

    private CharSequence text;

    {
        this.text = "你是我内心的一首歌";
        horizontalTextPadding = NumberUtils.dip2px(getContext(),20);
        verticalTextPadding = NumberUtils.dip2px(getContext(),40);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(0x88ffffff);

        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setTextSize(NumberUtils.dip2px(getContext(), 30));
        mTextPaint.setColor(Color.WHITE);
    }

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        String text = getText().toString();
        mTextPaint.getTextBounds(text,0,text.length(),textBound);
        if (widthMode != MeasureSpec.EXACTLY) {

            widthSize = textBound.width() + horizontalTextPadding;
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = textBound.height() + verticalTextPadding;
        }
        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (w <= 0) {
            return;
        }
        this.mGradient = null;

    }

    public CharSequence getText() {
        if(this.text == null){
            this.text = "";
        }
        return this.text;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        String text = getText().toString();
        mTextPaint.getTextBounds(text,0,text.length(),textBound);

        int width = getWidth();
        int height = getHeight();

        float x = (width - textBound.width())/ 2f;
        float y = (height)/ 2f;
        float textPaintBaseline = getTextPaintBaseline(mTextPaint);

        canvas.drawText(text,x,y + textPaintBaseline,mTextPaint);
        float radius = textBound.height();
        if(mGradient == null) {
            mGradient = new RadialGradient(0f, 0f, radius, new int[]{0x66ffffff,0x01ffffff}, new float[]{0, 0.95f}, Shader.TileMode.CLAMP); //边缘融合
            mPaint.setShader(mGradient);
        }

        if (mAnimating && mGradientMatrix != null) {

            if(mTranslate < radius){
                mTranslate = (int) radius;
                delta = Math.abs(delta);
            }else if (mTranslate > (width - radius + 1)) {
                delta = -Math.abs(delta);
            }
            mGradientMatrix.reset();
            mGradientMatrix.setTranslate(mTranslate, y);  //自动平移矩阵
            mGradient.setLocalMatrix(mGradientMatrix);
            canvas.drawCircle(mTranslate,y,radius,mPaint);
            postInvalidateDelayed(20);
            mTranslate += delta;

        }
    }

    float getTextPaintBaseline(TextPaint paint) {
        paint.getFontMetrics(fontMetrics);
        return (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent;
    }
    public void hide() {
        MLog.d(TAG, "call hide");
        setVisibility(GONE);
        mAnimating = false;
    }

    public void show() {
        MLog.d(TAG, "call show");
        setVisibility(VISIBLE);
        mAnimating = true;
        postInvalidate();
    }
}

本文案例

本文要实现的效果其实也是一样的方法,只不过不是自动移动,而是添加了触摸事件,同时加了放大缩小效果。

坑点

Shader 不支持矩阵Scale,本身打算利用Scale缩放光圈,但事与愿违,不仅不支持,连动都动不了了,因此,本文采用了两种Shader,按压时使用较大半径的Shader,手放开时使用默认的Shader。

知识点

canvas.drawPaint(mCommonPaint);

这个绘制并不是告诉你可以这么绘制,而是想说,设置了Shader之后,这样调用,Shader半径之外的颜色时Shader最后一个颜色值,我们最后一个颜色值时黑色,那就是黑色,我们改成白色当然也是白色,下图是改成白色之后的效果,周围都是白色

企业微信20231207-230353@2x.png

关键代码段

super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
if (width < 1 || height < 1) {
    return;
}
//大光圈shader
if (radialGradientLarge == null) {
    radialGradientLarge = new RadialGradient(0, 0,
            dp2px(100),
            new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
            new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
            Shader.TileMode.CLAMP);
}
//默认光圈shader
if (radialGradientNormal == null) {
    radialGradientNormal = new RadialGradient(0, 0,
            dp2px(50),
            new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
            new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
            Shader.TileMode.CLAMP);
}

//绘制地图
canvas.drawBitmap(mBitmap, 0, 0, null);

//移动shader中心点
matrix.setTranslate(x, y);
//设置到矩阵
radialGradientLarge.setLocalMatrix(matrix);
radialGradientNormal.setLocalMatrix(matrix);
if(isPressed()) {
//按压时
    mCommonPaint.setShader(radialGradientLarge);
}else{
//松开时
    mCommonPaint.setShader(radialGradientNormal);
}
//直接用画笔绘制,那么周围的颜色是Shader  最后的颜色
canvas.drawPaint(mCommonPaint);

好了,我们的效果基本实现了。

总结

本篇到这里就截止了,我们今天掌握的知识点是Shader相关的:

  • Shader 矩阵不能Scale
  • 设置完Shader 的画笔外围填充色为Ridial Shader最后的颜色
  • Canvas 可以直接drawPaint
  • Shader.setLocalMatrix是移动Shader中心点的方法

代码

按照惯例,给出全部代码

public class LightsView extends View {
    private final DisplayMetrics mDM;
    private TextPaint mCommonPaint;
    private Bitmap mBitmap;
    private RadialGradient radialGradientLarge = null;
    private RadialGradient radialGradientNormal = null;
    private float x;
    private float y;
    private boolean isPress = false;
    private Matrix matrix = new Matrix();
    public LightsView(Context context) {
        this(context, null);
    }

    public LightsView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public LightsView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();
        setClickable(true); //触发hotspot
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
        mCommonPaint.setAntiAlias(true);
        mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mCommonPaint.setStrokeCap(Paint.Cap.ROUND);
        mCommonPaint.setFilterBitmap(true);
        mCommonPaint.setDither(true);
        mBitmap = decodeBitmap(R.mipmap.mm_06);

    }

    private Bitmap decodeBitmap(int resId) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inMutable = true;
        return BitmapFactory.decodeResource(getResources(), resId, options);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        if (width < 1 || height < 1) {
            return;
        }
        if (radialGradientLarge == null) {
            radialGradientLarge = new RadialGradient(0, 0,
                    dp2px(100),
                    new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
                    new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
                    Shader.TileMode.CLAMP);
        }
        if (radialGradientNormal == null) {
            radialGradientNormal = new RadialGradient(0, 0,
                    dp2px(50),
                    new int[]{Color.TRANSPARENT, 0x01ffffff, 0x33ffffff, 0x66000000, 0x77000000, 0xff000000},
                    new float[]{0.1f, 0.3f, 0.5f, 0.7f, 0.9f, 0.95f},
                    Shader.TileMode.CLAMP);
        }

        canvas.drawBitmap(mBitmap, 0, 0, null);

        matrix.setTranslate(x, y);
        radialGradientLarge.setLocalMatrix(matrix);
        radialGradientNormal.setLocalMatrix(matrix);
        if(isPressed()) {
            mCommonPaint.setShader(radialGradientLarge);
        }else{
            mCommonPaint.setShader(radialGradientNormal);
        }
        canvas.drawPaint(mCommonPaint);
    }

    @Override
    public void dispatchDrawableHotspotChanged(float x, float y) {
        super.dispatchDrawableHotspotChanged(x, y);
        this.x = x;
        this.y = y;
        postInvalidate();
    }

    @Override
    protected void dispatchSetPressed(boolean pressed) {
        super.dispatchSetPressed(pressed);
        postInvalidate();
    }

    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

}