自定义ViewGroup练习一

学习目标

熟悉并掌握自定义ViewGroup控件流程与开发细节

效果图

概述

ViewGroup 是 View 的容器类,我们常用的 LinearLayoutRelativeLayout 等都是 ViewGroup 的子类。因为 ViewGroup 有很多子 View,所以它的整个绘制过程相对于 View 会复杂一点,但还是三个步骤 measure,layout,draw。

Measure

Measure 过程还是测量 ViewGroup 的大小。

如果 layout_widthlayout_height 是 match_parent 或具体的 xxdp 就很简单了,直接调用 setMeasuredDimension() 方法,设置 ViewGroup 的宽高即可;

如果是 wrap_content,我们需要遍历所有的子 View,然后对每个子 View 进行测量,然后根据子 View 的排列规则,计算出最终 ViewGroup 的大小。

过程中用到 getChildCount() 方法,返回子 View 的数量,measureChild() 方法,调用子 View 的测量方法。

Layout

Layout 过程其实就是对子 View 的位置进行排列。

其中 child.layout(left,top,right,bottom)方法可以对子 View 的位置进行设置。

Draw

在该阶段,就是按照子 View 的排列顺序,调用子 View 的 onDraw()方法。

因为 ViewGroup 只是 View 的容器,本身一般不需要 draw 额外的修饰,所以在 onDraw() 方法里面,只需要调用 ViewGroup 的 onDraw() 默认实现方法即可。

还有一个很重要的概念 LayoutParams

LayoutParams 存储了子 View 在加入 ViewGroup 中时的一些参数信息。

在继承 ViewGroup 类时,一般也需要新建一个新的 LayoutParams 类,就想 SDK 中我们所熟悉的 LinearLayout.LayoutParams,RelativeLayout.LayoutParams 类等一样。

具体操作步骤如下

在自定义的 ViewGroup 子类中,新建一个 LayoutParams 类继承与 ViewGroup.LayoutParams

 /***
     *
     * LayoutParams 存储了子 view 在加入 ViewGroup 中时的一些参数信息
     * 在继承 ViewGroup 类时,一般也需要新建一个新的 LayoutParams 类
     * */

    class LayoutParams extends  ViewGroup.LayoutParams {

        public int left = 0;
        public int top = 0;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }

有了新的 LayoutParams,接下来就是如何让我们自定义的 ViewGroup 使用我们自定义的 LayoutParams 类来添加子 View。

ViewGroup 中同样提供了几个方法供我们重写,我们只要重写这些方法然后返回我们自定义的 LayoutParams 对象即可。

 /***
     *
     * 有了新的 LayoutParams 类,就要让新自定义的 ViewGroup 使用我们自定义的 LayoutParaams 类
     * 来添加子 view ,ViewGroup 提供了下面几个方法,我们重写返回我们自己的 LayoutParams 对象即可
     *
     * */


    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(),attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

应用实例

首先在 values/attrs 文件下定义新的自定义控件属性

这里随便定义了图片间的横向和竖向间隔

 <declare-styleable name="ninephotoview">
        <attr name="ninephoto_hspace" format="dimension"/>
        <attr name="ninephoto_vspace" format="dimension"/>
    </declare-styleable>

在新建的自定义 ViewGroup 子类中,这里贴出主要代码


    public NinePhotoView(Context context, AttributeSet attrs, int defStyleAttr) {

        ......

        // 初始状态新建一个子 view 作为添加图片的标识
        addPhotoView = new View(context);
        addView(addPhotoView);
        // 记录 ViewGroup 容器中 view 的数量
        mImageResArrayList.add(1);
    }

    /**
     *  Measure 过程还是测量 ViewGroup 的大小
     *  如果layout_widht和layout_height是match_parent或具体的xxxdp,就很简单了,直接调用
     *  setMeasuredDimension()方法,设置ViewGroup的宽高即可
     *
     *  如果是 wrap_content,就比较麻烦了,我们需要遍历所有的子View,然后对每个子View进行测量,
     *  然后根据子View的排列规则,计算出最终ViewGroup的大小。
     *
     *
     *  子 view 四个一排,而且都是正方形,所以通过循环很好的得到所有子 view 的位置
     *  把子 view 的左上角坐标存储到我们自定义的 LayoutParams 的 left 和 top 二个字段中,Layout 阶段会使用
     *  最后算出整个 ViewGroup 的宽高
     * */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 获取容器宽高 size
        int rw = MeasureSpec.getSize(widthMeasureSpec);
        int rh = MeasureSpec.getSize(heightMeasureSpec);

        // 控制控件图片都在屏幕范围内显示完整
        childWidth = rw / 5; // 5:也可以是其它值,自己调整看效果
        childHeight = childWidth;

        // 获得子 view 数量
        int childCount = this.getChildCount();
        // 遍历子 view, 将子 view 的left,top 存入子 view 的 LayoutParams
        for (int i = 0; i < childCount; i++) {
            View child = this.getChildAt(i);
            NinePhotoView.LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
//            layoutParams.left = (i % 3) * (childWidth + hSpace);
//            layoutParams.top = (i / 3) * (childWidth + vSpace);

            // 以微信 3 行 4 列为例
            // 设置每个子 view 的 left 和 top
            // 横向排列的每列 left 扩大一倍
            layoutParams.left = (i % 4) * childWidth;
            // 竖直方向的每行 top 扩大一倍
            layoutParams.top = (i / 4) * childHeight;
            // 还可以加入图片间距等
        }
            // 这样就把每张图片的 left 和 top 位置保存到了 layoutParams

            // 使用默认容器宽高
//            setMeasuredDimension(widthMeasureSpec,heightMeasureSpec);
            // 定义控件的宽高
        int nineWidth = rw;
        int nineHeight = rh;

        Map<Integer,Integer> line = new HashMap<>();
        for (int i = 1; i < 10; i++) {
            if (i < 5) {
                line.put(i,1);
            } else if (i < 9) {
                line.put(i,2);
            } else {
                line.put(i,3);
            }

        }

        nineHeight = (line.get(childCount)) * childHeight;

        setMeasuredDimension(nineWidth,nineHeight);
    }


   ......

    // 对子 view 进行位置排列
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 获得子 view 数量
        int childCount = this.getChildCount();
        // 遍历子 view,取出存储在子 view 的 LayoutParams 中的 left 和 top

        for (int i =0; i < childCount; i++) {
            View child  = this.getChildAt(i);
            NinePhotoView.LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
            // 对子 view 位置进行设置
            child.layout(layoutParams.left,layoutParams.top,layoutParams.left + childWidth,
                    layoutParams.top + childHeight);

            if (i == mImageResArrayList.size() - 1 ) {

                // 需要注意的是,当选到最大上传图片数的倒数第二张图片时,会继续产生添加图片的占位符,导致
                // 子 view 数量已经达到最大上传图片数,这时点击最后的添加图片图标时,我们的操作不能再是生成子 view ,
                // 而是将最后一张的上传图标换成我们的图片
                // 如果不注意上面这个问题,会出现选择添加倒数第二张图片时,最后一张也被添加。因为
                if (mImageResArrayList.size() > MAX_PHOTO_NUMBER) {
                    child.setBackgroundResource(constImageIds[i]);
                    child.setOnClickListener(null);
                } else {
                    child.setBackgroundResource(R.drawable.add);
                    child.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            addPhotoBtnClick();
                        }
                    });
                }
            }
            else {
                child.setBackgroundResource(constImageIds[i]);
                child.setOnClickListener(null);
            }
        }

    }

    private void addPhotoBtnClick() {
        final CharSequence[] items = { "拍照", "从相册选择" };

        AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
        builder.setItems(items, new DialogInterface.OnClickListener() {

            @Override
            public void onClick(DialogInterface arg0, int arg1) {
                addPhoto();
            }

        });
        builder.show();
    }

    private void addPhoto() {
        // 在数量范围内添加图片
        if (mImageResArrayList.size() < MAX_PHOTO_NUMBER) {
            View newChild = new View(getContext());
            addView(newChild);
            // 每添加一个子 view ,计数加1
        }
        mImageResArrayList.add(1);
        // 重新调用 onLayout() 进行重新排列
        requestLayout();
        // 重新绘制 View
        invalidate();
    }


    /***
     *
     * 有了新的 LayoutParams 类,就要让新自定义的 ViewGroup 使用我们自定义的 LayoutParaams 类
     * 来添加子 view ,ViewGroup 提供了下面几个方法,我们重写返回我们自己的 LayoutParams 对象即可
     *
     * */


   ......

    /***
     *
     * LayoutParams 存储了子 view 在加入 ViewGroup 中时的一些参数信息
     * 在继承 ViewGroup 类时,一般也需要新建一个新的 LayoutParams 类
     * */

    class LayoutParams extends  ViewGroup.LayoutParams {

        public int left = 0;
        public int top = 0;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}

过程中出现的问题

在添加图片点击到倒数第二张的添加图标的时候,最后一张也跟着出来了。

因为图片控制在 9 张,那么当成功添加 8 张图片的时候,ViewGroup 容器里已经有 9 个字 View(包括了图片添加图标),当在点击第 9 个子 View 时,不能再添加子 View ,我们要把 最后的图标换成我们的图片。这个逻辑主要是在 onLayout() 方法中我们需要注意。还是把代码单独贴出来重视一下

 // 对子 view 进行位置排列
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 获得子 view 数量
        int childCount = this.getChildCount();
        // 遍历子 view,取出存储在子 view 的 LayoutParams 中的 left 和 top

        for (int i =0; i < childCount; i++) {
            View child  = this.getChildAt(i);
            NinePhotoView.LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
            // 对子 view 位置进行设置
            child.layout(layoutParams.left,layoutParams.top,layoutParams.left + childWidth,
                    layoutParams.top + childHeight);

            if (i == mImageResArrayList.size() - 1 ) {

                // 需要注意的是,当选到最大上传图片数的倒数第二张图片时,会继续产生添加图片的占位符,导致
                // 子 view 数量已经达到最大上传图片数,这时点击最后的添加图片图标时,我们的操作不能再是生成子 view ,
                // 而是将最后一张的上传图标换成我们的图片
                // 如果不注意上面这个问题,会出现选择添加倒数第二张图片时,最后一张也被添加。因为
                if (mImageResArrayList.size() > MAX_PHOTO_NUMBER) {
                    child.setBackgroundResource(constImageIds[i]);
                    child.setOnClickListener(null);
                } else {
                    child.setBackgroundResource(R.drawable.add);
                    child.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            addPhotoBtnClick();
                        }
                    });
                }
            }
            else {
                child.setBackgroundResource(constImageIds[i]);
                child.setOnClickListener(null);
            }
        }

    }

还有在设置 ViewGroup 宽高的时候,要根据子 View 的宽高和具体要求来具体设置,还是单独拿出代码重视一下

// 定义控件的宽高
        int nineWidth = rw;
        int nineHeight = rh;

        Map<Integer,Integer> line = new HashMap<>();
        for (int i = 1; i < 10; i++) {
            if (i < 5) {
                line.put(i,1);
            } else if (i < 9) {
                line.put(i,2);
            } else {
                line.put(i,3);
            }

        }

        nineHeight = (line.get(childCount)) * childHeight;

        setMeasuredDimension(nineWidth,nineHeight);

Github 源码下载

重要参考:

教你搞定Android自定义ViewGroup

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值