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);
    }

}
相关推荐
辻戋20 分钟前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保22 分钟前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun1 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp1 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.2 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
电鱼智能的电小鱼3 小时前
基于电鱼 AI 工控机的智慧工地视频智能分析方案——边缘端AI检测,实现无人值守下的实时安全预警
网络·人工智能·嵌入式硬件·算法·安全·音视频
孫治AllenSun4 小时前
【算法】图相关算法和递归
windows·python·算法
TeleostNaCl4 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
格图素书5 小时前
数学建模算法案例精讲500篇-【数学建模】DBSCAN聚类算法
算法·数据挖掘·聚类
前端大卫6 小时前
为什么 React 中的 key 不能用索引?
前端