自定义View

2020-04-01  本文已影响0人  Chenstyle

4.4 自定义View

本节将详细介绍自定义View相关的知识。自定义View的作用不用多说,这个读者都应该清楚,如果想要做出绚丽的界面效果仅仅靠系统的控件是远远不够的,这个时候就必须通过自定义View来实现这些绚丽的效果。自定义View是一个综合的技术体系,它涉及View的层次结构、事件分发机制和View的工作原理等技术细节,而这些技术细节每一项又都是初学者难以掌握的,因此就不难理解为什么初学者都觉得自定义View很难这一现状了。考虑到这一点,本书在第3章和第4章的前半部分对自定义View的各种技术细节都做了详细的分析,目的就是为了让读者更好得掌握本节的内容。尽管自定义View很难,甚至面对各种复杂的效果时往往还会觉得有点无章可循。但是,本节将从一定高度来重新审视自定义View,并以综述的形式介绍自定义View的分类和须知,旨在帮助初学者能够透过现象看本质,避免陷入只见树木不见森林的状态之中。同时为了让读者更好地理解自定义View,在本节最后还会针对自定义View的不同类别分别提供一个实际的例子,通过这些例子能够让读者更深入的掌握自定义View.

4.4.1 自定义View的分类

自定义View的分类标准不唯一,而笔者则把自定义View分为4类。

1. 继承View重写onDraw方法

这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态的显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。

2. 继承ViewGroup派生特殊的Layout

这种方法主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局之外,我们重新定义了一种新布局。当某种效果看起来像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方式稍微复杂一些,需要合适的处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。

3. 继承特定的View(比如TextView)

这种方法比较常见,一般是用于扩展某种已有的View的功能,比如TextView,这种方法比较容易实现。这种方法不需要自己支持warp_content和padding等。

4. 继承特定的ViewGroup(比如LinarLayout)

这种方法也比较常见,当某种效果看起来很像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。需要注意这种方法和方法2的区别,一般来说方法2能实现的效果方法4也能实现,两者的主要差别在于方法2更接近View的底层。

上面介绍了自定义View的4种方式,读者可以仔细体会一下,是不是的确可以这么划分?但是这里要说的是,自定义View讲究的是灵活性,一种效果可能多种方法都可以实现,我们需要做的就是找到一种代价最小、最高效的方法去实现,在4.4.2节会列举一些自定义View过程中常见的注意事项。

4.4.2 自定义View须知

本节将介绍自定义View过程中的一些注意事项,这些问题如果处理不好,有些会影响View的正常使用,而有些则会导致内存泄露等,具体的注意事项如下所示。

1. 让View支持wrap_content

这是因为直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果,具体情形已经在4.3.1节中进行了详细的介绍,这里不再重复了。

2. 如果有必要,让你的View支持padding

这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。

3. 尽量不要在View中使用Handler,没必要

这是因为View内部本身就提供了post系列的方法,完全可以替代Handler的作用,当然除非你很明确的要使用Handler来发送消息。

4. View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow

这一条也很好理解,如果有线程或者动画需要停止时,那么onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动时,View的onAttchedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄露。

5. View带有滑动嵌套情形时,需要处理好滑动冲突

如果有滑动冲突的话,那么要合适的处理滑动冲突,否则将会严重影响View的效果,具体怎么解决滑动冲突请参看第3章。

4.4.3 自定义View示例

4.4.1节和4.4.2节分别介绍了自定义View的类别和注意事项,本节将通过几个实际的例子来演示如何自定义一个规范的View,通过本节的例子再结合上面两节的内容,可以让读者更好的掌握自定义View。下面仍然按照自定义View的分类来介绍具体的实现细节。

1. 继承View重写onDraw方法

这种方法主要用于实现一些不规则的效果,一般需要重写onDraw方法。采用这种方式需要自己支持wrap_content,并且padding也需要自己处理。下面通过一个具体的例子来演示如何实现这种自定义View。

为了更好的展示一些平时不容易注意到的问题,这里选择实现一个很简单的自定义控件,简单到只是绘制一个圆,尽管如此,需要注意的细节还是很多的。为了实现一个规范的控件,在实现过程中必须考虑到wrap_content模式以及padding,同时为了提高便捷性,还要对外提供自定义属性。我们先来看一下最简单的实现,代码如下所示。

public class CircleView extends View {
    private int mColor = Color.RED;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    
    public CircleView(Context context) {
        super(context);
        init();
    }
    
    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    
    private void init() {
        mPaint.setColor(mColor);
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height) / 2;
        canvas.drawCircle(width / 2, height / 2, radius, mPaint);
    }
}

上面的代码实现了一个具有圆形效果的自定义View,它会在自己的中心点以宽/高的最小值为直径绘制一个红色的实心圆,它的实现很简单,并且上面的代码相信大部分初学者都能写出来,但是不得不说,上面的代码只是一种初级的实现,并不是一个规范的自定义View,为什么这么说呢?我们通过调整布局参数来对比一下。

请看下面的布局:

<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="match_parent"
    android:background="#FFFFFF"
    android:orientation="vertical" >
    
    <com.chenstyle.chapter_4.ui.CircleView
        android:id="@+id/circleView"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000" />
    
</LinearLayout>

再看一下运行的效果,如图4-3中的(1)所示,这是我们预期的效果。接着再调整CircleView的布局参数,为其设置20dp的margin,调整后的布局如下所示。

<com.chenstyle.chapter_4.ui.CircleView
    android:id="@+id/circleView1"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_margin="20dp"
    android:background="#000000" />

再看一下运行的效果,如图4-3中的(1)所示,这是我们预期的效果。接着再调整CircleView的布局参数,为其设置20dp的margin,调整后的布局如下所示。

<com.chenstyle.chapter_4.ui.CircleView
    android:id="@+id/circleView1"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_margin="20dp"
    android:background="#000000"
    />

运行后看一下效果,如图4-3中的(2)所示,这也是我们预期的效果,这说明margin属性是生效的。这是因为margin属性是由父容器控制的,因此不需要在CircleView中做特殊处理。再调整CircleView的布局参数,为其设置20dp的padding,如下所示。

<com.chensty.chapter_4.ui.CircleView
    android:id="@+id/circleView1"
    android:layout_width="match_parent"
    android:layout_heigt="100dp"
    android:layout_margin="20dp"
    android:padding-"20dp"
    android:background="#000000"
    />

运行后看一下效果,如图4-3中的(3)所示。结果发现padding根本没有生效,这就是我们再前面提到的直接继承自View和ViewGroup的空间,padding是默认无法生效的,需要自己处理。再调整一下CircleView的布局参数,将其宽度设置为wrap_content,如下所示。

<com.chenstyle.chapter_4.ui.CircleView
    android:id="@+id/circleView1"
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:margin="20dp"
    android:padding="20dp"
    android:background="#000000"
    />

运行后看一下效果,如图4-3中的(4)所示,结果发现wrap_content并没有达到预期的效果。对比下(3)和(4)的效果图,发现宽度使用wrap_content和使用match_parent没有任何区别。的确是这样的,这一点在前面也已经提到过:对于直接继承自View的控件,如果不对wrap_content做特殊处理,那么使用wrap_content相当于使用match_patent。

图4-3 CircleView的运行效果.jpg

图4-3 CircleView的运行效果

为了解决上面提到的几种问题,为我们需要做如下处理:

首先,针对wrap_content的问题,其解决方法在4.3.1节中已经做了详细的介绍,这里只需要制定一个wrap_content模式的默认宽/高即可,比如可以选择200px作为默认的宽/高。

其次,针对padding的问题,也很简单,只要在绘制的时候考虑一下padding即可,因此我们需要很对onDraw稍微做一下修改,修改后的代码如下所示。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    final int paddingLeft = getPaddingLeft();
    final int paddingRight = getPaddingRight();
    final int paddingTop = getPaddingTop();
    final 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);
}

上面的代码很简单,中心思想就是在绘制的时候考虑到View四周的空白即可,其中圆心和半径都会考虑到View四周的padding,从而做相应的调整。

<com.chenstyle.chapter_4.ui.CircleView
    android:id="@+id/circleView1"
    android:layout_width="wrap_content"
    android:layout_height="100dp"
    android:layout_margin="20dp"
    android:padding="20dp"
    android:background="#000000"
    />

针对上面额布局参数,我们再次运行一下,结果如图4-4中的(1)所示,可以发现布局参数中的wrap_content和padding均生效了。

图4-4 CircleView运行效果图.jpg

图4-4 CircleView运行效果图

最后,为了让我们的View更加容易使用,很多情况下我们还需要为其提供自定义属性,像android:layout_width和android:padding这种以android开头的属性是系统自带的属性,那么如何添加自定义属性呢?这也不是什么难事,遵循如下几部:

第一步,在values目录下面创建自定义属性XML,比如sttrs.xml,也可以选择类似于sttrs_circle_view.xml等这种以attrs_开头的文件名,当然这个文件名并没有什么限制,可以随便取名字。针对本例来说,选择创建attrs.xml文件,文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>
</resources>

在上面的XML中声明了一个自定义属性集合“CircleView”,在这个集合里面可以有很多自定义属性,这里只定义了一个格式为“color”的属性“circle_color”,这里的格式color指的是颜色。除了颜色格式,自定义属性还有其他格式,比如reference是指资源id,dimension是指尺寸,而像string、integer和boolean这种是指基本数据类型。除了列举的这些还有其他类型,这里就不一一描述了,读者查看一下文档即可,这并没有什么难度。

第二步,在View的构造方法中解析自定义属性的值并做相应处理。对于本例来说,我们需要解析circle_color这个属性的值,代码如下所示。

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    TypeArray a = context.obtainStyledAttributes(attrs, R.styleable, CircleView);
    mColor = a.getColor(styleable.CircleView_circle_color, Color.RED);
    a.recycle();
    init();
}

这看起来很简单,首先加载自定义属性集合CircleView,接着解析CircleView属性集合中的circle_color属性,它的id为R.styleable.CircleView_circle_color。在这一步骤中,如果在使用时没有指定circle_color这个属性,那么就会选择红色作为默认的颜色值,解析完自定义属性后,通过recycle方法来实现资源,这样CircleView中所做的工作就完成了。

第三步,在布局文件中使用自定义属性,如下所示。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FFFFFF"
    android:orientation="vertical"
    >
    
    <com.chenstyle.chapter_4.ui.CircleView
        android:id="@+id/circleView1"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        app:circle_color="@color/light_green"
        android:padding="20dp"
        android:background="#000000"
        />
    
</LinearLayout>

上面的布局文件中有一点需要注意,首先,为了使用自定义属性,必须在布局文件中添加schemas声明:xmlns:app="http://schemas.android.com/apk/res-auto"。在这个声明中,app是自定义属性的前缀,当然可以换其他名字,但是CircleView中的自定义属性的前缀必须和这里的一致,然后就可以在CircleView中使用自定义属性了,比如:app:circle_color="@color/light_green"。另外,也有按照如下方式声明schemas:xmlns:app="http://schemas.android.com/apk/res/com.chenstyle.chapter_4",这种方式会在apl/res后面附加应用的包名。但是这两种方式并没有本质区别,笔者比较喜欢的是xmlns:app="http://schemas.android.com/apk/res-auto"这种声明方式。

到这里自定义属性的使用过程就完成了,运行一下程序,效果如图4-4中的(2)所示,很显然,CircleView的自定义属性circle_color生效了。下面给出CircleView的完整代码,这时的CircleView已经是一个很规范的自定义View了,如下所示。

public class CircleView extends View {
    
    private int mColor = Color.RED;
    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(contetx, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
        a.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.AI_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasureDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasureDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasureDimension(widthSpecSIZE, 200);
        }
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRighet = getPaddingLeft();
        final int paddingTop = getPaddingLeft();
        final int paddingBottom = getPaddingLeft();
        int width = getWidth() - paddingLeft - paddingRight;
        int height = getHeight() - paddingTop - paddingBottom;
        int radius = Math.min(width, height) / 2;
        cavas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2, radius, mPaint);
    }
}

2. 继承ViewGroup派生特殊的Layout

这种方法主要用于实现自定义的布局,采用这种方式稍微复杂一些,需要合适的处理ViewGroup的测量,布局这两个过程,并同时处理子元素的测量和布局过程。在第3章的3.5.3节中,我们分析了滑动冲突的两种方式并实现了两个自定义View:HorizontalScrollViewEx和StickyLayout,其中HorizontalScrollViewEx就是通过继承ViewGroup来实现的自定义View,这里会再次分析它的measure和layout过程。

需要说明的是,如果要采用此种方法实现一个很规范的自定义View,是有一定的代价的,这点通过查看LinearLayout等的源码就知道,它们的实现都很复杂。对于HorizontalScrollViewEx来说,这里不打算实现它的方方面面,仅仅是完成主要功能,但是需要规范化的地方会给出说明。

这里再回顾一下HorizontalScrollViewEx的功能,它主要是一个类似于ViewPager的控件,也可以说是一个类似于水平方向的LinearLayout的控件,它内部的子元素可以进行水平滑动并且子元素的内部还可以进行竖直滑动,这显然是存在滑动冲突的,但是HorizontalScrollViewEx内部解决了水平和竖直方向的滑动问题。关于HorizontalScrollViewEx是如何解决滑动冲突的,请参看第3章的相关内容。这里有一个假设,那就是所有子元素的宽/高都是一样的。下面主要看一下它的onMeasure和onLayout方法的实现,先看onMeasure,如下所示。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int measuredWidth = 0;
    itn measuredHeight = 0;
    final int childCount = getChildCount();
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    
    int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpeceSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    if (childCount == 0) {
        setMeasuredDimension(0, 0);
    } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        final View childView = getChildAt(0);
        measuredWidth = childView.getMeasureWidth() * childCount;
        measureHeight = childView.getMeasureHeight();
        setMeasureDimension(measuredWidth, measuredHieght);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        final View childView = getChildAt(0);
        measuredHeight = childView.getMeasureHeight();
        setMeasuredDimension(widthSpeceSize, childView.getMeasuredHeight());
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        final View childView = getChildAt(0);
        measuredWidth = childView.getMeasuredWidth() * childCount;
        setMeasuredDimension(measuredWdith, heightSpeceSize);
    }
}

这里说明一下上述代码的逻辑,首先会判断是否有子元素,如果没有子元素就直接把自己的宽/高设为0;然后就是判断宽和高是不是采用了wrap_content,如果宽采用了wrap_content,那么HorizontalScrollViewEx的宽度就是所有子元素的宽度之和;如果高度采用了wrap_content,那么HorizontalScrollViewEx的高度就是第一个子元素的高度。

上述代码不太规范的地方有两点:第一点是没有子元素的时候不应该直接把宽/高设为0,而应该根据LayoutParams中的宽/高来做相应处理;第二点是在测量HorizontalScrollViewEx的宽/高时没有考虑到它的padding以及子元素的margin,因为它的padding以及子元素的margin会影响到HorizontalScrollViewEx的宽/高。这是很好理解的,因为不管是自己的padding还是子元素的margin,占用的都是HorizontalScrollViewEx的空间。

接着再看一下HorizontalScrollViewEx的onLayout方法,如下所示。

protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childLeft = 0;
    final int childCount = getChildCount();
    mChildrenSize = childCount;
    
    for (int i = 0; i < childCount; i++) {
        final View childView = getChildAt(i);
        if (childView.getVisibility() != View.GONE) {
            final int childWdith = childView.getMeasuredWidth();
            mChildWidth = childWidth;
            childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
            childLeft += childWidth;
        }
    }
}

上述代码的逻辑并不复杂,其作用是完成子元素的定位。首先会遍历所有的子元素,如果这个子元素不是出于GONE这个状态,那么就通过layout方法会将其放置在合适的位置上。从代码上来看,这个放置过程是由左向右的,这和水平方向的LinearLayout比较类似。上述代码的不完美之处仍然出于放置子元素的过程没有考虑到自身的padding以及子元素的margin,而从一个规范的控件的角度来看,这些都是应该考虑的。下面给出HorizontalScrollViewEx的完整代码,如下所示。

public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";
    
    priavte int mChildrenSize;
    priavte int mChildrenWidth;
    private int mChildIndex;
    
    // 分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    // 分别记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;
    
    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;
    
    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }
    
    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    public HorizontalScrollViewEX(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }
    
    private void init() {
        if (mScroller == null) {
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }
    
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        
        Log.d(TAG, "intercepted = " + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        
        return intercepted;
    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            }
            case MotionEvent.ACTION_UP: {
                int scrollX = getScrollX();
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx, 0);
                break;
            }
            default:
                break;
        }
        
        mLastX = x;
        mLastY = y;
        return true;
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        
        int widthSpeceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpeceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
        } else if(heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpacMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }
    
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;
        
        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }
    
    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }
    
    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            posiInvalidate();
        }
    }
    
    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

继承特定的View(比如TextView)和继承特定的ViewGroup(比如LinearLayout)这两种方式比较简单,这里就不再举例说明了,关于第3章中提到的StickyLayout的具体实现,大家可以参看笔者在Github上的开源项目:https://github.com/singwhatiwanna/PinnedHeaderExpandableListView

4.4.4 自定义View的思想

到这里,自定义View相关的知识都已经介绍完了,可能读者还是觉得有点模糊。前面说过,自定义View是一个综合的技术体系,很多情况下需要灵活的分析从而找出最高效的方法,因此本章不可能去分析一个个具体的自定义View的实现,因为自定义View五花八门,是不可能全部分析一遍的。虽然我们不能把自定义View都分析一遍,但是我们能够提取出一种思想,在面对陌生的自定义View时,运用这个思想去快速地解决问题。这种思想的描述如下:首先要掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等,这些东西都是自定义View所必须的,尤其是那些看起来很炫的自定义View,它们往往对这些技术点的要求更高;熟练掌握基本功以后,在面对新的自定义View时,要能够对其分类并选择合适的实现思路,自定义View的实现方法的分类在4.4.1节中已经介绍过了;另外平时还需多积累一些自定义View相关的经验,并逐渐做到融会贯通,通过这种思想慢慢的就可以提高自定义View的水平了。

上一篇下一篇

猜你喜欢

热点阅读