持续创造,加速生长!这是我参加「日新计划 10 月更文应战」的第9天,点击查看活动概况
前言
为什么会想到做这一个选题?之前写到一篇九宫格动态列表谈论的软键盘弹出作用,主动定位到指定坐标的文章【传送门】。
由于咱们的九宫格控件现已上线4年左右了,最近开端要大改动态模块,所以需求捡起来从头修正一下,所以顺便整理了一下其时完结的阅历与坑点。
其时完结九宫格控件的时分参阅了市面上的许多Demo与开源的计划,的确有许多计划,可是大多数计划也仅仅完结了作用,当数据多了、类型多了之后很简单发生卡顿,比较严重还有高度丈量问题,图片加载紊乱等等问题。
索性我就把从开端选计划到最终成型的过程记载一下,希望多咱们有所启示。
由于时刻久远,其间还发现有性能问题换了一种计划,其时记载的笔记也比较粗糙,没有记载彻底,我尽量把我其时的思路讲清楚。
一、清晰需求,完结思路
由于咱们的需求比较特别,不止是展现老友动态,还能够发布论题,指定发布圈子,这些都跟九宫格控件自身不相关,可是在列表的展现类型上也不止九宫格展现,还需求圆角图片,九宫格投票的类型,和视频的类型等等。
所以咱们九宫格控件还需求能完结视频的展现,九宫格图片的展现与预览,还需求对投票的类型支撑。
假如要完结九宫格的作用,其实大致就两种计划,要么运用GridView,要么运用自定义ViewGroup。
假如是概况页面还能够运用GridView的办法,可是在列表中由于需求复用的问题,导致Grid的功率相对比较差,而且还有一些特别的展现需求,如4个图片并行摆放,所以咱们就直接考虑了自定义ViewGroup的办法。
怎么完结自定义ViewGroup呢?其实咱们完结了两个版别的迭代。一起来看看吧。
二、榜首版别,直接完结
榜首个版别的思路,其实便是一个类,ViewGroup在里面丈量,布局,并运用一个Map容器记载每一个九宫格的宽高特点,防止复用的时分快速丈量。
丈量代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!canMeasureLayout){ //首要是在onPause的时分不要再丈量了
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = 0;
int totalWidth = width - getPaddingLeft() - getPaddingRight();
if (mImageInfo != null && mImageInfo.size() > 0) {
Log.e("onMeasure","进入条件,准备丈量了");
//查看缓存假如有缓存的宽高,那么直接运用,不需求再次丈量,防止收回运用的时分gridWidth是收回的宽度形成丈量不精确
if (mCurPosition != -1 && WHCacheHelper.mAllWidth.get(mCurPosition) != null && WHCacheHelper.mAllWidth.get(mCurPosition) != 0) {
gridWidth = WHCacheHelper.mAllWidth.get(mCurPosition);
gridHeight = WHCacheHelper.mAllHeight.get(mCurPosition);
} else {
Log.e("onMeasure","进入条件,开端丈量了");
//没有缓存需求丈量,丈量完结之后需求缓存起来
if (mImageInfo.size() == 1) {
//只要一张图片的时分
columnCount = 1;
rowCount = 1;
//阐明是初始化,先尝试拿到url中的宽高
ImageInfo imageInfo = mImageInfo.get(0);
String thumbnailUrl = imageInfo.getThumbnailUrl();
int startIndex = thumbnailUrl.lastIndexOf("-");
int endIndex = thumbnailUrl.lastIndexOf(".");
if (startIndex != -1 && startIndex != 0 && endIndex != -1 && endIndex != 0 && endIndex - startIndex <= 10) {
String substring = thumbnailUrl.substring(startIndex + 1, endIndex);
if (!TextUtils.isEmpty(substring) && substring.contains("x")) {
String[] split = substring.split("x");
try {
handleWidthHeight(Integer.parseInt(split[0]), Integer.parseInt(split[1]), totalWidth);
} catch (NumberFormatException e) {
e.printStackTrace();
handleWidthHeight(0, 0, totalWidth);
}
} else {
handleWidthHeight(0, 0, totalWidth);
}
} else {
handleWidthHeight(0, 0, totalWidth);
}
} else {
//假如是多张图片,核算宽度,宽都按总宽度的 1/3 核算一个grid的宽高
gridWidth = gridHeight = (totalWidth - gridSpacing * 2) / 3;
}
//缓存每一个NineGridView的宽高
if (mCurPosition != -1) {
WHCacheHelper.mAllWidth.put(mCurPosition, gridWidth);
WHCacheHelper.mAllHeight.put(mCurPosition, gridHeight);
}
}
}
width = gridWidth * columnCount + gridSpacing * (columnCount - 1) + getPaddingLeft() + getPaddingRight();
height = gridHeight * rowCount + gridSpacing * (rowCount - 1) + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
}
由于需求咱们处理单个图片的状况与多个图片的状况,由于一行只展现三个Item,所以咱们能够核算公共多少行,再加上间距即可核算需求丈量的宽高。
首要是单个图片的丈量相对费事,需求拿到图片链接中的宽高特点,判别是否大于控件宽度,假如大于控件宽度则需求等于控件宽度,再对高度进行份额的缩放。
/**
* 处理丈量真实的宽高(单张图片的)
*/
private void handleWidthHeight(int width, int height, int totalWidth) {
if (width == 0) {
//假如没有带着分辨率,那么就按1:1的份额
gridWidth = singleImageSize;
gridHeight = singleImageSize;
} else {
//假如带着了分辨率,那么直接赋值
float width2 = width;
float height2 = height;
//依据不同的份额获取不同的宽高
singleImagefinalWidthHeight(width, height, width2 / height2);
}
}
/**
* 依据宽高比,从头核算最终的gridWidth,gridHeight
*/
private void singleImagefinalWidthHeight(float imgWidth, float imgHeight, float ratio) {
singleImageRatio = ratio;
int realImgWith = 0;
int realImgHeight = 0;
//设置独自相片的时分份额扩大,不要写死了-指定宽度高度。设置一个最小的长度,低于这个长度就依照这个长度
if (ratio >= 1.0f) {
//横长竖短 - 假如大于最大值,不做处理,图片展现为最大长度算
if (imgWidth >= singleImageSize) {
//假如在最大和最小值之间,那么按图片的自身的宽高
realImgWith = singleImageSize;
} else if (imgWidth > singleImageMinSize) {
realImgWith = (int) imgWidth;
} else {
//假如小于最小值,那么按最小的长度算
realImgWith = singleImageMinSize;
}
gridWidth = realImgWith;
gridHeight = (int) (realImgWith / ratio);
} else {
//竖长横短 - 和上面相同的逻辑
if (imgHeight >= singleImageSize) {
realImgHeight = singleImageSize;
} else if (imgHeight > singleImageMinSize) {
realImgHeight = (int) imgHeight;
} else {
realImgHeight = singleImageMinSize;
}
gridWidth = (int) (realImgHeight * ratio);
gridHeight = realImgHeight;
}
}
首要需求处理的便是单个图片的宽高与份额,除了丈量之外,ViewGroup最重要的便是给子View布局了:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (!canMeasureLayout) return; //首要是在onPause的时分不要再布局了
Boolean aBoolean = WHCacheHelper.mAllPositionLayouted.get(mCurPosition);
if (aBoolean != null && aBoolean) { //假如现已丈量过了,有缓存标识,那么不要再次丈量了
return;
}
if (mImageInfo == null) {
return;
}
Log.e("onLayout","进入条件,开端布局了");
int childrenCount = mImageInfo.size();
for (int i = 0; i < childrenCount; i++) {
final ImageView childrenView = (ImageView) getChildAt(i);
//先清除收回img上面的原有bitmap缓存
childrenView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_default_color));
int rowNum = i / columnCount;
int columnNum = i % columnCount;
int left = (gridWidth + gridSpacing) * columnNum + getPaddingLeft();
int top = (gridHeight + gridSpacing) * rowNum + getPaddingTop();
int right = left + gridWidth;
int bottom = top + gridHeight;
childrenView.layout(left, top, right, bottom);
//处理图片的加载,并动态的处理只要一张图片的时分的宽高份额
if (mImageLoader != null) {
mImageLoader.onDisplayImage(getContext(), childrenView, mImageInfo.get(i).thumbnailUrl);
}
WHCacheHelper.mAllPositionLayouted.clear();
WHCacheHelper.mAllPositionLayouted.put(mCurPosition, true);
}
}
这个是依据行与列核算每一个Item的位置,而且运用图片加载引擎去加载图片。
其实比较难操控的便是对缓存类的操控
public class WHCacheHelper {
public static Map<Integer, Integer> mAllWidth = new HashMap<>();
public static Map<Integer, Integer> mAllHeight = new HashMap<>();
}
由于运用缓存很简单导致犯错,导致一些状况下缓存的宽高不对,从而导致偶现的作用错乱的问题。假如去掉自定义的缓存就能够完结作用,可是滑动还是会卡顿,大致原因便是在丈量中进行了复杂的逻辑校验与判别,导致耗时相对比较多,形成列表的卡顿。
所以才有了之前说的榜首个版别上线之后横竖翻滚卡顿,才有了后边的一个版别重做。
三、第二版别优化完结,解耦逻辑
榜首个版别上线的作用并不是很好,而且由于后边的版别上线了谈论,投票等其他九宫格的办法,所以迭代更新了第二个版别。
咱们需求把九宫格的逻辑提取出来作为一个基类的笼统类,而不同的布局类型去完结不同的布局,不止是图片,关于一些自定义的布局也能做成九宫格的方法去展现,扩展性也更好一点。
/**
* 笼统九宫格-具体的布局和丈量在这里完结
*/
public abstract class AbstractNineGridLayout<T> extends ViewGroup {
private static final int MAX_CHILDREN_COUNT = 9;
private int itemWidth;
private int itemHeight;
private int horizontalSpacing;
private int verticalSpacing;
private boolean singleMode;
private boolean fourGridMode;
private int singleWidth;
private int singleHeight;
private boolean singleModeOverflowScale;
public AbstractNineGridLayout(Context context) {
this(context, null);
}
public AbstractNineGridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NineGridLayout);
int spacing = a.getDimensionPixelSize(R.styleable.NineGridLayout_spacing, 0);
horizontalSpacing = a.getDimensionPixelSize(R.styleable.NineGridLayout_horizontal_spacing, spacing);
verticalSpacing = a.getDimensionPixelSize(R.styleable.NineGridLayout_vertical_spacing, spacing);
singleMode = a.getBoolean(R.styleable.NineGridLayout_single_mode, true);
fourGridMode = a.getBoolean(R.styleable.NineGridLayout_four_gird_mode, true);
singleWidth = a.getDimensionPixelSize(R.styleable.NineGridLayout_single_mode_width, 0);
singleHeight = a.getDimensionPixelSize(R.styleable.NineGridLayout_single_mode_height, 0);
singleModeOverflowScale = a.getBoolean(R.styleable.NineGridLayout_single_mode_overflow_scale, true);
a.recycle();
}
//优先填充布局,再执行丈量和制作
fillChildView();
}
/**
* 设置显现的数量
*/
public void setDisplayCount(int count) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).setVisibility(i < count ? VISIBLE : GONE);
}
}
/**
* 设置独自布局的宽和高
*/
public void setSingleModeSize(int w, int h) {
if (w != 0 && h != 0) {
this.singleWidth = w;
this.singleHeight = h;
}
}
/**
* 一般用这个办法填充布局,每一个小布局的布局文件
*/
protected void inflateChildLayout(int layoutId) {
removeAllViews();
for (int i = 0; i < MAX_CHILDREN_COUNT; i++) {
LayoutInflater.from(getContext()).inflate(layoutId, this);
}
}
/**
* 回来每一个小布局的内部控件ID,用数组包装回来
*/
@SuppressWarnings("unchecked")
protected <V extends View> V[] findInChildren(int viewId, Class<V> clazz) {
V[] result = (V[]) Array.newInstance(clazz, getChildCount());
for (int i = 0; i < result.length; i++) {
result[i] = (V) getChildAt(i).findViewById(viewId);
}
return result;
}
//子类去完结-填充布局文件
protected abstract void fillChildView();
//子类去完结-对布局文件赋值数据(一般专门去给adapter去调用的)
public abstract void renderData(T data);
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int notGoneChildCount = getNotGoneChildCount();
if (notGoneChildCount == 1 && singleMode) {
itemWidth = singleWidth > 0 ? singleWidth : widthSize;
itemHeight = singleHeight > 0 ? singleHeight : widthSize;
if (itemWidth > widthSize && singleModeOverflowScale) {
itemWidth = widthSize; //单张图片先定宽度。
itemHeight = (int) (widthSize * 1f / singleWidth * singleHeight); //依据宽度核算高度
}
} else {
itemWidth = (widthSize - horizontalSpacing * 2) / 3;
itemHeight = itemWidth;
}
//丈量子布局
measureChildren(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY));
if (heightMode == MeasureSpec.EXACTLY) {
setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
} else {
notGoneChildCount = Math.min(notGoneChildCount, MAX_CHILDREN_COUNT);
int height = ((notGoneChildCount - 1) / 3 + 1) * (itemHeight + verticalSpacing) - verticalSpacing + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int notGoneChildCount = getNotGoneChildCount();
int position = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
int row = position / 3;
int col = position % 3;
if (notGoneChildCount == 4 && fourGridMode) {
row = position / 2;
col = position % 2;
}
int x = col * itemWidth + getPaddingLeft() + horizontalSpacing * col;
int y = row * itemHeight + getPaddingTop() + verticalSpacing * row;
child.layout(x, y, x + itemWidth, y + itemHeight);
//最多只摆放9个
position++;
if (position == MAX_CHILDREN_COUNT) {
break;
}
}
}
//获取真实显现的子布局
private int getNotGoneChildCount() {
int childCount = getChildCount();
int notGoneCount = 0;
for (int i = 0; i < childCount; i++) {
if (getChildAt(i).getVisibility() != View.GONE) {
notGoneCount++;
}
}
return notGoneCount;
}
}
为了方便咱们观看,先把全部的代码贴出。
第二个计划和榜首个计划的区别:
- 九宫格子View的填充办法露出出去,让子类自在完结,可所以控件也可所以布局
- 把九宫格子View的数据填充办法露出,子类自在的设置数据类型
- 对子View中的控件放入数组中管理,有多少个子View就有多少个数量
- 子类自在设置数据类型,方便对每一个子View的操控
- 对独自图片的宽高提取预取到目标中,没有在丈量中进行耗时逻辑
- 对独自图片的宽高最大值进行限制,而并非一股脑的控件宽度
此办法的丈量和布局办法其实是和榜首种计划差不多的,仅仅把数据预处理放在请求数据之后了,把缓存的逻辑去掉了。由于丈量功率比较高不需求缓存也能满意了。(当然假如想提高功率也能自己拓宽完结宽高的缓存)
怎么运用呢?
比如咱们默许的图片九宫格,咱们就能够直接承继这个基类。
/**
* 默许的图片九宫格
*/
public class ImageViewNineGridLayout extends AbstractNineGridLayout<List<ImageInfo>> {
private ImageView[] imageViews;
private final ImageLoader mImageLoader;
public ImageViewNineGridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mImageLoader = new NineGlideLoader();
}
@Override
protected void fillChildView() {
inflateChildLayout(R.layout.item_image_grid);
imageViews = findInChildren(R.id.iv_image, ImageView.class);
}
@Override
public void renderData(List<ImageInfo> imageInfos) {
setSingleModeSize(imageInfos.get(0).getImageViewWidth(), imageInfos.get(0).getImageViewHeight());
setDisplayCount(imageInfos.size());
for (int i = 0; i < imageInfos.size(); i++) {
String url = imageInfos.get(i).getThumbnailUrl();
ImageView imageView = imageViews[i];
//运用自定义的Loader加载
mImageLoader.onDisplayImage(getContext(), imageView, url);
//点击事情
setClickListener(imageView, i, imageInfos);
}
}
//设置内部每一个图片的点击事情,跳转到预览页面
private void setClickListener(ImageView imageView, int position, List<ImageInfo> imageInfos) {
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
List<Object> list = new ArrayList<>();
for (ImageInfo imageInfo : imageInfos) {
if (imageInfo != null) {
list.add(imageInfo.getThumbnailUrl());
}
}
if (mListener != null) {
mListener.onPreview(imageViews, position, list);
}
}
});
}
private OnPreViewListener mListener;
public void setOnPreViewListener(OnPreViewListener listener) {
mListener = listener;
}
public interface OnPreViewListener {
void onPreview(ImageView[] imageViews, int position, List<Object> imageInfos);
}
}
由于布局中便是一个ImageView,所以很简单,只需求供给数据,然后在数据填充的办法中运用图片加载引擎去加载图片即可。
作用如图:(本图片为本地测试数据,无任何特别意义)
而假如是投票的类型,咱们就需求增加一个布局,需求确定投票的选项与勾勾的选择逻辑。
/**
* 新版别投票的九宫格
*/
public class BallotNineGridLayout extends AbstractNineGridLayout<List<ImageInfo>> {
private MyOptionImageView[] imageViews;
private final ImageLoader mImageLoader;
//默许在xml中运用
public BallotNineGridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
mImageLoader = new NineGlideLoader();
}
@Override
protected void fillChildView() {
inflateChildLayout(R.layout.item_ballot_image_grid);
imageViews = findInChildren(R.id.iv_image, MyOptionImageView.class);
}
@Override
public void renderData(List<ImageInfo> imageInfos) {
setSingleModeSize(imageInfos.get(0).getImageViewWidth(), imageInfos.get(0).getImageViewHeight());
setDisplayCount(imageInfos.size());
for (int i = 0; i < imageInfos.size(); i++) {
String url = imageInfos.get(i).getThumbnailUrl();
MyOptionImageView optionImageView = imageViews[i];
//运用自定义的Loader加载
mImageLoader.onDisplayImage(getContext(), optionImageView, url);
//设置点击事情
setClickListener(optionImageView, i);
}
}
//设置内部每一个图片的点击事情
private void setClickListener(MyOptionImageView optionImageView, int position) {
optionImageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mListener != null) mListener.onInnerClick(position);
}
});
}
private OnInnerClickListener mListener;
public void setOnInnerClickListener(OnInnerClickListener listener) {
mListener = listener;
}
public interface OnInnerClickListener {
void onInnerClick(int innerPosition);
}
//获取到指定索引的投票图片控件
public MyOptionImageView getOptionImageView(int index) {
if (index >= imageViews.length) return null;
return imageViews[index];
}
}
这里我是把投票的View做了一个封装,所以跟图片九宫格有点相似,当然假如直接运用自定义的投票布局也能相同的完结的。
完结的作用大致如下:(本图片为本地测试数据,无任何特别意义)
假如想扩展的话,能够自己承继笼统类,填充自己的layout,这样的话今后不管是什么样的布局,只要是以九宫格的办法展现,都能够运用自定义的布局办法来完结了。
四、其他留意事项
当前真实完结一个九宫格列表还有其他一些细枝末节的点,比如圆角,点击覆盖作用,预览作用。
比如咱们的图片都是圆角的,而且加了点击的暗影作用。
public class NineGridViewWrapper extends CustomRoundImageView {
private int moreNum = 0;
private int maskColor = 0x88000000;
public NineGridViewWrapper(Context context) {
this(context, null);
}
public NineGridViewWrapper(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NineGridViewWrapper(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setFocusable(false);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Drawable drawable = getDrawable();
if (drawable != null) {
drawable.setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY);
ViewCompat.postInvalidateOnAnimation(this);
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
Drawable drawableUp = getDrawable();
if (drawableUp != null) {
drawableUp.clearColorFilter();
ViewCompat.postInvalidateOnAnimation(this);
}
break;
}
return super.onTouchEvent(event);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
setImageDrawable(null);
}
}
比如如安在九宫格的最后一个子View上显现剩余图片数量的文本,也能够直接修正上面的类,在ImageView上面制作。
比如选中与未选中的图片的drawable的制作,直接在ImageView上面制作,和上面的计划都是相同的思路去完结。
而需求留意的是,自定义Item的宽高缓存需求慎重,假如想持续优化速度,其实咱们也能够参加Item宽高缓存,在丈量的时分直接赋值,会稍微加快处理的速度,可是记住在onLayout的时分记得一定要从头加载数据。
还有便是在列表上下翻滚的时分有复用的问题,记住图片最好在加载前清除之前的图片并展现站位图,不然可能会呈现九宫格其间的一张图片显现复用的图片,假如运用得其时能够加速丈量速度,假如运用不当反倒会负优化。
大致运行环境如下:(本图片为本地测试数据,无任何特别意义)
录制GIF软件帧数比较低,咱们理解即可。
总结
总的来说九宫格控件的自定义并不难,首要便是ViewGroup的自定义,怎么丈量,怎么布局子View,能够说是比较规范的丈量与布局示例了,然后便是对一些子View的抽取,子View的数据赋值的抽取,便是一个比较完善的自定义布局了。
本文的中心代码都现已在文中贴出了,想要能够自取即可。
当然了,这种计划可能也仅仅闭门造车,还需求咱们提提意见,假如你有更好的计划,或者优化的空间都也能够一起沟通一下。如有讹夺的当地还请指出,假如有疑问也能够在谈论区咱们一起讨论哦。
假如感觉本文对你有一点点的启示,还望你能点赞
支撑一下,你的支撑是我最大的动力。
Ok,这一期就此完结。