自界说ViewGroup九宫格

前言

在之前的文章咱们复习了 ViewGroup 的丈量与布局,那么咱们这一篇作用就能够在之前的基础上完成一个灵活的九宫格布局。

那么一个九宫格的 ViewGroup 怎么界说,咱们分解为如下的几个过程来完成:

  1. 先核算与丈量九宫格内部的子View的宽度与高度。
  2. 再核算整体九宫格的宽度和高度。
  3. 进行子View九宫格的布局。
  4. 对独自的图片和四宫格的图片进行独自的布局处理
  5. 对填充的子View的办法进行抽取,能够自由增加布局。
  6. 对自界说特点的抽取,设置通用的特点。

只需在前文的基础上掌握了 ViewGroup 的丈量与布局,其实完成起来一点都不难,甚至咱们还能完成一些特别的作用。

好了,话不多说,Let’s go

Android自定义ViewGroup布局进阶,完整的九宫格实现

一、九宫格的丈量

之前的文章,咱们的丈量办法是现已知道子 View 的详细巨细了,让咱们的父布局做宽高的适配,所以咱们的逻辑次序也是先布局,然后再丈量,对 ViewGroup 的宽高做约束。

可是在咱们做九宫格控件的时分,就和之前有所区别了。咱们不管子 View 的宽高丈量形式是怎样的,咱们都是经过九宫格控件的宽度对子 View 的宽高进行强制赋值。

public class AbstractNineGridLayout extends ViewGroup {
    private static final int MAX_CHILDREN_COUNT = 9;  //最大的子View数量
    private int horizontalSpacing = 20;  //每一个Item的左右距离
    private int verticalSpacing = 20;  //每一个Item的上下距离
    private int itemWidth;
    private int itemHeight;
    public AbstractNineGridLayout(Context context) {
        this(context, null);
    }
    public AbstractNineGridLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public AbstractNineGridLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }
    private void init(Context context) {
        for (int i = 0; i < MAX_CHILDREN_COUNT; i++) {
            ImageView imageView = new ImageView(context);
            imageView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            imageView.setBackgroundColor(Color.RED);
            addView(imageView);
        }
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
        int notGoneChildCount = getNotGoneChildCount();
        //不管什么形式,都是指定的固定宽高
        itemWidth = (widthSize - horizontalSpacing * 2) / 3;
        itemHeight = itemWidth;
        //measureChildren内部调用measureChild,这儿咱们就能够指定宽高
        measureChildren(MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY));
        if (heightMode == MeasureSpec.EXACTLY) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        } else {
            notGoneChildCount = Math.min(notGoneChildCount, MAX_CHILDREN_COUNT);
            int heightSize = ((notGoneChildCount - 1) / 3 + 1) *
                    (itemHeight + verticalSpacing) - verticalSpacing + getPaddingTop() + getPaddingBottom();
            setMeasuredDimension(widthSize, heightSize);
        }
    }
}

刚开始的时分咱们在布局初始化的时分先增加5个 mathc_parent 的9个子 View 作为测验。那么咱们在布局的时分,就需求对宽度进行切割,而且强制性的丈量每一个子 View 的宽高为 EXACTLY 形式。

丈量完每一个子 View 之后,咱们再动态的给 ViewGroup 设置宽高。

这样丈量之后的作用为:

Android自定义ViewGroup布局进阶,完整的九宫格实现

Android自定义ViewGroup布局进阶,完整的九宫格实现

为了便利检查作用,加上了测验的灰色布景,看着巨细是符合预期的。接下来咱们就开始布局。

二、九宫格的布局

在之前流式布局的 onLayout 办法中,咱们是经过动态的拿到每一个子 View 的宽度去判断当时是否会超过总宽度,是否需求换行。

而这儿咱们就无需这么做了,因为每一个子 View 都是固定的宽度,一行便是三个,一列最多也是三个。咱们直接经过子 View 的数量就能够确认当时的行数与列数。

然后咱们就能行数和列数进行布局了,详细的看代码:

 @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;    //当时子View是第几行(索引)
            int column = position % 3; //当时子View是第几列(索引)
            //当时需求制作的光标的X与Y值
            int x = column * itemWidth + getPaddingLeft() + horizontalSpacing * column;
            int y = row * itemHeight + getPaddingTop() + verticalSpacing * row;
            child.layout(x, y, x + itemWidth, y + itemHeight);
            //最多只摆放9个
            position++;
            if (position == MAX_CHILDREN_COUNT) {
                break;
            }
        }
    }

作用为:

Android自定义ViewGroup布局进阶,完整的九宫格实现

假如对行和列的核算不清楚的,咱们能够对每一个子 View 的方位进行回顾,一共最多也就 9 个,当为第 0 个子 View 的时分,position为 0 ,那么 position / 3 是 0,row 便是 0, position % 3 也是 0,便是第最左上角的方位了。

当为第1个子 View 的时分,position为1 ,那么 position / 3 仍是0,row便是0, position % 3是1了,便是第一排中间的方位了。

只有当View超过三个之后,position /3 便是 1 了,row为 1 之后,才是第二行的方位。顺次类推就能够定位到每一个子 View 需求制作的方位。

而 x 与 y 的值与核算逻辑,咱们能够幻想为需求制作当时 View 的时分,当时画笔需求地点的方位。加上左右和上下的距离之后,咱们经过这样的办法也能够完成 margin 的作用。还记得前文流式布局是怎么完成 margin 作用的吗?异曲同工的作用。

最终详细的 child.layout 反而是最简单的,只需求制作子 View 本身的宽高即可。

三、单图片与四宫格的独自处理。

一般来说咱们需求独自的处理一张图片与四张图片的逻辑。包含丈量与布局都需求独自的处理。

一张图片的时分,咱们需求经过办法独自的指定图片的宽度与高度。而四张图片咱们需求固定两行的高度即可。

public class AbstractNineGridLayout extends ViewGroup {
    private static final int MAX_CHILDREN_COUNT = 9;  //最大的子View数量
    private int horizontalSpacing = 20;  //每一个Item的左右距离
    private int verticalSpacing = 20;  //每一个Item的上下距离
    private boolean fourGridMode = true;  //是否支撑四宫格形式
    private boolean singleMode = true;  //是否支撑单布局形式
    private boolean singleModeScale = true;  //是否支撑单布局形式按份额缩放
    private int singleWidth;
    private int singleHeight;
    private int itemWidth;
    private int itemHeight;
    @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 && singleModeScale) {
                itemWidth = widthSize;  //单张图片先定宽度。
                itemHeight = (int) (widthSize * 1f / singleWidth * singleHeight);  //依据宽度核算高度
            }
        } else {
            //除了单布局形式,其他的都是指定的固定宽高
            itemWidth = (widthSize - horizontalSpacing * 2) / 3;
            itemHeight = itemWidth;
        }
        ...
    }
    /**
     * 设置独自布局的宽和高
     */
    public void setSingleModeSize(int w, int h) {
        if (w != 0 && h != 0) {
            this.singleMode = true;
            this.singleWidth = w;
            this.singleHeight = h;
        }
    }
}

丈量的时分咱们对单布局进行丈量,而且对超过宽度的一些布局做等份额的缩放。然后再丈量父布局。

findViewById<AbstractNineGridLayout>(R.id.nine_grid).setSingleModeSize(dp2px(200f), dp2px(400f))

作用:

Android自定义ViewGroup布局进阶,完整的九宫格实现

而假如是四宫格形式,咱们好像也不需求重新丈量,横竖也是二行的高度,可是布局的时分咱们需求处理一下,不然第三个子 View 的方位就会不对了。咱们只需求修改x 与 y的核算办法,它们是依据行和列动态核算你的,那么修改行和列的核算办法即可。

    @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;    //当时子View是第几行(索引)
            int column = position % 3; //当时子View是第几列(索引)
            if (notGoneChildCount == 4 && fourGridMode) {
                row = position / 2;
                column = position % 2;
            }
            //当时需求制作的光标的X与Y值
            int x = column * itemWidth + getPaddingLeft() + horizontalSpacing * column;
            int y = row * itemHeight + getPaddingTop() + verticalSpacing * row;
            child.layout(x, y, x + itemWidth, y + itemHeight);
            //最多只摆放9个
            position++;
            if (position == MAX_CHILDREN_COUNT) {
                break;
            }
        }
    }
    /**
     * 独自设置是否支撑四宫格形式
     */
    public void setFourGridMode(boolean enable) {
        this.fourGridMode = enable;
    }

这样咱们就能够支撑四宫格的布局形式,作用如下:

Android自定义ViewGroup布局进阶,完整的九宫格实现

到此,咱们的九宫格控件大体上是竣工了,可是还不行灵活,内部的子 View 都是咱们自己 new 出来的,咱们接下来就要露出出去让其能够自界说布局。

四、自界说布局的抽取

怎么把填充布局的逻辑抽取出来呢?一般分为两种思路:

  1. 每次初始化九宫格的时分就把九个布局悉数增加进来,先丈量布局了再说,然后经过露出的办法隐藏剩余的布局。
  2. 经过一个界说一个数据适配器Adapter,内部封装一些逻辑,让详细完成的类去完成详细的逻辑。

两种办法都能够,没有好坏之分。可是运用数据适配器的计划因为内部的View会少,性能会好那么一丢丢,整体来说不同不大。

4.1 先布局再隐藏的思路

一般咱们在笼统的九宫格类中就需求露出这两个重要办法,一个是填充子布局的,一个是填充数据而且隐藏剩余的布局。

    //子类去完成-填充布局文件
    protected abstract void fillChildView();
    //子类去完成-对布局文件赋值数据(一般专门去给adapter去调用的)
    public abstract void renderData(T data);

例如咱们的完成类:

    @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);
        }
    }

重点是填充的办法 inflateChildLayout 分为两种状况,一种是布局都相同的状况,一种是依据索引填充不同的布局状况。

    /**
     * 能够为每一个子布局加载对应的布局文件(不同的文件)
     */
    protected void inflateChildLayoutCustom(ViewGetter viewGetter) {
        removeAllViews();
        for (int i = 0; i < MAX_CHILDREN_COUNT; i++) {
            addView(viewGetter.getView(i));
        }
    }
    /**
     * 一般用这个办法填充布局,每一个小布局的布局文件(相同的文件)
     */
    protected void inflateChildLayout(int layoutId) {
        removeAllViews();
        for (int i = 0; i < MAX_CHILDREN_COUNT; i++) {
            LayoutInflater.from(getContext()).inflate(layoutId, this);
        }
    }

而咱们设置数据的办法中调用的 setDisplayCount 办法则是隐藏剩余的控件的。

    /**
     * 设置显现的数量
     */
    public void setDisplayCount(int count) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).setVisibility(i < count ? VISIBLE : GONE);
        }
    }

作用:

Android自定义ViewGroup布局进阶,完整的九宫格实现

4.2 数据适配器的思路

而运用数据适配器的计划,就无需每次上来就先填充9个子布局,而是经过Adapter动态的装备当时需求填充的数量,而且创立对应的子 View 和绑定对应的子 View 的数据。

听起来是不是很像RV的Apdater,没错便是参考它的完成办法。

咱们先创立一个基类的Adapter:

    public static abstract class Adapter {
        //返回一共子View的数量
        public abstract int getItemCount();
        //依据索引创立不同的布局类型,假如都是相同的布局则不需求重写
        public int getItemViewType(int position) {
            return 0;
        }
        //依据类型创立对应的View布局
        public abstract View onCreateItemView(Context context, ViewGroup parent, int itemType);
        //能够依据类型或索引绑定数据
        public abstract void onBindItemView(View itemView, int itemType, int position);
    }

然后咱们需求露出一个办法,设置Adapter,设置完成之后咱们就能够增加对应的布局了。

 public void setAdapter(Adapter adapter) {
        mAdapter = adapter;
        inflateAllViews();
    }
    private void inflateAllViews() {
        removeAllViewsInLayout();
        if (mAdapter == null || mAdapter.getItemCount() == 0) {
            return;
        }
        int displayCount = Math.min(mAdapter.getItemCount(), MAX_CHILDREN_COUNT);
        //单布局处理
        if (singleMode && displayCount == 1) {
            View view = mAdapter.onCreateItemView(getContext(), this, -1);
            addView(view);
            requestLayout();
            return;
        }
        //多布局处理
        for (int i = 0; i < displayCount; i++) {
            int itemType = mAdapter.getItemViewType(i);
            View view = mAdapter.onCreateItemView(getContext(), this, itemType);
            view.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            addView(view);
        }
        requestLayout();
    }

需求注意的是咱们再丈量的布局的时分,假如没有 Adpter 或许没有子布局的时分,咱们需求独自处理一下九宫格ViewGroup的高度。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
        int notGoneChildCount = getNotGoneChildCount();
        if (mAdapter == null || mAdapter.getItemCount() == 0 || notGoneChildCount == 0) {
            setMeasuredDimension(widthSize, 0);
            return;
        }
        ...
    }

那么怎么绑定布局呢?在咱们 onLayout完成之后咱们就能够绑定数据了。

 @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ...
        performBind();
    }
    /**
     * 布局完成之后绑定对应的数据到对应的ItemView
     */
    private void performBind() {
        if (mAdapter == null || mAdapter.getItemCount() == 0) {
            return;
        }
        post(() -> {
            for (int i = 0; i < getNotGoneChildCount(); i++) {
                int itemType = mAdapter.getItemViewType(i);
                View view = getChildAt(i);
                mAdapter.onBindItemView(view, itemType, i);
            }
        });
    }

详细的完成便是在 Adapter 中完成了。

例如咱们创立一个最简单的图片九宫格适配器。

public class ImageNineGridAdapter extends AbstractNineGridLayout.Adapter {
    private List<String> mDatas = new ArrayList<>();
    public ImageNineGridAdapter(List<String> data) {
        mDatas.addAll(data);
    }
    @Override
    public int getItemCount() {
        return mDatas.size();
    }
    @Override
    public View onCreateItemView(Context context, ViewGroup parent, int itemType) {
        return LayoutInflater.from(context).inflate(R.layout.item_img, parent, false);
    }
    @Override
    public void onBindItemView(View itemView, int itemType, int position) {
        itemView.findViewById(R.id.iv_img).setBackgroundColor(Color.RED);
    }
}

在Activity中设置对应的数据适配器:

     findViewById<AbstractNineGridLayout>(R.id.nine_grid).run {
            setSingleModeSize(dp2px(200f), dp2px(400f))
            setAdapter(ImageNineGridAdapter(imgs))
        }

咱们就能得到相同的作用:

Android自定义ViewGroup布局进阶,完整的九宫格实现

假如想九宫格内运用不同的布局,不同的索引展示不同的逻辑,都能够很便利的完成:

public class ImageNineGridAdapter extends AbstractNineGridLayout.Adapter {
    private List<String> mDatas = new ArrayList<>();
    public ImageNineGridAdapter(List<String> data) {
        mDatas.addAll(data);
    }
    @Override
    public int getItemViewType(int position) {
        if (position == 1) {
            return 10;
        } else {
            return 0;
        }
    }
    @Override
    public int getItemCount() {
        return mDatas.size();
    }
    @Override
    public View onCreateItemView(Context context, ViewGroup parent, int itemType) {
        if (itemType == 0) {
            return LayoutInflater.from(context).inflate(R.layout.item_img, parent, false);
        } else {
            return LayoutInflater.from(context).inflate(R.layout.item_img_icon, parent, false);
        }
    }
    @Override
    public void onBindItemView(View itemView, int itemType, int position) {
        if (itemType == 0) {
            itemView.findViewById(R.id.iv_img).setBackgroundColor(position == 0 ? Color.RED : Color.YELLOW);
        }
    }
}

作用:

Android自定义ViewGroup布局进阶,完整的九宫格实现

到这儿咱们的控件就基本上能完成大部分事务需求了,接下来我会对一些特点与装备进行抽取,并开源上传到云端。

后记

总的来说,只需理解了ViewGroup的丈量与布局之后,像相似的作用都能够完成,假如想要一些特殊的宽高与作用,大家完全能够自行修改。

假如想看类型微信微博的那种列表,能够看看我之前的文章【传送门】。里边有完整的完成流程。

关于本文的内容假如想检查源码能够点击这儿 【传送门】。你也能够重视我的这个Kotlin项目,我有时间都会继续更新。

详细的组件等我收拾一下,我开源到Maven上面。待续…

后续的文章可能会讲一下 ViewGroup 的事情处理,之前讲过View的事情处理,为什么又说ViewGroup,因为他们仍是有区别的,和 View 的事情处理比较多了几种分类,常用几种分类大致如下:一种是自己翻滚的,一种是事情拦截与分发的,一种是内部和谐翻滚的,每种又分不同的完成办法,待续…

2022-12-19 更新

NineGridView: 九宫格控件的抽取 (gitee.com)

现已抽取并发布到Maven了,有兴趣的能够去看看。

怎么运用:

implementation “com.gitee.newki123456:nine_grid_view:1.0.0”

直接依靠即可,运用的办法和上述的一致,自界说特点已做了注释。

惯例,我如有讲解不到位或讹夺的当地,希望同学们能够指出交流。

假如感觉本文对你有一点点的启发,还望你能点赞支撑一下,你的支撑是我最大的动力。

Ok,这一期就此完结。

Android自定义ViewGroup布局进阶,完整的九宫格实现

本文正在参加「金石计划 . 分割6万现金大奖」