开发中可能遇到这样的需求,要求某个图片或者视图显示在指定范围,这个范围如果是圆角矩形倒也好说,通过CardView等可以轻松实现。但如果这个范围是不规则图形,或者以指定切件为遮罩、作为显示范围,此时通过混合模式实现更为方便。
假设切件bg_res.png作为显示遮罩,tiger.png作为显示内容:
最终效果图如下,可以看到tiger以bg_res作为遮罩进行了显示。实际上这种需求通常背景切件和显示内容宽高比是一样的,但是也懒得找这样的美术切件了,反正原理是一样的。
代码实现上也比较简单,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>