自定义ViewGroup练习一

来源:互联网 发布:英语教材推荐 知乎 编辑:程序博客网 时间:2024/04/30 20:33

学习目标

熟悉并掌握自定义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

原创粉丝点击