【Android 笔记】自定义 ViewGroup

Author Avatar
vecrates 12月 14, 2017

 ViewGroup 的存在是为了管理其内部子View的测量、显示和事件响应等。自定义 ViewGroup 通常需要重写:

  • onMeasure(),测量子 View
  • onLayout(),确定子 View 的位置
  • onTouchEvent(),响应事件

    Demo

一个可以滑动的 ScrollView,其子控件是垂直方向的线性布局。支持 wrap_content 属性,支持 margin 属性。

5892091-36cfb40723bb0456

准备
  • View 的三种测量模式:EXACTLY、AT_MOST 和 UNSPECIFIED
  • scrollTo(x,y) 是滚动到坐标点为 x, y 的位置;scrollBy(x,y) 是滚动到 +x, +y 的位置
步骤

 1)重写 generateLayoutParams(),以便支持 margin
 2)重写 onMeasure(),测量所有子 View,以及自身大小
 3)重写 onLayout(),确定子 View 位置
 4)重写 onTouchEvent(),响应滚动

代码:

public class MyScrollView extends ViewGroup {

    private int mScreenHeight; //屏幕高度

    private int mChildsHeight; //wrap_ceontent情况下viewGroup高度(所有子view的高度之和)
    private int mChildsWidth; //wrap_ceontent情况下viewGroup宽度(最大子View的宽度)

    private int MEASURE_WIDTH = 0; //表示测量宽度
    private int MEASURE_HEIGHT = 1; //表示测量高度

    private int mLastY; //最后接触的Y坐标

    public MyScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScreenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
        Log.v("_v", "create MyScrollView");
    }

    /**
     * 测量子 View
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        mChildsWidth = 0;
        mChildsHeight = 0;

        //测量所有的子View
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //这里计算是为了属性值为wrap_content的情况好给viewGroup赋具体的高宽值
        MarginLayoutParams mlp;
        for(int i=0; i<count; i++) {
            View child = getChildAt(i);
            mlp = (MarginLayoutParams) child.getLayoutParams();
            //getHeight和getWidth在onLayout执行完之后才能获取
            mChildsHeight += child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;
            mChildsWidth = Math.max(mChildsWidth, child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin); //最大值为viewGroup的宽
        }
        //设置viewGroup自身高宽
        setMeasuredDimension(measureSize(widthMeasureSpec, MEASURE_WIDTH),
                measureSize(heightMeasureSpec, MEASURE_HEIGHT));
    }

    /**
     * 测量 viewGroup 的宽或高
     */
    private int measureSize(int measureSpec, int type) {
        int result = 0;
        int specMode = MeasureSpec.getMode(measureSpec); //获得测量模式
        int specSize = MeasureSpec.getSize(measureSpec); //获得测量值
        int WRAP_CONTENT;

        if(type == MEASURE_WIDTH) {
            WRAP_CONTENT = mChildsWidth;
        } else {
            WRAP_CONTENT = mChildsHeight;
        }

        switch (specMode) {
            case MeasureSpec.EXACTLY:
                //精确值模式
                result = specSize;
                break;
            case MeasureSpec.AT_MOST:
                //最大值模式
                result = WRAP_CONTENT;
                break;
            case MeasureSpec.UNSPECIFIED:
                //未指定模式,未指定我这里当成是at_most
                result = WRAP_CONTENT;
                break;
        }
        return result;
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    //参数changed表示view有新的尺寸或位置;
    // 参数l表示相对于父view的Left位置;后同理
        int count = getChildCount();
        int paintL = 0;
        int paintT = 0;
        int paintR = 0;
        int paintB = 0;

        MarginLayoutParams mlp;
        for(int i=0; i<count; i++) {
            View child = getChildAt(i);
            mlp = (MarginLayoutParams) child.getLayoutParams();

            //计算子View绘制位置
            paintT += mlp.topMargin;
            paintL = mlp.leftMargin;
            paintR = paintL + child.getMeasuredWidth();
            paintB = paintT + child.getMeasuredHeight();
            if(child.getVisibility() != GONE) {
                child.layout(paintL, paintT, paintR, paintB);
                paintT = paintB + mlp.bottomMargin;
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY(); //获得接触到的纵坐标

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int dy = y - mLastY;
                //getScrollY为 手机屏幕左上角Y坐标-viewGroup左上角Y坐标
                if(getScrollY() < 0) {
                    dy = 0;
                }
                if(getScrollY() >  getHeight() - mScreenHeight) {
                    dy = getHeight() - mScreenHeight;
                }
                //如果越界则滚回
                if(dy == 0 || dy == getHeight() - mScreenHeight) {
                    scrollTo(0, dy);
                } else {
                    //滚动到距离当前位置为dy的位置
                    //滚动坐标和android坐标相反
                    scrollBy(0, -dy);
                }
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
                break;
            case MotionEvent.ACTION_CANCEL:
                break;
        }
        return true;
    }

    /**
     * 为了支持margin,重写此方法返回marginLayoutParams
     * @param attrs
     * @return
     */
    @Override
    public MarginLayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MarginLayoutParams(getContext(), attrs);
    }
}

xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="cn.vecrates.androidjinjie.MainActivity">

    <cn.vecrates.androidjinjie.MyScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/colorAccent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="这是一个子 View0"
            android:background="#ddc"
            android:textSize="20sp"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:text="这是一个子 View1"
            android:background="#54f"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="40dp"
            android:textSize="20sp"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="400dp"
            android:text="这是一个子 View2"
            android:background="#ead"
            android:textSize="20sp"/>

    </cn.vecrates.androidjinjie.MyScrollView>


</LinearLayout>

补充:

Scroller的使用
Scroller和computeScroll关系*