Android 切件作为ViewGroup显示范围

546 阅读2分钟

      开发中可能遇到这样的需求,要求某个图片或者视图显示在指定范围,这个范围如果是圆角矩形倒也好说,通过CardView等可以轻松实现。但如果这个范围是不规则图形,或者以指定切件为遮罩、作为显示范围,此时通过混合模式实现更为方便。

      假设切件bg_res.png作为显示遮罩,tiger.png作为显示内容:

Image Image
      最终效果图如下,可以看到tiger以bg_res作为遮罩进行了显示。实际上这种需求通常背景切件和显示内容宽高比是一样的,但是也懒得找这样的美术切件了,反正原理是一样的。

image.png
      代码实现上也比较简单,Android绘制体系中,ViewGroup会调用dispatchDraw方法触发子view的绘制,我们只需要拿到子view的绘制内容然后与遮罩切件做混合即可。下面给出了实现代码,还有些地方可以优化,比如获取遮罩bgBitmap最好放到子线程等。如果有更多优化建议,也可以在评论区里指出。

public class BitmapClipFrameLayout extends FrameLayout {  
  
private int resId = -1;  
private final Paint paint = new Paint();  
private final Rect srcRect = new Rect();  
private final Rect dstRect = new Rect();  
  
private final Rect srcRect2 = new Rect();  
private final PorterDuffXfermode duffXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);  
private Bitmap bgBitmap;  
private Bitmap viewBitmap;  
private boolean detached;  
  
public BitmapClipFrameLayout(@NonNull Context context) {  
this(context, null);  
}  
  
public BitmapClipFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {  
this(context, attrs, 0);  
}  
  
public BitmapClipFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {  
super(context, attrs, defStyleAttr);  
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.BitmapClipFrameLayout);  
resId = typedArray.getResourceId(R.styleable.BitmapClipFrameLayout_clip_res_id, R.drawable.transparent);  
typedArray.recycle();  
  
init();  
}  
  
private void init() {  
if (resId == -1) {  
return;  
}  
  
setLayerType(View.LAYER_TYPE_SOFTWARE, null); // 关闭硬件加速  
paint.setDither(true);  
paint.setAntiAlias(true);  
  
bgBitmap = BitmapFactory.decodeResource(getResources(), resId);  
if (detached) {  
recycleBitmap(bgBitmap);  
return;  
}  
if (isBitmapUseful(bgBitmap)) {  
srcRect.set(0, 0, bgBitmap.getWidth(), bgBitmap.getHeight());  
postInvalidate();  
}  
}  
  
@Override  
protected void dispatchDraw(Canvas canvas) {  
if (isBitmapUseful(bgBitmap)) {  
dstRect.set(0, 0, getWidth(), getHeight());  
int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);  
paint.setXfermode(null);  
canvas.drawBitmap(bgBitmap, srcRect, dstRect, paint);  
  
recycleBitmap(viewBitmap);  
viewBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);  
srcRect2.set(0, 0, viewBitmap.getWidth(), viewBitmap.getHeight());  
Canvas viewCanvas = new Canvas(viewBitmap);  
super.dispatchDraw(viewCanvas);  
  
paint.setXfermode(duffXfermode);  
canvas.drawBitmap(viewBitmap, srcRect2, dstRect, paint);  
paint.setXfermode(null);  
  
canvas.restoreToCount(saveCount);  
} else {  
super.dispatchDraw(canvas);  
}  
}  
  
@Override  
protected void onDetachedFromWindow() {  
super.onDetachedFromWindow();  
detached = true;  
recycleBitmap(bgBitmap);  
recycleBitmap(viewBitmap);  
}  
  
private boolean isBitmapUseful(Bitmap bitmap) {  
return bitmap != null && !bitmap.isRecycled();  
}  
  
private void recycleBitmap(Bitmap bitmap) {  
if (isBitmapUseful(bitmap)) {  
bitmap.recycle();  
}  
}  
}
<com.nft.customview.view.BitmapClipFrameLayout  
android:layout_width="160dp"  
android:layout_height="120dp"  
app:clip_res_id="@drawable/bg_res">  
  
<ImageView  
android:layout_width="match_parent"  
android:layout_height="match_parent"  
android:scaleType="centerInside"  
android:src="@drawable/tiger" />
<declare-styleable name="BitmapClipFrameLayout">  
<attr name="clip_res_id" format="reference"/>  
</declare-styleable>