前言
经常在掘金博客上看到使用前端技术实现的手电筒照亮效果,但是搜了一下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);
}
}