Android 手电筒照亮效果

前言

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

实现方法梳理

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

效果

实现原理

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

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

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

js 复制代码
1,0,0,   1,0,1,
0,1,0, X 0,1,2,
0,0,1    0,0,1

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

java 复制代码
public class GradientShaderTextView extends TextView {

    private LinearGradient mLinearGradient;
    private Matrix mGradientMatrix;
    private Paint mPaint;
    private int mViewWidth = 0;
    private int mTranslate = 0;

    private boolean mAnimating = true;
    private int delta = 15;
    public GradientShaderTextView(Context ctx)
    {
        this(ctx,null);
    }

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                mPaint = getPaint();
                String text = getText().toString();
                // float textWidth = mPaint.measureText(text);
                int size;
                if(text.length()>0)
                {
                    size = mViewWidth*2/text.length();
                }else{
                    size = mViewWidth;
                }
                mLinearGradient = new LinearGradient(-size, 0, 0, 0,
                        new int[] { 0x33ffffff, 0xffffffff, 0x33ffffff },
                        new float[] { 0, 0.5f, 1 }, Shader.TileMode.CLAMP); //边缘融合
                mPaint.setShader(mLinearGradient);
                mGradientMatrix = new Matrix();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int length = Math.max(length(), 1);
        if (mAnimating && mGradientMatrix != null) {
            float mTextWidth = getPaint().measureText(getText().toString());
            mTranslate += delta;
            if (mTranslate > mTextWidth+1 || mTranslate<1) {
                delta  = -delta;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);  //自动平移矩阵
            mLinearGradient.setLocalMatrix(mGradientMatrix);
            postInvalidateDelayed(30);
        }
    }

}

本文案例

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

坑点

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

知识点

js 复制代码
canvas.drawPaint(mCommonPaint);

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

关键代码段

scss 复制代码
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中心点的方法

代码

按照惯例,给出全部代码

java 复制代码
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);
    }

}
相关推荐
2的n次方_10 分钟前
二维费用背包问题
java·算法·动态规划
M_emory_24 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito28 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
simple_ssn38 分钟前
【C语言刷力扣】1502.判断能否形成等差数列
c语言·算法·leetcode
寂静山林1 小时前
UVa 11855 Buzzwords
算法
Curry_Math1 小时前
LeetCode 热题100之技巧关卡
算法·leetcode
ahadee1 小时前
蓝桥杯每日真题 - 第10天
c语言·vscode·算法·蓝桥杯
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法