一篇文章搞懂Android 自定义viewgroup的难点

本文的目的

目的在于教会你们到底如何自定义viewgroup,自定义布局和自定义测量到底如何写。不少网上随便搜搜的概念和流程图 这里再也不过多描述了,建议你们看本文以前,先看看基本的自定义viewgroup流程,心中有个大概便可。本文注重于实践android

viewgroup 的测量布局流程基本梳理

稍微回顾下,基本的viewgroup绘制和布局流程中的重点:算法

1.view 在onMeasure()方法中进行自我测量和保存,也就是说对于view(不是viewgroup噢)来讲必定在onMeasure方法中 计算出本身的尺寸而且保存下来canvas

2.viewgroup实际上最终也是循环从上大小来调用子view的measure方法,注意子view的measure其实最终调用的是子view的onMeasure 方法。因此咱们理解这个过程为: viewgroup循环遍历调用全部子view的onmeasure方法,利用onmeasure方法计算出来的大小,来肯定这些子view最终能够占用的大小和所处的布局的位置。bash

3.measure方法是一个final方法,能够理解为作测量工做准备工做的,既然是final方法因此咱们没法重写它,不须要过多 关注他,由于measure最终要调用onmeasure ,这个onmeasure咱们是能够重写的。要关注这个。layout和onlayout是同样的 关系。app

4.父view调用子view的layout方法的时候会把以前measure阶段肯定的位置和大小都传递给子view。ide

5.对于自定义view/viewgroup来讲 咱们几乎只须要关注下面三种需求:布局

  • 对于已有的android自带的view,咱们只须要重写他的onMeasure方法便可。修改一下这个尺寸便可完成需求。
  • 对于android系统没有的,属于咱们自定义的view,比上面那个要复杂一点,要彻底重写onMeasure方法。
  • 第三种最复杂,须要重写onmeasure和onlayout2个方法,来完成一个复杂viewgroup的测量和布局。
  1. onMeasure方法的特殊说明:

  1. 如何理解父view对子view的限制?字体

    onMeasure的两个参数既然是父view对子view的限制,那么这个限制的值究竟是哪来的呢?ui

    实际上,父view对子view的限制绝大多数就来自于咱们开发者所设置的layout开头的这些属性spa

    比方说咱们给一个imageview设置了他的layout_width和layout_height 这2个属性,那这2个属性其实就是咱们开发者 所指望的宽高属性,可是要注意了, 设置的这2个属性是给父view看的,实际上对于绝大多数的layout开头的属性这些属性都是设置给父view看的

    为何要给父view看?由于父view要知道这些属性之后才知道要对子view的测量加以什么限制?

    究竟是不限制(UNSPECIFIED)?仍是限制个最大值(AT_MOST),让子view不超过这个值?仍是直接限制死,我让你是多少就得是多少(EXACTLY)。

自定义一个BannerImageView 修改onMeasure方法

所谓bannerImageview,就是不少电商其实都会放广告图,这个广告图的宽高比都是可变的,咱们在平常开发过程当中 也会常常接触到这种需求:imageview的宽高比 在高保真中都标注出来,可是考虑到不少手机的屏幕宽度或者高度都不肯定 因此咱们一般都要手动来计算出这个imageview高度或者宽度,而后动态改变width或者height的值。这种方法可用可是很麻烦 这里给出一个自定义的imageview,经过设置一个ratio的属性便可动态的设置iv的高度。非常方便

看下效果

最后看下代码,重要的部分都写在注释里了,再也不过多讲了。

public class BannerImageView extends ImageView {

    //宽高比
    float ratio;

    public BannerImageView(Context context) {
        super(context);
    }

    public BannerImageView(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BannerImageView);
        ratio = typedArray.getFloat(R.styleable.BannerImageView_ratio, 1.0f);
        typedArray.recycle();
    }

    public BannerImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //人家本身的测量仍是要本身走一遍的,由于这个方法内部会调用setMeasuredDimension方法来保存测量结果了
        //只有保存了之后 咱们才能取得这个测量结果 不然你下面是取不到的
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //取测量结果
        int mWidth = getMeasuredWidth();

        int mHeight = (int) (mWidth * ratio);

        //保存了之后,父view就能够拿到这个测量的宽高了。不保存是拿不到的噢。
        setMeasuredDimension(mWidth, mHeight);
    }
}
复制代码

自定义view,彻底本身写onMeasure方法

首先明确一个结论:

对于彻底自定义的view,彻底本身写的onMeasure方法来讲,你保存的宽高必需要符合父view的限制,不然会发生bug, 保存父view对子view的限制的方法也很简单直接调用resolveSize方法便可。

因此对于彻底自定义的view onMeasure方法也不难写了,

  1. 先算本身想要的宽高,好比你画了个圆,那么宽高就确定是半径的两倍大小, 要是圆下面还有字, 那么高度确定除了半径的两倍还要有字体的大小。对吧。很简单。这个纯看你自定义view是啥样的

  2. 算完本身想要的宽高之后 直接拿resolveSize 方法处理一下 便可。

  3. 最后setMeasuredDimension 保存。

范例:

public class LoadingView extends View {

    //圆形的半径
    int radius;

    //圆形外部矩形rect的起点
    int left = 10, top = 30;


    Paint mPaint = new Paint();

    public LoadingView(Context context) {
        super(context);
    }

    public LoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
        radius = typedArray.getInt(R.styleable.LoadingView_radius, 0);
    }

    public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);


        int width = left + radius * 2;
        int height = top + radius * 2;

        //必定要用resolveSize方法来格式化一下你的view宽高噢,不然遇到某些layout的时候必定会出现奇怪的bug的。
        //由于不用这个 你就彻底没有父view的感觉了 最后强调一遍
        width = resolveSize(width, widthMeasureSpec);
        height = resolveSize(height, heightMeasureSpec);

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        RectF oval = new RectF(left, top,
                left + radius * 2, top + radius * 2);
        mPaint.setColor(Color.BLUE);
        canvas.drawRect(oval, mPaint);
        //先画圆弧
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(2);
        canvas.drawArc(oval, -90, 360, false, mPaint);
    }
}

复制代码

布局文件:

<LinearLayout
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#000000"
        android:orientation="horizontal">

        <com.example.a16040657.customviewtest.LoadingView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/dly"
            app:radius="200"></com.example.a16040657.customviewtest.LoadingView>

        <com.example.a16040657.customviewtest.LoadingView
            android:layout_marginLeft="10dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/dly"
            app:radius="200"></com.example.a16040657.customviewtest.LoadingView>
    </LinearLayout>

复制代码

最后效果:

自定义一个viewgroup

这个其实也就是稍微复杂了一点,可是仍是有迹可循的,只是稍微须要一点额外的耐心。

自定义一个viewgroup 须要注意的点以下:

  1. 必定是先重写onMeasure肯定子view的宽高和本身的宽高之后 才能够继续写onlayout 对这些子view进行布局噢~~
  2. viewgroup 的onMeasure其实就是遍历本身的view 对本身的每个子view进行measure,绝大多数时候对子view的 measure均可以直接用 measureChild()这个方法来替代,简化咱们的写法,若是你的viewgroup很复杂的话 没法就是本身写一遍measureChild 而不是调用measureChild 罢了。
  3. 计算出viewgroup本身的尺寸而且保存,保存的方法仍是哪一个setMeasuredDimension 不要忘记了
  4. 逼不得已要重写measureChild方法的时候,其实也不难无非就是对父view的测量和子view的测量 作一个取舍关系而已, 你看懂了基础的measureChild方法,之后就确定会写本身的复杂的measureChild方法了。

下面是一个极简的例子,一个很简单的flowlayout的实现,没有对margin paddding作处理,也假设了每个tag的高度 是固定的,能够说是极为简单了,可是麻雀虽小 五脏俱全,足够大家好好理解自定义viewgroup的关键点了。

/**
 * 写一个简单的flowlayout 从左到右的简单layout,若是宽度不够放 就直接另起一行layout
 * 这个相似的开源控件有不少,有不少写的出色的,我这里只仅仅实现一个初级的flowlayout
 * 也是最简单的,目的是为了理解自定义viewgroup的关键核心点。
 * <p>
 * 比方说这里并无对padding或者margin作特殊处理,大家本身写viewgroup的时候 记得把这些属性的处理都加上
 * 不然一旦有人用了这些属性 发现没有生效就比较难看了。。。。。。
 */
public class SimpleFlowLayout extends ViewGroup {
    public SimpleFlowLayout(Context context) {
        super(context);
    }

    public SimpleFlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SimpleFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    /**
     * layout的算法 其实就是 不够放剩下一行 那另外放一行 这个过程必定要本身写一遍才能体会,
     * 我的有我的的写法,说不定你的写法比开源的项目还要好
     * 其实也没什么夸张的,没法就是前面onMeasure结束之后 你能够拿到全部子view和本身的 测量宽高 而后就算呗
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childTop = 0;
        int childLeft = 0;
        int childRight = 0;
        int childBottom = 0;

        //已使用 width
        int usedWidth = 0;


        //customlayout 本身可以使用的宽度
        int layoutWidth = getMeasuredWidth();
        Log.v("wuyue", "layoutWidth==" + layoutWidth);
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            //取得这个子view要求的宽度和高度
            int childWidth = childView.getMeasuredWidth();
            int childHeight = childView.getMeasuredHeight();

            //若是宽度不够了 就另外启动一行
            if (layoutWidth - usedWidth < childWidth) {
                childLeft = 0;
                usedWidth = 0;
                childTop += childHeight;
                childRight = childWidth;
                childBottom = childTop + childHeight;
                childView.layout(0, childTop, childRight, childBottom);
                usedWidth = usedWidth + childWidth;
                childLeft = childWidth;
                continue;
            }
            childRight = childLeft + childWidth;
            childBottom = childTop + childHeight;
            childView.layout(childLeft, childTop, childRight, childBottom);
            childLeft = childLeft + childWidth;
            usedWidth = usedWidth + childWidth;

        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //先取出SimpleFlowLayout的父view 对SimpleFlowLayout 的测量限制 这一步很重要噢。
        //你只有知道本身的宽高 才能限制你子view的宽高
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);


        int usedWidth = 0;      //已使用的宽度
        int remaining = 0;      //剩余可用宽度
        int totalHeight = 0;    //总高度
        int lineHeight = 0;     //当前行高

        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            LayoutParams lp = childView.getLayoutParams();

            //先测量子view
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            //而后计算一下宽度里面 还有多少是可用的 也就是剩余可用宽度
            remaining = widthSize - usedWidth;

            //若是一行不够放了,也就是说这个子view测量的宽度 大于 这一行 剩下的宽度的时候 咱们就要另外启一行了
            if (childView.getMeasuredWidth() > remaining) {
                //另外启动一行的时候,使用过的宽度 固然要设置为0
                usedWidth = 0;
                //另外启动一行了 咱们的总高度也要加一下,否则高度就不对了
                totalHeight = totalHeight + lineHeight;
            }

            //已使用 width 进行 累加
            usedWidth = usedWidth + childView.getMeasuredWidth();
            //当前 view 的高度
            lineHeight = childView.getMeasuredHeight();
        }

        //若是SimpleFlowLayout 的高度 为wrap cotent的时候 才用咱们叠加的高度,不然,咱们固然用父view对若是SimpleFlowLayout 限制的高度
        if (heightMode == MeasureSpec.AT_MOST) {
            heightSize = totalHeight;
        }
        setMeasuredDimension(widthSize, heightSize);
    }
}

复制代码

最后看下效果

相关文章
相关标签/搜索