因为技术原理比较简单,所以就不详细赘述实现的细节了。
效果需求
一个具有圆形背景的等宽等高的视图上,上下来回滚动一个渐变的矩形,矩形的两边不能超出圆,也不能比圆小。
遇到的问题
如果只看上面这句话,大家都会觉得很简单,在视图上绘制一个圆,再绘制一个渐变的矩形,控制坐标来移动矩形就好了。但当大家实际操作的时候可能就会遇到这样一个问题:矩形绘制的时候总感觉很不协调,达不到预期效果。为什么呢?因为android默认的视图轮廓都是矩形的,即使背景是个圆,但是轮廓还是矩形,拟绘制的矩形宽度不变的话,就像一根固定的棍子在圆内滚动,还会超出圆的边界;如果动态设置矩形的宽度,矩形的两端还是不能平滑的和圆重叠。
解决的一些思路
1.如果是Android5.0及以上的系统,这个问题很好解决,有一个方法:View.setClipToOutline(boolean clip)或者在xml里android:clipToOutline =boolean,设置成true后,给view设置一个任何形状的背景,画矩形时把矩形的宽度设得比view个宽度大些或者相等,那么绘制的矩形两端就会很平滑的和圆相切,像是被圆的边盖住了一样;
2.如果是Android5.0以下的系统,也不要灰心,我们可以从裁剪Canvas入手。说到底,View的绘制还是在Canvas上面进行的,只要我们能把Canvas裁剪成一个以view的中心为圆心,以view的宽度的一半或高度的一半为半径的圆,那么不管滚动的矩形多宽,也只能绘制在这个圆形的画板上,多月部分被平滑的截掉了,达到了预期效果。
作为一个自定义view新手,可能这些方法对大牛们而言就是小儿科,但是我却花了一天多时间才折腾出来,大家随意看看就好。
代码
/**
* com.ykb.json.customview
* 描述 :带扫描线的ImageView
* 作者 : ykb
* 时间 : 15/11/4.
*/
public class ScanningImageView extends ImageView {
private static final int CHANGE_BOUNDS = 50;
private Paint mPaint;
private int mHeight = 0;
private Path mPath;
public ScanningImageView(Context context) {
this(context, null);
}
public ScanningImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ScanningImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setClipToOutline(true);//设置绘制的覆盖物不能超出背景的轮廓
}
mPath = new Path();
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.TRANSPARENT);
mPaint.setAlpha(255);
}
@Override
protected void onDraw(Canvas canvas) {
mHeight += 10;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
mPath.reset();
canvas.clipPath(mPath);
mPath.addCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, Path.Direction.CCW);
canvas.clipPath(mPath, Region.Op.REPLACE);
}
LinearGradient linearGradient = new LinearGradient(0, mHeight - CHANGE_BOUNDS, 0, mHeight, new int[]{Color.TRANSPARENT, Color.WHITE}, null, Shader.TileMode.CLAMP);
mPaint.setShader(linearGradient);
canvas.drawRect(0, mHeight - CHANGE_BOUNDS, getWidth(), mHeight, mPaint);
if (mHeight >= getHeight()) {
mHeight = 0;
}
postInvalidateDelayed(40);
super.onDraw(canvas);
}
}
======================我是华丽的分割线==========================
因为当时没有适配机型测试,后来发现在三星等手机5.0以下的系统版本上会出现无法裁剪的bug,现在来修正一下以前的做法o(╯□╰)o
1.首先,在5.0以下的手机上,必须把这个视图关闭硬件加速——解决不能正常裁剪的问题;
2.把构造方法里的
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setClipToOutline(true);//设置绘制的覆盖物不能超出背景的轮廓
}
去掉;
3.把onDraw方法里的
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
mPath.reset();
canvas.clipPath(mPath);
mPath.addCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, Path.Direction.CCW);
canvas.clipPath(mPath, Region.Op.REPLACE);
}
if判断去掉
@Override
protected void onDraw(Canvas canvas) {
mHeight += 10;
mPath.reset();
canvas.clipPath(mPath);
mPath.addCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, Path.Direction.CCW);
canvas.clipPath(mPath, Region.Op.REPLACE);
LinearGradient linearGradient = new LinearGradient(0, mHeight - CHANGE_BOUNDS, 0, mHeight, new int[]{Color.TRANSPARENT, Color.WHITE}, null, Shader.TileMode.CLAMP);
mPaint.setShader(linearGradient);
canvas.drawRect(0, mHeight - CHANGE_BOUNDS, getWidth(), mHeight, mPaint);
if (mHeight >= getHeight()) {
mHeight = 0;
}
postInvalidateDelayed(40);
super.onDraw(canvas);
}
执行以上三个操作后,基本可以适配绝大部分机型了。
但如果你认为这样就完了,那还真的没完。。。
后来没事的时候,我有捣鼓了一下以前的一些项目,其中就包括这个扫描ImageView,现在我把它弄得更“臃肿”了——集合了可以裁剪为圆形ImageView的功能:
/**
* 包名:com.ykb.json.customview
* 描述:可裁剪为圆形和自带扫描线的ImageView
* 创建者:yankebin
* 日期:2015/12/15
*/
public class RoundScanningImageView extends ImageView {
private Paint mPaint;
private int mHeight = 0;
private Path mPath;
private float centerX;
private float centerY;
private float moveSpeed;
private float outStrokeWidth;
private int outStrokeColor;
private int outStrokeAlpha;
private boolean enableClipPathRound;
private boolean enableScan;
private float strokeWidth;
private float scanLineHeight;
private int invalidateTime;
private PorterDuffXfermode porterDuffXfermode;
public RoundScanningImageView(Context context) {
this(context, null);
}
public RoundScanningImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundScanningImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//此处容易抛异常,导致关闭硬件加速失败
try {
setLayerType(LAYER_TYPE_SOFTWARE, null);
} catch (Exception e) {
e.printStackTrace();
}
TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.RoundScanningImageView);
moveSpeed = typedArray.getFloat(R.styleable.RoundScanningImageView_scan_speed, 10);
outStrokeWidth = typedArray.getFloat(R.styleable.RoundScanningImageView_out_stroke_width, 10);
outStrokeColor = typedArray.getColor(R.styleable.RoundScanningImageView_out_stroke_color, Color.LTGRAY);
outStrokeAlpha = typedArray.getInt(R.styleable.RoundScanningImageView_out_stroke_alpha, 100);
enableClipPathRound = typedArray.getBoolean(R.styleable.RoundScanningImageView_enable_clipPath_round, true);
enableScan = typedArray.getBoolean(R.styleable.RoundScanningImageView_enable_scan, true);
strokeWidth = typedArray.getFloat(R.styleable.RoundScanningImageView_stroke_width, 10);
scanLineHeight = typedArray.getFloat(R.styleable.RoundScanningImageView_scan_line_height, 50);
invalidateTime = typedArray.getInt(R.styleable.RoundScanningImageView_scan_invalidate_time, 50);
typedArray.recycle();
porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
mPath = new Path();
mPaint = new Paint();
}
/**
* 创建渲染器
*
* @return
*/
private LinearGradient buildLinearGradient() {
LinearGradient linearGradient = new LinearGradient(0, mHeight - scanLineHeight, 0, mHeight, new int[]{Color.TRANSPARENT, Color.WHITE}, null, Shader.TileMode.CLAMP);
return linearGradient;
}
/**
* 画笔重置
*
* @param color
* @param alpha
*/
private void resetPaint(int color, int alpha) {
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(color);
mPaint.setAlpha(alpha);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
centerX = getWidth() / 2;
centerY = getHeight() / 2;
// ViewParent parent = getParent();
// if (null != parent && parent instanceof ViewGroup) {
// ((ViewGroup) parent).setLayerType(LAYER_TYPE_SOFTWARE, null);
// }
}
/**
* 获取裁剪后的圆形图片
*
* @param bmp
* @param radius
* @return
*/
private Bitmap getCroppedRoundBitmap(Bitmap bmp, int radius) {
Bitmap scaledSrcBmp;
int diameter = radius * 2;
// 为了防止宽高不相等,造成圆形图片变形,因此截取长方形中处于中间位置最大的正方形图片
int bmpWidth = bmp.getWidth();
int bmpHeight = bmp.getHeight();
int squareWidth, squareHeight;
int x, y;
Bitmap squareBitmap;
if (bmpHeight > bmpWidth) {// 高大于宽
squareWidth = squareHeight = bmpWidth;
x = 0;
y = (bmpHeight - bmpWidth) / 2;
// 截取正方形图片
squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,
squareHeight);
} else if (bmpHeight < bmpWidth) {// 宽大于高
squareWidth = squareHeight = bmpHeight;
x = (bmpWidth - bmpHeight) / 2;
y = 0;
squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,
squareHeight);
} else {
squareBitmap = bmp;
}
if (squareBitmap.getWidth() != diameter
|| squareBitmap.getHeight() != diameter) {
scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, diameter,
diameter, true);
} else {
scaledSrcBmp = squareBitmap;
}
Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),
scaledSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
Paint paint = new Paint();
Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(),
scaledSrcBmp.getHeight());
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawCircle(scaledSrcBmp.getWidth() / 2,
scaledSrcBmp.getHeight() / 2, scaledSrcBmp.getWidth() / 2,
paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(scaledSrcBmp, rect, rect, paint);
return output;
}
@Override
protected void onDraw(Canvas canvas) {
if (!enableClipPathRound && !enableScan) {
super.onDraw(canvas);
} else {
if (enableClipPathRound) {
int radius = getWidth() > getHeight() ? getHeight() / 2 : getWidth() / 2;
radius -= outStrokeWidth / 2;
//绘制圆边
resetPaint(outStrokeColor, outStrokeAlpha);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(outStrokeWidth);
canvas.drawCircle(centerX, centerY, radius, mPaint);
//处理图片
Drawable drawable = getDrawable();
if (null != drawable) {
Bitmap mBitmap = ((BitmapDrawable) drawable).getBitmap();
if (null != mBitmap) {
radius -= strokeWidth;
//裁剪图片为圆形
Bitmap roundBitmap = getCroppedRoundBitmap(mBitmap, radius);
if (null != roundBitmap) {
try {
canvas.drawBitmap(roundBitmap, centerX - radius, centerY - radius, null);
roundBitmap.recycle();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
} else {
super.onDraw(canvas);
}
if (enableScan) {
//移动扫描白线的位置
mHeight += moveSpeed;
if (enableClipPathRound) {
//裁剪画布
mPath.reset();
canvas.clipPath(mPath); // makes the clip empty
mPath.addCircle(centerX, centerY, centerX - strokeWidth - outStrokeWidth / 2, Path.Direction.CCW);
canvas.clipPath(mPath, Region.Op.REPLACE);
}
//绘制扫描线
resetPaint(Color.TRANSPARENT, 255);
mPaint.setXfermode(porterDuffXfermode);
mPaint.setShader(buildLinearGradient());
canvas.drawRect(0, mHeight - scanLineHeight, getWidth(), mHeight, mPaint);
if (mHeight >= getHeight()) {
mHeight = 0;
}
postInvalidateDelayed(invalidateTime);
}
}
}
}
自定义的属性:
<declare-styleable name="RoundScanningImageView">
<attr name="out_stroke_width" format="float"/>
<attr name="enable_scan" format="boolean"/>
<attr name="scan_speed" format="float"/>
<attr name="enable_clipPath_round" format="boolean"/>
<attr name="stroke_width" format="float"/>
<attr name="out_stroke_color" format="color"/>
<attr name="out_stroke_alpha" format="integer"/>
<attr name="scan_line_height" format="float"/>
<attr name="scan_invalidate_time" format="integer"/>
declare-styleable>
=============================再一次优化=============================
优化如下:
引入弱引用缓存相关图片,避免高频率gc
public class RoundScanningImageView extends ImageView {
private Paint mPaint;
private int mHeight = 0;
private Path mPath;
private float centerX;
private float centerY;
private float moveSpeed;
private float outStrokeWidth;
private int outStrokeColor;
private int outStrokeAlpha;
private boolean enableClipPathRound;
private boolean enableScan;
private float strokeWidth;
private float scanLineHeight;
private int invalidateTime;
private PorterDuffXfermode porterDuffXfermode;
private Drawable mLastDrawable;
private WeakReference mTempBitmap;
private WeakReference mLastBitmap;
public RoundScanningImageView(Context context) {
this(context, null);
}
public RoundScanningImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RoundScanningImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
try {
setLayerType(LAYER_TYPE_SOFTWARE, null);
} catch (Exception e) {
e.printStackTrace();
}
TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.RoundScanningImageView);
moveSpeed = typedArray.getFloat(R.styleable.RoundScanningImageView_scan_speed, 10);
outStrokeWidth = typedArray.getFloat(R.styleable.RoundScanningImageView_out_stroke_width, 10);
outStrokeColor = typedArray.getColor(R.styleable.RoundScanningImageView_out_stroke_color, Color.LTGRAY);
outStrokeAlpha = typedArray.getInt(R.styleable.RoundScanningImageView_out_stroke_alpha, 100);
enableClipPathRound = typedArray.getBoolean(R.styleable.RoundScanningImageView_enable_clipPath_round, true);
enableScan = typedArray.getBoolean(R.styleable.RoundScanningImageView_enable_scan, true);
strokeWidth = typedArray.getFloat(R.styleable.RoundScanningImageView_stroke_width, 10);
scanLineHeight = typedArray.getFloat(R.styleable.RoundScanningImageView_scan_line_height, 50);
invalidateTime = typedArray.getInt(R.styleable.RoundScanningImageView_scan_invalidate_time, 50);
typedArray.recycle();
porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
mPath = new Path();
mPaint = new Paint();
}
/**
* 创建渲染器
*
* @return
*/
private LinearGradient buildLinearGradient() {
LinearGradient linearGradient = new LinearGradient(0, mHeight - scanLineHeight, 0, mHeight, new int[]{Color.TRANSPARENT, Color.WHITE}, null, Shader.TileMode.CLAMP);
return linearGradient;
}
/**
* 画笔重置
*
* @param color
* @param alpha
*/
private void resetPaint(int color, int alpha) {
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setDither(true);
mPaint.setColor(color);
mPaint.setAlpha(alpha);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = getWidth() / 2;
centerY = getHeight() / 2;
}
/**
* 获取裁剪后的圆形图片
*
* @param bmp
* @param radius
* @return
*/
private Bitmap getCroppedRoundBitmap(Bitmap bmp, int radius) {
Bitmap scaledSrcBmp;
int diameter = radius * 2;
// 为了防止宽高不相等,造成圆形图片变形,因此截取长方形中处于中间位置最大的正方形图片
int bmpWidth = bmp.getWidth();
int bmpHeight = bmp.getHeight();
int squareWidth, squareHeight;
int x, y;
Bitmap squareBitmap;
if (bmpHeight > bmpWidth) {// 高大于宽
squareWidth = squareHeight = bmpWidth;
x = 0;
y = (bmpHeight - bmpWidth) / 2;
// 截取正方形图片
squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,
squareHeight);
} else if (bmpHeight < bmpWidth) {// 宽大于高
squareWidth = squareHeight = bmpHeight;
x = (bmpWidth - bmpHeight) / 2;
y = 0;
squareBitmap = Bitmap.createBitmap(bmp, x, y, squareWidth,
squareHeight);
} else {
squareBitmap = bmp;
}
if (squareBitmap.getWidth() != diameter
|| squareBitmap.getHeight() != diameter) {
scaledSrcBmp = Bitmap.createScaledBitmap(squareBitmap, diameter,
diameter, true);
} else {
scaledSrcBmp = squareBitmap;
}
Bitmap output = Bitmap.createBitmap(scaledSrcBmp.getWidth(),
scaledSrcBmp.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
Paint paint = new Paint();
Rect rect = new Rect(0, 0, scaledSrcBmp.getWidth(),
scaledSrcBmp.getHeight());
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
canvas.drawARGB(0, 0, 0, 0);
canvas.drawCircle(scaledSrcBmp.getWidth() / 2,
scaledSrcBmp.getHeight() / 2, scaledSrcBmp.getWidth() / 2,
paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(scaledSrcBmp, rect, rect, paint);
return output;
}
@Override
protected void onDraw(Canvas canvas) {
if (!enableClipPathRound && !enableScan) {
super.onDraw(canvas);
} else {
if (enableClipPathRound) {
int radius = getWidth() > getHeight() ? getHeight() / 2 : getWidth() / 2;
radius -= outStrokeWidth / 2;
//绘制圆边
resetPaint(outStrokeColor, outStrokeAlpha);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(outStrokeWidth);
canvas.drawCircle(centerX, centerY, radius, mPaint);
//处理图片
Drawable drawable = getDrawable();
if (null != drawable) {
boolean isSameImg = true;
if (null != mLastDrawable) {
if (drawable.getConstantState() != mLastDrawable.getConstantState()) {
isSameImg = false;
mLastDrawable = drawable;
}
} else {
isSameImg = false;
mLastDrawable = drawable;
}
boolean needCreate = false;
Bitmap roundBitmap = null;
if (isSameImg) {
roundBitmap = null == mLastBitmap || null == mLastBitmap.get() ? null : mLastBitmap.get();
if (null == roundBitmap) {
needCreate = true;
}
} else {
needCreate = true;
}
if (needCreate) {
Bitmap mBitmap = null;
//防止引用上一次的缓存原图
boolean needCreateTemp=false;
if(isSameImg){
mBitmap = null == mTempBitmap || null == mTempBitmap.get() ? null : mTempBitmap.get();
if (null == mBitmap) {
needCreateTemp=true;
}
}else {
needCreateTemp=true;
}
if(needCreateTemp){
mBitmap = ((BitmapDrawable) mLastDrawable).getBitmap();
mTempBitmap = new WeakReference<>(mBitmap);
}
if (null != mBitmap) {
radius -= strokeWidth;
//裁剪图片为圆形
roundBitmap = getCroppedRoundBitmap(mBitmap, radius);
if (null != roundBitmap) {
mLastBitmap = new WeakReference<>(roundBitmap);
}
}
} else {
radius -= strokeWidth;
}
if (null != roundBitmap) {
canvas.drawBitmap(roundBitmap, centerX - radius, centerY - radius, null);
}
}
//裁剪画布
mPath.reset();
canvas.clipPath(mPath); // makes the clip empty
mPath.addCircle(centerX, centerY, centerX - strokeWidth - outStrokeWidth / 2, Path.Direction.CCW);
canvas.clipPath(mPath, Region.Op.REPLACE);
} else {
super.onDraw(canvas);
}
if (enableScan) {
//移动扫描白线的位置
mHeight += moveSpeed;
//绘制扫描线
resetPaint(Color.TRANSPARENT, 255);
mPaint.setXfermode(porterDuffXfermode);
mPaint.setShader(buildLinearGradient());
canvas.drawRect(0, mHeight - scanLineHeight, getWidth(), mHeight, mPaint);
if (mHeight >= getHeight()) {
mHeight = 0;
}
postInvalidateDelayed(invalidateTime);
}
}
}
}