妙用布局代码xml进行RecyclerView.ItemDecoration的绘制
- 小伙伴是不是经常用RecyclerView.ItemDecoration来做分割线,实际上ItemDecoration除了做分割线,也可以做其他很多东西,比如微信聊天页里的时间或者系统消息显示
- 如果我们的布局比较复杂,直接用ItemDecoration的canvas去绘制会非常麻烦,后期要调整UI也是很麻烦,有没有更简单的方法呢
验证版
- 实际上我们可以利用LayoutInflater得到xml布局里的view,再利用View.getDrawingCache()方法获得bitmap,最后canvas绘制这个bitmap就可以了
- 但比较遗憾的是我们看到View.getDrawingCache()已经被声明过时了,而且View的内容如果是变化的,那么也要想办法解决实时更新的问题,还有Bitmap的管理也是个问题,否则内存可能就会暴涨或抖动
- 对于View.getDrawingCache()已经过时的问题,其实我们可以自己创建bitmap并绘制,这并不复杂也不难实现
- view的内容要实时更新的话,其实也就是重新测量并布局的过程,这个也并不复杂
- 至于Bitmap大内存的问题,可以考虑使用缓存复用的思路来解决
- 上面的问题都有一定解决思路后,经过试验,确实可行,代码后面提供,可供参考
- 以后想要绘制复杂的ItemDecoration,那么只要搞定xml就行了
- 比如想要在列表里绘制时间显示,那么我们可以按照自己的要求写好布局xml,参考如下,后期UI上有任何变动和微调,就是简单的改改xml的问题了,完全不用动到canvas绘制那块的代码
- item_time_bar.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="12dp"
tools:background="@color/white">
<View
android:layout_width="0dp"
android:layout_height="0.33dp"
android:layout_weight="1"
android:background="@color/black_20" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:textColor="@color/black_40"
tools:text="2020/11/11 - 2020/11/20" />
<View
android:layout_width="0dp"
android:layout_height="0.33dp"
android:layout_weight="1"
android:background="@color/black_20" />
</LinearLayout>
- 最终为了通用一点,我把公共的进行了抽取,用户使用的时候只要继承它就能很方便的使用了
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.TextPaint;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
public abstract class BaseLayoutItemDecoration extends RecyclerView.ItemDecoration {
LruCache<String, Bitmap> mBitmapCache;
private View mItemView;
private int mItemHeight;
private int mParentRestMeasureWidth;
private int mParentRestMeasureHeight;
private Paint mDefaultPaint;
public BaseLayoutItemDecoration() {
initDefaultPaint();
initBitmapCache();
}
public abstract int getItemDecorationLayoutResId();
public abstract void initItemDecorationLayoutViews(@NonNull View mItemView);
@CallSuper
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (mItemView == null) {
setCurrentItemDecorationRestMeasureWidth(getRestMeasureWidth(parent));
setCurrentItemDecorationRestMeasureHeight(getRestMeasureHeight(parent));
mItemView = LayoutInflater.from(parent.getContext()).inflate(getItemDecorationLayoutResId(), parent, false);
initItemDecorationLayoutViews(mItemView);
}
}
public int getRestMeasureWidth(@NonNull View view) {
return view.getMeasuredWidth() - view.getPaddingEnd() - view.getPaddingStart();
}
public int getRestMeasureHeight(@NonNull View view) {
return view.getMeasuredHeight() - view.getPaddingTop() - view.getPaddingBottom();
}
public void setCurrentItemDecorationRestMeasureWidth(int width) {
mParentRestMeasureWidth = width;
}
public void setCurrentItemDecorationRestMeasureHeight(int height) {
mParentRestMeasureHeight = height;
}
public View getItemDecorationView() {
return mItemView;
}
public Paint getDefaultPaint() {
return mDefaultPaint;
}
public int getItemDecorationHeight() {
return mItemHeight;
}
public int getCurrentItemDecorationHeight() {
notifyItemDecorationChanged();
return mItemHeight;
}
public void notifyItemDecorationChanged() {
measureAndLayout();
}
public void measure(@NonNull View view, int restWidth, int restHeight) {
int widthMeasureMode = View.MeasureSpec.EXACTLY;
int heightMeasureMode = View.MeasureSpec.EXACTLY;
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp == null) {
view.measure(View.MeasureSpec.makeMeasureSpec(restWidth, widthMeasureMode),
View.MeasureSpec.makeMeasureSpec(restHeight, heightMeasureMode));
return;
}
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
widthMeasureMode = View.MeasureSpec.AT_MOST;
}
if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
heightMeasureMode = View.MeasureSpec.AT_MOST;
}
view.measure(View.MeasureSpec.makeMeasureSpec(restWidth, widthMeasureMode),
View.MeasureSpec.makeMeasureSpec(restHeight, heightMeasureMode));
}
@Deprecated
@Nullable
public Bitmap getBitmap(@NonNull View v) {
int width = v.getWidth();
int height = v.getHeight();
if (width <= 0 || height <= 0) {
return null;
}
Bitmap b = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(b);
v.draw(c);
return b;
}
@Nullable
public Bitmap getBitmapWithCache(@NonNull View v, int cacheIndex) {
int width = v.getWidth();
int height = v.getHeight();
if (width <= 0 || height <= 0) {
return null;
}
String key = getBitmapCacheKey(cacheIndex, v.getWidth(), v.getHeight());
Bitmap cache = mBitmapCache.get(key);
if (cache == null || cache.isRecycled()) {
cache = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
mBitmapCache.put(key, cache);
}
Canvas c = new Canvas(cache);
cache.eraseColor(Color.TRANSPARENT);
v.draw(c);
return cache;
}
public void drawLayoutItemDecorationWithCache(@NonNull Canvas c, @NonNull Paint paint, float left, float top, int cacheIndex) {
Bitmap bitmap = getBitmapWithCache(mItemView, cacheIndex);
if (bitmap != null) {
c.drawBitmap(bitmap, left, top, paint);
}
}
@Deprecated
public void drawLayoutItemDecorationWithoutCache(@NonNull Canvas c, @NonNull Paint paint, float left, float top) {
Bitmap bitmap = getBitmap(mItemView);
if (bitmap != null) {
c.drawBitmap(bitmap, left, top, paint);
bitmap.recycle();
}
}
private void initDefaultPaint() {
mDefaultPaint = new TextPaint();
mDefaultPaint.setAntiAlias(true);
mDefaultPaint.setDither(true);
}
private String getBitmapCacheKey(int cacheIndex, int width, int height) {
return cacheIndex + "_" + width + "_" + height;
}
private void measureAndLayout() {
measure(mItemView, mParentRestMeasureWidth, mParentRestMeasureHeight);
mItemHeight = mItemView.getMeasuredHeight();
mItemView.layout(0, 0, mItemView.getMeasuredWidth(), mItemView.getMeasuredHeight());
}
private void initBitmapCache() {
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 16;
mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount() / 1024;
}
@Override
protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
if (oldValue != null && !oldValue.isRecycled()) {
oldValue.recycle();
}
}
};
}
}
优化版
- 上面的方案由于引入了bitmap,始终是个麻烦,占用内存比较大
- 后面决定弃用bitmap方案,改用View.draw(Canvas c)这种方案,这个方案主要的难点就是canvas的位置偏移问题,解决掉这个难点,画面才能绘制在正确的地方
- 我们只要计算出要绘制的区域的左上角坐标,然后利用Canvas.translate即可位移到指定坐标,再调用View.draw方法即可
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.CallSuper;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public abstract class BaseLayoutOrderItemDecoration extends RecyclerView.ItemDecoration {
public View mItemView;
private int mItemHeight;
private int mItemWidth;
private int mParentRestMeasureWidth;
private int mParentRestMeasureHeight;
public BaseLayoutOrderItemDecoration() {
}
@LayoutRes
public abstract int getItemDecorationLayoutResId();
public abstract void initItemDecorationLayoutViews(@NonNull View itemView);
@CallSuper
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (mItemView == null) {
mItemView = LayoutInflater.from(parent.getContext()).inflate(getItemDecorationLayoutResId(), parent, false);
initItemDecorationLayoutViews(mItemView);
setCurrentItemDecorationRestMeasureWidth(getRestMeasureWidth(parent));
setCurrentItemDecorationRestMeasureHeight(getRestMeasureHeight(parent));
}
}
public Rect getDecoratedBoundsWithMargins(RecyclerView.LayoutManager layoutManager, View child) {
Rect rect = new Rect();
layoutManager.getDecoratedBoundsWithMargins(child, rect);
return rect;
}
public Rect getCurrentOrderDecoratedBoundsWithMargins(@NonNull View child, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
int left = child.getLeft() - lp.getMarginStart();
int top = child.getTop() - lp.topMargin;
int right = child.getRight() + lp.getMarginEnd();
int bottom = child.getBottom() + lp.bottomMargin;
Rect childPosition = new Rect(left, top, right, bottom);
Rect distanceRect = getCurrentOrderDecorationDistance(child, parent, state);
childPosition.left -= distanceRect.left;
childPosition.top -= distanceRect.top;
childPosition.right += distanceRect.right;
childPosition.bottom += distanceRect.bottom;
return childPosition;
}
public Rect getCurrentOrderDecorationDistance(@NonNull View child, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
Rect rect = new Rect();
int itemDecorCount = parent.getItemDecorationCount();
Rect tmp = new Rect();
for (int i = 0; i < itemDecorCount; i++) {
RecyclerView.ItemDecoration id = parent.getItemDecorationAt(i);
tmp.setEmpty();
id.getItemOffsets(tmp, child, parent, state);
rect.left += tmp.left;
rect.top += tmp.top;
rect.right += tmp.right;
rect.bottom += tmp.bottom;
if (id == this) {
break;
}
}
return rect;
}
public int getCurrentOrderDecoratedMeasuredHeight(@NonNull View child, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
Rect distanceRect = getCurrentOrderDecorationDistance(child, parent, state);
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
return child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin + distanceRect.top + distanceRect.bottom;
}
public int getCurrentOrderDecoratedMeasuredWidth(@NonNull View child, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
Rect distanceRect = getCurrentOrderDecorationDistance(child, parent, state);
ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) child.getLayoutParams();
return child.getMeasuredWidth() + mlp.getMarginStart() + mlp.getMarginEnd() + distanceRect.left + distanceRect.right;
}
public int getRestMeasureWidth(@NonNull View view) {
return view.getMeasuredWidth() - view.getPaddingEnd() - view.getPaddingStart();
}
public int getRestMeasureHeight(@NonNull View view) {
return view.getMeasuredHeight() - view.getPaddingTop() - view.getPaddingBottom();
}
public void setCurrentItemDecorationRestMeasureWidth(int width) {
mParentRestMeasureWidth = width;
}
public void setCurrentItemDecorationRestMeasureHeight(int height) {
mParentRestMeasureHeight = height;
}
public View getItemDecorationView() {
return mItemView;
}
public int getItemDecorationHeight() {
notifyItemDecorationChanged();
return mItemHeight;
}
public int getItemDecorationWidth() {
notifyItemDecorationChanged();
return mItemWidth;
}
public void notifyItemDecorationChanged() {
measureAndLayout();
}
public void measure(@NonNull View view, int restWidth, int restHeight) {
int widthMeasureMode = View.MeasureSpec.EXACTLY;
int heightMeasureMode = View.MeasureSpec.EXACTLY;
ViewGroup.LayoutParams lp = view.getLayoutParams();
if (lp == null) {
view.measure(View.MeasureSpec.makeMeasureSpec(restWidth, widthMeasureMode),
View.MeasureSpec.makeMeasureSpec(restHeight, heightMeasureMode));
return;
}
if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
widthMeasureMode = View.MeasureSpec.AT_MOST;
} else if (lp.width > 0) {
restWidth = Math.min(lp.width, restWidth);
}
if (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
heightMeasureMode = View.MeasureSpec.AT_MOST;
} else if (lp.height > 0) {
restHeight = Math.min(lp.height, restHeight);
}
view.measure(View.MeasureSpec.makeMeasureSpec(restWidth, widthMeasureMode),
View.MeasureSpec.makeMeasureSpec(restHeight, heightMeasureMode));
}
public void drawDecorationViewOnLeftOrTop(@NonNull Canvas canvas, @NonNull View child, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
canvas.save();
Rect rect = getCurrentOrderDecoratedBoundsWithMargins(child, parent, state);
canvas.translate(rect.left, rect.top);
getItemDecorationView().draw(canvas);
canvas.restore();
}
public void drawDecorationViewOnRight(@NonNull Canvas canvas, @NonNull View child, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
canvas.save();
Rect rect = getCurrentOrderDecoratedBoundsWithMargins(child, parent, state);
canvas.translate(rect.right - getItemDecorationWidth(), rect.top);
getItemDecorationView().draw(canvas);
canvas.restore();
}
public void drawDecorationViewOnBottom(@NonNull Canvas canvas, @NonNull View child, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
canvas.save();
Rect rect = getCurrentOrderDecoratedBoundsWithMargins(child, parent, state);
canvas.translate(rect.left, rect.bottom - getItemDecorationHeight());
getItemDecorationView().draw(canvas);
canvas.restore();
}
private void measureAndLayout() {
measure(mItemView, mParentRestMeasureWidth, mParentRestMeasureHeight);
mItemHeight = mItemView.getMeasuredHeight();
mItemWidth = mItemView.getMeasuredWidth();
mItemView.layout(0, 0, mItemView.getMeasuredWidth(), mItemView.getMeasuredHeight());
}
}
参考