博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android艺术开发探索第四章——View的工作原理(下)
阅读量:5825 次
发布时间:2019-06-18

本文共 14794 字,大约阅读时间需要 49 分钟。

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:layout_margin="20dp"

这样会是什么效果呢?

这里写图片描述

这样按理说也是我们预期的效果,对吧,这样的话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四周的空白即可,圆心和半径都会考虑到,现在我们来运行下,就有效果了

这里写图片描述

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

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

这个很简单吧,我们只定义了一个颜色的属性,这里面有个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();    }

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

上门的布局唯一要注意的就是这个命名空间了 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:

MakeDown: 密码:xdgt

Sample:

你可能感兴趣的文章
Ubuntu设置python3为默认版本
查看>>
JsonCpp 的使用
查看>>
问题账户需求分析
查看>>
JavaSE-代码块
查看>>
爬取所有校园新闻
查看>>
32、SpringBoot-整合Dubbo
查看>>
python面向对象基础
查看>>
HDU 2044 一只小蜜蜂(递归)
查看>>
docker 下 安装rancher 笔记
查看>>
spring两大核心对象IOC和AOP(新手理解)
查看>>
数据分析相关
查看>>
Python LDAP中的时间戳转换为Linux下时间
查看>>
微信小程序蓝牙连接小票打印机
查看>>
环境错误2
查看>>
C++_了解虚函数的概念
查看>>
全新jmeter视频已经上架
查看>>
Windows 8下如何删除无线配置文件
查看>>
解决Windows 7中文件关联和打开方式
查看>>
oracle系列(五)高级DBA必知的Oracle的备份与恢复(全录收集)
查看>>
hp 服务器通过串口重定向功能的使用
查看>>