让自定义 View 支持 ScrollView

看过《Android 开发艺术探索》一书的小伙伴都知道,这本书将自定义 View 分成四个类型,分别是:

  • 继承 View 重写 onDraw 方法
  • 继承 ViewGroup 派生特殊的 Layout
  • 继承已有的 View
  • 继承已有的 ViewGroup

我们本次并不讨论具体的类型应该如何实现,自定义 View 的范围实在是太宽广了,只有想不到,没有做不到。在书中任玉刚大大还提到了自定义 View 应该注意的几个方面:

  • 让 View 支持 wrap_content
  • 让 View 支持 padding
  • 尽量不要在 View 中使用 Handler
  • View 中有线程或者动画,需要及时停止
  • View 有滑动嵌套情形的,需要处理好滑动冲突

这些注意事项都非常有用,即使是一个新手做自定义 View,在本书的指引下,遵循这些标准也能做出可用性较高的自定义 View,比如说我(微笑)。不过我在实践的过程中发现一个任玉刚大大没有提到的方面,那就是让自定义 View 支持 ScrollView,毕竟 ScrollView 已经是个非常常用的布局了。

首先看一个小例子,我们就拿书中的自定义 View 案例来示范,也就是单纯的画个圆:

public class CircleView extends View {
    private int mColor = getResources().getColor(R.color.colorAccent);
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

    public CircleView(Context context) {
        super(context);
        init();
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = array.getColor(R.styleable.CircleView_circle_color,
                getResources().getColor(R.color.colorAccent));
        array.recycle();
        init();
    }

    private void init() {
        mPaint.setColor(mColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
    }
}

以上代码除了颜色我没有做其他改动,将这个 CircleView 放在纵向的 LinearLayout 中,宽度设置 match_parent,高度设置 wrap_content,背景色设置为黑色,为了比较,在其下面放一个 TextView,我们来看看显示的结果:

让自定义 View 支持 ScrollView_第1张图片

还是很正常的,符合我们的预期。

如果在 Layout 最外层套一个 ScrollView,再来看看:

让自定义 View 支持 ScrollView_第2张图片

自定义 View 看不见了!首先自定义 View 的外层是 LinearLayout,高度是 match_parent,从常理来分析,ScrollView 内部的高度无限大的,如果内部的 View 的不做精确设置,可能会导致 View 无限大,所以 ScrollView 内部没有设置精确高度的 View 都会无法显示,除非内部做特殊处理。比如下面的 TextView ,设置的高度也是 wrap_content,但它却能显示,为什么呢?按照我们在 onMeasure 方法中的逻辑,如果自定义 View 是大小不定,也就是对应 MeasureSpec.AT_MOST,那么宽高都应该为默认的 200 才对,这样也不会不显示。那么就调试一下看看:

让自定义 View 支持 ScrollView_第3张图片

heightMeasureSpec 的值是0,我们知道 MesureSpec 是一个 32 位的 int 值,高 2 位表示测量模式,低 30 位表示在这种模式下的测量值。显然这不属于任何一种 MeasureSpec 已知的模式,所以自定义 View 无法获得测量高度,也就无法显示了。知道了原因就好办了,只需要对 heightMeasureSpec 的值作出识别处理就行了,比如下面是我的方法:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        //避免在 scrollView 里获取不到高度
        if (heightMeasureSpec == 0) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.AT_MOST);
        }
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, widthSpecSize);
        }
    }

如果无法获取 heightMeasureSpec,就用 widthSpecSize 重新实例化一个 heightMeasureSpec 出来,模式设置为 AT_MOST,值默认与宽度相同,如果获取不到高度,就默认设置为与宽度相同。因为这里是个圆,那么就有个好处,即使宽高设置的都是 match_parent,那么真正的高度也只是最大宽大的值,毕竟在 ScrollView 中高度是不会有 match_parent 的效果的。当然根据自己的 View 的用途最好设置适合的默认值。

看看效果:

让自定义 View 支持 ScrollView_第4张图片

再把高度设置为 match_parent

让自定义 View 支持 ScrollView_第5张图片

一样的效果,这种在 ScrollView 中就算是一种比较合理的方式,并且完全不会影响自定义 View 在非 ScrollView 布局中的表现。所以除了任玉刚大大提到的 5 点注意事项,我还想再加一条,那就是 让自定义 View 支持 ScrollView

本文最早发布于 alphagao.com 。

你可能感兴趣的:(让自定义 View 支持 ScrollView)