目录
1.需求分析
首先来看钉钉--发现--圈子,实际多图展示效果
-
单图时限定最大高度及最大宽度,按比例缩放宽高,四周有圆角,ImageView的ScaleType为默认值
-
2-9张图片时,为大小图混合排列,父view四周有圆角,ImageView的ScaleType为centerCrop
2.功能实现
实现效果
实现思路
自定义 NineGridView 继承于 CardView ,添加10个ImageView(其中一个为单图专用ImageView)到该View中,接下来
1)确定ImageView宽高
重写onMeasure方法:获取当前View可用宽高,根据图片数量设置子View大小,根据图片数量及子View大小确定当前View高度,通过setMeasuredDimension存储测量的宽度和测量的高度
2)确定ImageView摆放位置
重写onLayout方法:根据图片数量设置子view摆放位置
3)设置间距及圆角
2.1 确定ImageView宽高
设当前View可展示宽高: width , height
图片间间隔: space
由钉钉圈子实际展示效果可知,当图片数量大于1时,小图宽高均为1:1,且小图宽高等于九张图时的宽高
可得小图边长为
baseLength = ( width - 2*space ) / 3
下面根据图片数量确定ImageView宽高:
1张图片 (限定最大高度及最大宽度,宽高按比例缩放)
imageViewWidth = wrap_content
imageViewHeight = wrap_content
imageViewMaxWidth = width
imageViewMaxHeight = 设定值/默认值
2张图片 (横向排列,对分父view宽度)
imageViewWidth = ( width - space ) / 2
imageViewHeight = imageViewWidth
3张图片 (大图在左,两张小图在右竖向排列)
两张小图宽高
imageViewWidth = baseLength
imageViewHeight = baseLength
大图宽高
maxImageViewWidth = width - space - baseLength
maxImageViewHeight = 2*baseLength + space
4张图片 (大图在上,三张小图在下横向排列)
大图宽高
maxImageViewWidth = width
maxImageViewHeight = width - space - baseLength
三张小图宽高
imageViewWidth = baseLength
imageViewHeight = baseLength
5张图片 (两张大图片在上横向排列,三张小图在下横向排列)
两张大图宽高
maxImageViewWidth = ( width - space ) / 2
maxImageViewHeight = maxImageViewWidth
三张小图宽高
imageViewWidth = baseLength
imageViewHeight = baseLength
6张图片 (大图在左上角,五张图片围绕大图排列)
五张小图宽高
imageViewWidth = baseLength
imageViewHeight = baseLength
大图宽高
maxImageViewWidth = width - space - baseLength
maxImageViewHeight = 2*baseLength + space
7张图片 (大图在上,六张小图分两行在下横向排列)
六张小图宽高
imageViewWidth = baseLength
imageViewHeight = baseLength
大图宽高
maxImageViewWidth = width
maxImageViewHeight = 1.5*baseLength
8张图片 (两张大图片在上横向排列,六张小图分两行在下横向排列)
六张小图宽高
imageViewWidth = baseLength
imageViewHeight = baseLength
两张大图宽高
maxImageViewWidth = ( width - space ) / 2
maxImageViewHeight = maxImageViewWidth
9张图片
imageViewWidth = baseLength
imageViewHeight = baseLength
具体代码实现为NineGridView中的measureChildView方法
2.2 确定ImageView摆放位置
/**
* 子View位置摆放
*/
private void layoutChildView() {
//布局时要考虑 padding
switch (mUrls.size()) {
default:
case 0:
case 1:
break;
case 2:
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
if (i < 2) {
int width = childView.getMeasuredWidth();
childView.layout(mPaddingLeft + (i * (width + mSpace + mOverSpace)),
mPaddingTop,
mPaddingLeft + (i * (width + mSpace + mOverSpace)) + width,
mPaddingTop + width);
} else {
childView.layout(0, 0, 0, 0);
}
}
break;
case 3:
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
if (i < 3) {
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (i == 0) {
childView.layout(mPaddingLeft, mPaddingTop, width + mPaddingLeft, mPaddingTop + height);
} else {
int count = i - 1;
childView.layout(mPaddingLeft + mMeasuredShowWidth - width,
mPaddingTop + (count * mSpace) + (count * height),
mPaddingLeft + mMeasuredShowWidth,
mPaddingTop + ((count + 1) * height) + (count * mSpace));
}
} else {
childView.layout(0, 0, 0, 0);
}
}
break;
case 4:
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
if (i < 4) {
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (i == 0) {
childView.layout(mPaddingLeft, mPaddingTop, width + mPaddingLeft, mPaddingTop + height);
} else {
int count = i - 1;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mMeasuredShowWidth - mBaseLength + mPaddingTop,
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mMeasuredShowWidth + mPaddingTop);
}
} else {
childView.layout(0, 0, 0, 0);
}
}
break;
case 5:
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
if (i < 5) {
int width = childView.getMeasuredWidth();
if (i < 2) {
childView.layout(mPaddingLeft + (i * (width + mSpace + mOverSpace)),
mPaddingTop,
mPaddingLeft + (i * (width + mSpace + mOverSpace)) + width,
mPaddingTop + width);
} else {
int count = i - 2;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mPaddingTop + ((mMeasuredShowWidth - mSpace) / 2) + mSpace,
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mPaddingTop + ((mMeasuredShowWidth - mSpace) / 2) + mSpace + width);
}
} else {
childView.layout(0, 0, 0, 0);
}
}
break;
case 6:
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
if (i < 6) {
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (i == 0) {
childView.layout(mPaddingLeft, mPaddingTop, mPaddingLeft + width, height + mPaddingTop);
} else if (i < 3) {
int count = i - 1;
childView.layout(mPaddingLeft + mMeasuredShowWidth - width,
mPaddingTop + (count * mSpace) + (count * height),
mPaddingLeft + mMeasuredShowWidth,
mPaddingTop + ((count + 1) * height) + (count * mSpace));
} else {
int count = i - 3;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mPaddingTop + (2 * mSpace) + (2 * mBaseLength),
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mPaddingTop + mMeasuredShowWidth);
}
} else {
childView.layout(0, 0, 0, 0);
}
}
break;
case 7:
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
if (i < 7) {
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (i == 0) {
childView.layout(mPaddingLeft, mPaddingTop, mPaddingLeft + width, height + mPaddingTop);
} else if (i < 4) {
int count = i - 1;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mPaddingTop + (int) (1.5 * height) + mSpace,
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mPaddingTop + (int) (2.5 * height) + mSpace);
} else {
int count = i - 4;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mPaddingTop + (int) (2.5 * height) + (2 * mSpace),
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mPaddingTop + (int) (3.5 * height) + (2 * mSpace));
}
} else {
childView.layout(0, 0, 0, 0);
}
}
break;
case 8:
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
if (i < 8) {
int width = childView.getMeasuredWidth();
if (i < 2) {
childView.layout(mPaddingLeft + (i * (width + mSpace + mOverSpace)),
mPaddingTop,
mPaddingLeft + (i * (width + mSpace + mOverSpace)) + width,
mPaddingTop + width);
} else if (i < 5) {
int count = i - 2;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mPaddingTop + ((mMeasuredShowWidth - mSpace) / 2) + mSpace,
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mPaddingTop + ((mMeasuredShowWidth - mSpace) / 2) + mSpace + width);
} else {
int count = i - 5;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mPaddingTop + ((mMeasuredShowWidth - mSpace) / 2) + width + (2 * mSpace),
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mPaddingTop + ((mMeasuredShowWidth - mSpace) / 2) + (2 * width) + (2 * mSpace));
}
} else {
childView.layout(0, 0, 0, 0);
}
}
break;
case 9:
for (int i = 0; i < getChildCount() - 1; i++) {
View childView = getChildAt(i);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
if (i < 3) {
childView.layout(mPaddingLeft + (i * mSpace) + (i * width),
mPaddingTop,
mPaddingLeft + (i * mSpace) + ((i + 1) * width),
mPaddingTop + height);
} else if (i < 6) {
int count = i - 3;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mPaddingTop + height + mSpace,
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mPaddingTop + (2 * height) + mSpace);
} else {
int count = i - 6;
childView.layout(mPaddingLeft + (count * mSpace) + (count * width),
mPaddingTop + (2 * height) + (2 * mSpace),
mPaddingLeft + (count * mSpace) + ((count + 1) * width),
mPaddingTop + (3 * height) + (2 * mSpace));
}
}
break;
}
}
2.3 设置间距及圆角
间距:通过自定义属性 pictureSpace 获取间距值,在onLayout和onMeasure时对子view进行相应调整
圆角:通过继承于 CardView 沿用其设置圆角的功能
2.4 完整源码
3.功能说明
-
该View继承于CardView以达到实现圆角的目的(也就是通过cardCornerRadius属性设置圆角值)
-
圆角及实现效果因为不依赖于图片加载框架实现,所以可自行选用替换图片加载框架
-
图片数量为一张时,按比例缩放图片,圆角不会随着图片宽高不同而变形
-
提供自定义属性pictureMaxHeight:图片数量只有一张时该图最大显示高度
-
提供自定义属性pictureSpace:图片之间的间隔距离
-
填充网络图片:setUrls(List urls)
-
监听图片点击事件:setCallback(NineGridView.Callback callback)
4.注意事项
-
需在NineGridView中的TODO标注里自行替换当前项目所使用的图片加载框架
-
该View中的ImageView,除了图片数量为1时,采用的是按比例自适应宽高(有最大高度及宽度限制),其余情况下ImageView的宽高都是固定值。具体值由NineGridView能展示的最大宽度决定
-
该View用于RecyclerView时,为了解决单图item被回收后,重新出现在屏幕时由于高度变化造成item跳动的问题,需要牺牲部分recyclerView的复用特性。具体表现为重写适配器getItemViewType方法,判断图片数量是否为1,是则将position返回作为itemViewType(写法参考)(实则最优解应该是为单图情况专门写一个itemLayout并赋予指定itemViewType)
5.最后
- Demo地址:https://github.com/ziwenL/NineGridView
- 其中NineGridView使用Java实现,使用示例使用kotlin实现
- 如有更好的见解或建议,欢迎留言