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

枕上诗书闲处好,门前风景雨来佳。这篇文章主要讲述Android艺术开发探索第四章——View的工作原理(下)相关的知识,希望能为你提供帮助。
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的思想
【Android艺术开发探索第四章——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博客之星, 请投我一票吧!

    推荐阅读