多图展示自定义ViewGroup

天天都刷朋友圈,正好最近不那么忙,就当巩固一下知识了。先看效果图,如下图:

android viewbinding include里的id androidsdk中的viewgroup_android

还有单图和四图的样式,图片点击事件和更多的点击事件都暴露出来了,回调里自个处理一下,挺简单的。

我就说一下思路和要注意的点吧,主要是动态添加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());
    }
}

android viewbinding include里的id androidsdk中的viewgroup_ide_02