Android艺术开发探索第四章——View的工作原理(下)

时间:2021-09-05 22:41:13

Android艺术开发探索第四章——View的工作原理(下)


我们上篇BB了这么多,这篇就多多少少要来点实战了,上篇主席叫我多点自己的理解,那我就多点真诚,少点套路了,老司机,开车吧!

我们这一篇就扯一个内容,那就是自定义View

  • 自定义View
    • 自定义View的分类
    • 自定义View的须知
    • 自定义View的实例
    • 自定义View的思想

一.自定义View的分类

自定义View百花齐放,没有什么具体的分类,不过可以从特性大致的分为4类,其实在我看来,就三类,继承原生View,继承View和继承ViewGroup。

  • 1.继承View重写onDraw方法

    重写了绘制,一般就是想自己实现某些图形了,因为原生控件已经满足不了你了,很显然这需要绘制的方式来完成,采用这个方式需要自身支=warp_content,并且pading也要自己处理,比较考验你的功底了

  • 2.继承ViewGroup派生出来的Layout

    这个相当于重写容器了,当某些效果看起来像是View的组合的时候,就是他上场的时候了,不过这个很复杂,需要合理的使用测量和布局这两个过程,还要兼顾子元素的这两个过程

  • 3.继承特定的View

    比如TextView,就是重写原生的View嘛,比如你想让TextView默认有颜色之类的,有一些小改动,这个就可以用它的,他相对来说比较简单,这个就不需要自己支持包裹内容和pading了

  • 4.继承特定的ViewGroup

    这个和上述一样,只不过是重写容器而已,这个也比较常见,事件分发的时候用的也多

二.自定义View的须知

这节大致的说一下注意事项

  • 1.让View支持warp_content

    这个在之前将测量的时候说过,如果你不特殊处理一下是达不到满意的效果的,这里就不重复了

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

    这是因为如果你不处理下的话,那么该属性是不会生效的,在ViewGroup也是一样

  • 3.尽量不要在View中使用Handler

    为什么不能用,是因为没有必要,View本身就有一系列的post方法,当然,你想用也没人拦着你,我倒是觉得handler写起来代码简洁很多

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

    这个问题那就更好理解了,你要是不停止这个线程或者动画,容易导致内存溢出的,所以你要在一个合适的机会销毁这些资源,在Activity有生命周期,而在View中,当View被remove的时候,onDetachedFromWindow会被调用,,和此方法对应的是onAttachedToWindow

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

    滑动冲突之前就BB过,这里就不讲了

三.自定义View的实例

  • 1.继承View重写onDraw方法

我们来实现一个很简单的图形:圆。尽管如此,还是有很多细节需要注意的,实现的过程中需要考虑warp_content和padding,OK,我们先来看代码

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);
        //View的宽
        int width = getWidth();
        //View的高
        int height = getHeight();
        //圆的半径 = 宽和高比较出的数 / 2
        int radiu = Math.min(width, height) / 2;
        //绘制圆
        canvas.drawCircle(width / 2, height / 2, radiu, mPaint);
    }
}

上面的代码就绘制出了一个圆,运行看下效果

Android艺术开发探索第四章——View的工作原理(下)

上面的代码很简单,估摸着会点自定义的完全能写出来,我们写这个案例就是要抛砖引玉,不信,我们接着看下去,我们把布局改成这个样子

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.liuguilin.viewwork.view.CircleView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="#000000" />

</LinearLayout>

现在我们来运行一下你就会看到不一样的效果了

Android艺术开发探索第四章——View的工作原理(下)

接下来我们再调整一下,只给他增加一个

    android:layout_margin="20dp"

这样会是什么效果呢?

Android艺术开发探索第四章——View的工作原理(下)

这样按理说也是我们预期的效果,对吧,这样的话margin属性是生效的,这是因为margin由父容器所控制的,所以不需要View去动,我们进一步实验,我现在给他继续增加,加上一个padding

    android:padding="20dp"

这里是重头戏了,我们运行后会发现,他没什么反应呀,我们之前说过,如果你直接继承View,在测量的时候需要做点处理的,不然的话,你的warp_content就和match_parent是一样的了。

为了解决这几个问题,我们需要做如下的处理

首先,关于warp_content的问题,我们只需要指定一个warp_content模式宽/高即可,比如设置200px作为默认的宽高

其次,针对padding的问题,我们再绘制的时候考虑进去就好了,修改后的onDraw如下

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //padding值
        int left = getPaddingLeft();
        int right = getPaddingRight();
        int top = getPaddingTop();
        int bottom = getPaddingBottom();
        //View的宽
        int width = getWidth() - left - right;
        //View的高
        int height = getHeight() - top - bottom;
        //圆的半径 = 宽和高比较出的数 / 2
        int radiu = Math.min(width, height) / 2;
        //绘制圆
        canvas.drawCircle(left + width / 2, top + height / 2, radiu, mPaint);
    }

这样就解决了,主要的逻辑就是绘制的时候考虑到View四周的空白即可,圆心和半径都会考虑到,现在我们来运行下,就有效果了

Android艺术开发探索第四章——View的工作原理(下)

最后,为了让View更加容易应用,我们需要提供一些自定义的属性,这些怎么玩呢,我们继续看

第一步实在values目录下面创建自定义属性的xml,比如attrs.xml,也可以其他名字,名字没什么限制,不过为了规范,还是…你懂的,我们就来写一个

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

这个很简单吧,我们只定义了一个颜色的属性,这里面有个format是类型,看下就懂了,然后呢

第二步,在View的构造方法里解析到我们这个属性,仔细看代码:

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray type = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        //没有指定颜色的话默认红色
        mColor = type.getColor(R.styleable.CircleView_circle_color, Color.RED);
        type.recycle();
        init();
    }

这段代码就是加载一个资源文件,拿到里面的属性,如果没有指定的话,默认就是红色了,那我们要使用的话,写一个命名空间,然后…:

<?xml version="1.0" encoding="utf-8"?>
<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:orientation="vertical">

    <com.liuguilin.viewwork.view.CircleView
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_margin="20dp"
        android:background="#000000"
        android:padding="20dp"
        app:circle_color="@color/colorPrimary" />

</LinearLayout>

上门的布局唯一要注意的就是这个命名空间了 xmlns:app=”http://schemas.android.com/apk/res-auto”,然后就可以使用app:属性的方式添加了,那我们运行一下,效果也很明显,来看下全部的代码吧:

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);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray type = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        //没有指定颜色的话默认红色
        mColor = type.getColor(R.styleable.CircleView_circle_color, Color.RED);
        type.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.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getMode(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);
        //padding值
        int left = getPaddingLeft();
        int right = getPaddingRight();
        int top = getPaddingTop();
        int bottom = getPaddingBottom();
        //View的宽
        int width = getWidth() - left - right;
        //View的高
        int height = getHeight() - top - bottom;
        //圆的半径 = 宽和高比较出的数 / 2
        int radiu = Math.min(width, height) / 2;
        //绘制圆
        canvas.drawCircle(left + width / 2, top + height / 2, radiu, mPaint);
    }
}

这代码清晰脱俗吧,简单好记,就是这样

  • 2.继承ViewGroup派生出来的Layout

这个同等于自定义布局,在之前介绍滑动的时候,有过类似的例子,主席就偷懒的搬上来了,当时分析滑动冲突的两种自定义View:HorizontalScrollViewEx和StickyLayout,其中HorizontalScrollViewEx就是通过继承ViewGroup来实现的,我们再次来分析他的测量和布局过程


这里BB一句,要规范的写View,需要一定的代价,这个,需要去看线性布局去了解了,他们的实现都很复杂,对于HorizontalScrollViewEx来说,就不这么精细了

回顾下HorizontalScrollViewEx的功能,他类似于ViewPager,或者说水平方向的线性布局,它内部的View可以竖直滑动,解决他的冲突的代码就不提了,我们主要还是看下他的测量

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec,heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
        if(childCount ==0){
            setMeasuredDimension(0, 0);
        }else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth() * childCount;
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measureWidth, measureHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpecSize, measureHeight);
        }
    }

这里发现一点小bug,不过不碍事,这里的逻辑呢,可以这样理理,首先有没有子元素,没有就全部都是0,有的话再去判断是否是warp_content,,如果是包裹内容,那这个控件的宽度就是所以的总和了,如果高度采用包裹内容,那这个控件就是第一个子元素的高度,这样说应该好理解一点

再回来说说规范性,上面的代码可以说有两点吧,首先,是不应该直接设置为0,还有就是测量的时候没有考虑到padding和子元素的maggin,好的我们继续来看下onLayout

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildSize = childCount;
        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            final int childWidth = childView.getMeasuredWidth();
            mChildWidth = childWidth;
            childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
            childLeft += childWidth;
        }
    }

这个布局的逻辑也没多少代码,我们拿到子元素之后将其放在合适的位置,位置是从左往右的,但是仍然没有考虑padding和子元素的maggin,这个也不是很规范,好的,那我们直接撸完整代码:

public class HorizontalScrollViewEx extends ViewGroup {

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

    //分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;

    //分别记录上次滑动的坐标
    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 defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        if (mScroller == null) {
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.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;
        }
        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 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);
                mVelocityTracker.clear();
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = 0;
        int measureHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth() * childCount;
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measureWidth, measureHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measureWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measureWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measureHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpecSize, measureHeight);
        }
    }

    @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);
            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());
            postInvalidate();
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

OK,代码慢慢看

四.自定义View的思想

整体来讲,还是有点模糊,不过精髓都已经体现出来了,自定义算是一个综合体系,大多数情况下还是要灵活一点,而且有些可能需要公式计算,所以比较五花八门,那我们这里肯定不能一一去概括了,但是基本功大家应该都已经了解了,我在后续的章节中会挑一些好的View来介绍,这是我,不是书上的,最主要的是基本功然后就是实现思路了,这点我特别推荐去学习优秀的开源库了解一下,好了,我们第四章,View的工作原理到这里就GG了,下章再见!!!

PPT:http://download.csdn.net/detail/qq_26787115/9699388

MakeDown:http://pan.baidu.com/s/1o7Z4Djs 密码:xdgt

Sample:http://download.csdn.net/detail/qq_26787115/9699383

我正在参加2016博客之星,请投我一票吧!