多图展示自定义ViewGroup
天天都刷朋友圈,正好最近不那么忙,就当巩固一下知识了。先看效果图,如下图:
还有单图和四图的样式,图片点击事件和更多的点击事件都暴露出来了,回调里自个处理一下,挺简单的。
我就说一下思路和要注意的点吧,主要是动态添加imageview到ViewGroup里。我们都知道view的绘制流程有三要素,measure、layout、draw,一般我们需要继承一个view或者viewgroup,然后重写这三个方法。这个没什么好说的,都是根据自己的需求来的 。但是得提一下MeasureSpec这个东西 ,因为这个关系到整体view的大小,算了,还是着重讲一下,毕竟做笔记嘛。
关于MeasureSpec的解析
Measurespec 是一个int类型的32位值,高俩位代表SpecMode,低三十位SpecSize。SpecMode代表的是测量模式,SpecSize代表测量规格大小,说到这里就很明显了,MeasureSpec关系到整个view的大小,这就是他作用。
SpecMode的说明
SpecMode有三种类别:UNSPECIFIED、EXACTLY、AT_MOST,这三种类别分别对应着xml的view里的宽高属性,详细点儿的如下:
UNSPECIFIED:父容器对view没有任何限制,要多大给多大,这种情况一般是用于系统内部。
EXACTLY:父容器已经读取出xml里view的精确大小了,他对应于xml里的match_parent和具体的数值
AT_MOST:父容器已经读取出xml里view的精确大小了,但是view的大小不能超过Specsize,对应xml里的wrap_content
有人可能会问,这东西不是和LayoutParams差不多么。还真是这样,但是MeasureSpec不是唯一由LayoutParams决定的,LayouParams需要和父容器一起才能决定view的MeasureSpec,从而进一步的来确定view的宽高,MeasureSpec一旦确定,在OnMeasure方法里就可以获取到view的宽高了。对了,对于顶级DecorView来讲,MeasureSpec的转换过程有点不同。对于DecoreVIew,这个值是由窗口的大小和自身的LayoutParams决定的。
自定义view需要注意的点
1.要让View支持wrap_content,简单点儿的处理方法就是在OnMeasure方法里设置一个默认的值
2.计算view的宽高时,要考虑到padding
3.尽量不要在view中使用handler,官方的DialogFragment就是个例子,内存泄漏弹一天,炸了。。。。
4.处理好滑动冲突,
5.要是有线程、动画啥的,及时停止、资源记得销毁
好了好了,贴代码,用的时候需要继承MultipleGridLayout这个类,加载图片和一些监听暴露出来了,根据需要自己往上加。
/**
* @Author: Ryan
* @Date: 2020/7/14 15:21
* @Description: 多张图片展示,仿朋友圈布局
*/
public abstract class MultipleGridLayout extends ViewGroup implements IMultipleGridLayout {
private static final float DEFAULT_SPACING = 5f;
private static final int MAX_IMAGE_COUNT = 9;//只能最大展示九张图片
protected Context mContext;
private float mSpacing = DEFAULT_SPACING;
private int mColumns;
private int mRows;
private int mTotalWidth;
private int singleImageWidth;//单张图片的宽度
private int singleImageHeight;//单张图片的高度
private int onlySingleImageW;//只有一张图片时图片的自定义宽度
private int onlySingleImageH;//只有一张图片时图片的自定义高度
private boolean mIsFirst = true;
private List<String> mUrlList = new ArrayList<>();
private int color = Color.WHITE;
private int textSize = 18;
public MultipleGridLayout(Context context) {
super(context);
init(context);
}
public MultipleGridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MultipleGridLayout);
mSpacing = typedArray.getDimension(R.styleable.MultipleGridLayout_spacing, DEFAULT_SPACING);
typedArray.recycle();
init(context);
}
/**
* 初始化,如果数据源是空的 ,就不显示view
*
* @param context
*/
private void init(Context context) {
mContext = context;
if (getListSize(mUrlList) == 0) {
setVisibility(GONE);
}
}
/**
* 设置多余图片的数字样式
*
* @param color
* @param textSize
*/
public void setExtraNumStyle(@ColorInt int color, int textSize) {
this.color = color;
this.textSize = textSize;
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
public int px2dip(float px) {
final float scale = mContext.getResources().getDisplayMetrics().density;
return (int) (px / scale + 0.5f);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildViewRanks(getListSize(mUrlList));
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize;
if (widthSpecMode == MeasureSpec.EXACTLY) {//xml里布局的宽度 不要设置成wrap content
mTotalWidth = widthSpecSize - getPaddingLeft() - getPaddingRight();
singleImageWidth = (int) ((mTotalWidth - mSpacing * (mColumns - 1)) / mColumns);
singleImageHeight = singleImageWidth;//根据view的宽度,子view设置成正方形
if (getListSize(mUrlList) == 1) {
setMeasuredDimension(onlySingleImageW, onlySingleImageH);
} else {
widthSpecSize = (int) (singleImageWidth * mColumns + mSpacing * (mColumns - 1)) + getPaddingLeft() + getPaddingRight();
heightSpecSize = (int) (singleImageHeight * mRows + mSpacing * (mRows - 1) + getPaddingTop() + getPaddingBottom());
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (mIsFirst) {
notifyDataSetChanged();
mIsFirst = false;
}
}
@Override
public ShadeRatioImageView createImageView(final int i, final String url) {
ShadeRatioImageView imageView = new ShadeRatioImageView(mContext);
imageView.setLayoutParams(new LinearLayout.LayoutParams(singleImageWidth, singleImageHeight));
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onClickImage(i, url, mUrlList);
}
});
return imageView;
}
@Override
public void layoutChildView(ShadeRatioImageView imageView, final int i, String url, boolean isShowExtraNum) {
int[] position = findPosition(i);
int left = (int) ((singleImageWidth + mSpacing) * position[1]) + getPaddingLeft();
int top = (int) ((singleImageHeight + mSpacing) * position[0]) + getPaddingTop();
int right = left + singleImageWidth;
int bottom = top + singleImageHeight;
imageView.layout(left, top, right, bottom);
addView(imageView);
//显示超过9张的图片数量
if (isShowExtraNum && i == MAX_IMAGE_COUNT - 1) {
int overCount = getListSize(mUrlList) - MAX_IMAGE_COUNT;
if (overCount > 0) {
final TextView textView = new TextView(mContext);
textView.setText("+" + overCount);
textView.setTextColor(color);
textView.setPadding(0, singleImageHeight / 2 - getFontHeight(textSize), 0, 0);
textView.setTextSize(textSize);
textView.setGravity(Gravity.CENTER);
textView.setBackgroundColor(Color.BLACK);
textView.getBackground().setAlpha(100);
textView.layout(left, top, right, bottom);
addView(textView);
//点击更多的
imageView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onClickMore(i, mUrlList.get(i), mUrlList);
}
});
}
}
displayImages(imageView, url);
}
@Override
public void setData(List<String> urlList) {
if (getListSize(urlList) == 0) {
setVisibility(GONE);
return;
}
setVisibility(VISIBLE);
mUrlList.clear();
mUrlList.addAll(urlList);
if (!mIsFirst) {
notifyDataSetChanged();
}
}
@Override
public void setSpacing(float spacing) {
mSpacing = spacing;
}
@Override
public void notifyDataSetChanged() {
/*
数据刷新在很多地方都调用到,考虑到外部调用的时候或者设置数据源的时候,可能不是在主线程,比如设置数据源的时候是在子线程里面 所以这里直接调用post方法
*/
post(new Runnable() {
@Override
public void run() {
refreshViews();
}
});
}
/**
* 刷新view
*/
private void refreshViews() {
removeAllViews();
int size = getListSize(mUrlList);
if (size > 0) {
setVisibility(VISIBLE);
} else {
setVisibility(GONE);
}
if (size == 1) {
String url = mUrlList.get(0);
ShadeRatioImageView imageView = createImageView(0, url);
//避免在ListView中一张图未加载成功时,布局高度受其他item影响
LayoutParams params = getLayoutParams();
params.height = singleImageHeight;
setLayoutParams(params);
imageView.layout(0, 0, singleImageWidth, singleImageWidth);
boolean isShowDefault = displaySingleImage(imageView, url, mTotalWidth);
if (isShowDefault) {
layoutChildView(imageView, 0, url, false);
} else {
addView(imageView);
}
return;
}
for (int i = 0; i < size; i++) {
String url = mUrlList.get(i);
ShadeRatioImageView imageView;
//最多显示九张,多余的用数字表示
if (i < MAX_IMAGE_COUNT - 1) {
imageView = createImageView(i, url);
layoutChildView(imageView, i, url, false);
} else {
if (i == MAX_IMAGE_COUNT - 1) {
imageView = createImageView(i, url);
layoutChildView(imageView, i, url, true);
break;
}
}
}
}
/**
* 确定每个图片的行数和列数
*
* @param childNum
* @return
*/
private int[] findPosition(int childNum) {
int[] position = new int[2];
for (int i = 0; i < mRows; i++) {
for (int j = 0; j < mColumns; j++) {
if ((i * mColumns + j) == childNum) {
position[0] = i;//行
position[1] = j;//列
break;
}
}
}
return position;
}
/**
* 当只有一张图片时,设置其大小
*
* @param imageView
* @param width
* @param height
*/
protected void setSingleImageLayoutParams(ShadeRatioImageView imageView, int width, int height) {
imageView.setLayoutParams(new LayoutParams(width, height));
imageView.layout(0, 0, width, height);
onlySingleImageH = height;
onlySingleImageW = width;
}
/**
* 根据图片数量确定行列数量
*
* @param size
*/
private void measureChildViewRanks(int size) {
if (size <= 3) {
mRows = 1;
mColumns = size;
} else if (size <= 6) {
mRows = 2;
mColumns = 3;
if (size == 4) {
mColumns = 2;
}
} else {
mColumns = 3;
mRows = 3;
}
}
/**
* 获取List的大小
*
* @param list
* @return
*/
private int getListSize(List<String> list) {
if (list == null || list.size() == 0) {
return 0;
}
return list.size();
}
/**
* 根据字体大小,获取字体高度
*
* @param fontSize
* @return
*/
private int getFontHeight(float fontSize) {
Paint paint = new Paint();
paint.setTextSize(fontSize);
Paint.FontMetrics fm = paint.getFontMetrics();
return (int) Math.ceil(fm.descent - fm.ascent);
}
/**
* @param imageView
* @param url
* @param parentWidth 父控件宽度 当只有一张图片的时候让它占满父控件
* @return true 代表按照九宫格默认大小显示,false 代表按照自定义宽高显示
*/
protected abstract boolean displaySingleImage(ShadeRatioImageView imageView, String url, int parentWidth);
protected abstract void displayImages(ShadeRatioImageView imageView, String url);
protected abstract void onClickImage(int position, String url, List<String> urlList);
protected abstract void onClickMore(int position, String url, List<String> urlList);
}
功能接口:
/**
* @Author: Ryan
* @Date: 2020/7/20 11:38
* @Description: 多图展示View 基础功能
*/
public interface IMultipleGridLayout {
/**
* 生成子view
*
* @param i
* @param url
* @return
*/
ShadeRatioImageView createImageView(final int i, final String url);
/**
* 布局子view
*
* @param imageView
* @param i
* @param url
* @param isShowExtraNum
*/
void layoutChildView(ShadeRatioImageView imageView, int i, String url, boolean isShowExtraNum);
/**
* 设置图片间隔
*
* @param spacing
*/
void setSpacing(float spacing);
/**
* 设置数据源
*
* @param urlList
*/
void setData(List<String> urlList);
/**
* 刷新数据
*/
void notifyDataSetChanged();
}
还有个,仿微信图片点击效果的自定义ImageView,在测量的时候还可以设置宽高比例
/**
* @Author: Ryan
* @Date: 2020/7/20 10:12
* @Description: 可以设置图片宽高比,点击的时候仿微信阴影
*/
public class ShadeRatioImageView extends androidx.appcompat.widget.AppCompatImageView {
/**
* 宽高比例
*/
private float mRatio = 0f;
public ShadeRatioImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ShadeRatioImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ShadeRatioImageView);
mRatio = typedArray.getFloat(R.styleable.ShadeRatioImageView_ratio, 0f);
typedArray.recycle();
}
public ShadeRatioImageView(Context context) {
super(context);
}
/**
* 设置ImageView的宽高比
*
* @param ratio
*/
public void setRatio(float ratio) {
mRatio = ratio;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
if (mRatio != 0) {
float height = width / mRatio;
heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) height, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Drawable drawable = getDrawable();
if (drawable != null) {
drawable.mutate().setColorFilter(Color.GRAY, PorterDuff.Mode.MULTIPLY);
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
Drawable drawableUp = getDrawable();
if (drawableUp != null) {
drawableUp.mutate().clearColorFilter();
}
break;
}
return super.onTouchEvent(event);
}
}
最后,看一下实例,要不点个赞再走,也是可以的。。。。。
/**
* @Author: Ryan
* @Date: 2020/7/16 14:20
* @Description: java类作用描述
*/
public class MyGridLayout extends MultipleGridLayout {
public MyGridLayout(Context context) {
super(context);
}
public MyGridLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected boolean displaySingleImage(ShadeRatioImageView imageView, String url, int parentWidth) {
setSingleImageLayoutParams(imageView, 500, 700);
Glide.with(this).asBitmap().load(url).into(imageView);
return false;
}
@Override
protected void displayImages(ShadeRatioImageView imageView, String url) {
Glide.with(this).asBitmap().load(url).into(imageView);
}
@Override
protected void onClickImage(int position, String url, List<String> urlList) {
}
@Override
protected void onClickMore(int position, String url, List<String> urlList) {
Log.e(TAG, "onClickMore: -*-*-*-*-*-*--------------444444444044" + urlList.toString());
}
}