View是Android很重要的一部分,常用的View有Button、TextView、EditView、ListView、GridView、各种layout等等,开发者通过对这些View的各种组合以形成丰富多彩的交互界面,一个应用中界面交互的体验往往在应用的受欢迎程度上起了很关键得作用,所以开发者们大多会想方设法的做出一个更加精美的界面,例如:通过自定义View、深入学习View的原理以便更好的对其优化使其在操作起来更加流畅等等,也正因为如此,在面试中View也常常作为面试官重点考察的对象之一。

View是所有控件的基类,包括Button、TextView、EditView等等都直接或间接继承自view,View下面还有ViewGroup子类,即LinearLayout、RelativeLayout等都属于ViewGroup。

我们需要知道的是在Android中,无论是View还是其他界面,右方向代表着x轴的正向,下方向代表着y轴的正向。

View 的基本工作原理

在 ActivityThread 中,当Activity被创建后会将 DecorView 添加到 Window 中,同时创建 ViewRootImpl 对象,并将 ViewRootImpl 和 DecorView 建立关联,而 DecorView 就是一个 Activity 的顶级 View,在一个默认的主题中,它分为标题栏,和内容区域,我们所添加的 View 均是添加到了 DecorView 的内容区域,这些被添加进去的 View 的工作流程正式通过 ViewRootImpl 完成的。

ViewRoot、DecorView 及 View 的三大流程简介:

  • ViewRoot:对应于 ViewRootImpl,链接 WindowManager 和 DecorView 的纽带,View 的三大流程均是通过它完成的。(View 的绘制流程是从 ViewRoot 的 performTraversals() 方法开始的,它经过 measure、layout、draw 三个流程最终才能将一个 View 完整的绘制出来。)

  • DecorView:新建一个 Android 应用时我们都知道,默认主题的情况下这个应用的界面会分为两部分:标题栏、内容区域。而这个界面的顶级 View 就是 DecorView。

  • View的绘制经过了 measure、layout、draw 三个流程:

  1. measure:对应 onMeasure() 方法,测量View的宽、高。
  2. layout:对应 onLayout() 方法,确定view的四个顶点,即确定View在父容器中的位置。
  3. draw:对应 onDraw(),绘制View。在自定义 View 时我们也正是在 onDraw() 方法内可以在 Canvas 画布上随心所欲的画出我们想要的 View。

自定义 View

自定义 View 的方式不止一种,可以直接继承 View,重写 onDraw() 方法,也可以直接继承 ViewGroup,还可以继承现有的控件(如:TextView、LinearLayout)等,本篇主要介绍一下直接继承 View 的方式。

直接继承 View 来实现自定义 View 的这种方式比较灵活,可以实现很多复杂的效果,这种方式最关键的步骤就是重写 onDraw() 方法,通过 Paint 画笔等工具在 Canvas 画布上进行各种图案的绘制以达到我们想要的效果。

其实在自定义 View 过程中,难点往往不是怎么使用画笔本身,而是绘制出预期效果的思路,例如:你想通过自定义 View 来做一个折线图控件,传入一组数据怎么确定这些数据在画布上对应点的相对坐标,而确定点的坐标就需要通过相关的数学公式来计算了,推算出合适的公式往往就是解决问题的关键。

接下来就用这种方式来写个圆形的小 demo 来说明一下自定义 View 的流程。

  • 新建一个继承 View 的类,添加构造方法,设置 Paint 画笔,重写 onDraw() 方法,先在画布上以最简单的方式话一个半径为100的圆。
  1. /**
  2. * 自定义 View 简单示例
  3. * Created by liuwei on 17/12/14.
  4. */
  5. public class MyView extends View {
  6. private final static String TAG = MyView.class.getSimpleName();
  7. private Paint mPaint = new Paint();
  8. private int mColor = Color.parseColor("#ff0000");
  9. public MyView(Context context) {
  10. super(context);
  11. Log.i(TAG, "MyView(Context context):content=" + context);
  12. init();
  13. }
  14. public MyView(Context context, @Nullable AttributeSet attrs) {
  15. super(context, attrs);
  16. Log.i(TAG, "MyView(Context context, @Nullable AttributeSet attrs):content=" + context + " | attrs=" + attrs);
  17. init();
  18. }
  19. private void init() {
  20. mPaint.setAntiAlias(true); // 消除锯齿
  21. mPaint.setColor(mColor); // 为画笔设置颜色
  22. }
  23. @Override
  24. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  25. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  26. // 重写此方法,对自定义控件在 wrap_content 情况下设置默认宽、高
  27. int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
  28. int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
  29. int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
  30. int heithtSpecSize = MeasureSpec.getSize(heightMeasureSpec);
  31. if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
  32. setMeasuredDimension(200, 200);
  33. } else if (widthSpecMode == MeasureSpec.AT_MOST) {
  34. setMeasuredDimension(200, heithtSpecSize);
  35. } else if (heightSpecMode == MeasureSpec.AT_MOST) {
  36. setMeasuredDimension(widthSpecSize, 200);
  37. }
  38. }
  39. @Override
  40. protected void onDraw(Canvas canvas) {
  41. Log.i(TAG, "onDraw: ");
  42. super.onDraw(canvas);
  43. canvas.drawCircle(100, 100, 100, mPaint);
  44. }
  45. }

运行结果就是一个红色的实心圆,在这个示例中为了使得布局文件中的 wrap_content 正常生效,重写了 onMeasure() 方法,关于这个问题,在这篇博文《Android查缺补漏--自定义 View 中 wrap_content 无效的解决方案》中也介绍过了,这里就不多说了。

  • 将上面的圆再扩展一下:做成以画布的可用区域的中心为圆点,画出最大的圆。同时为自定义 View 设置 padding

    对于一个控件,有 margin 和 padding,margin 是外间距,属于控件之外的范围,在自定义 View 时不需要对 margin 做特殊处理。但 padding 就不同了,是内间距,需要我们在控件的内部做处理才能让布局文件中对控件设置的 padding 生效。
  1. private int mPaddingTop;
  2. private int mPaddingBottom;
  3. private int mPaddingLeft;
  4. private int mPaddingRight;
  5. private int mUsableWidth; // 可用宽度(减去padding后的宽度)
  6. private int mUsableHeight;// 可用高度(减去padding后的高度)
  7. private int mUsableStartX = 0; // 画笔起始点的x坐标
  8. private int mUsableStartY = 0; // 画笔其实点的y坐标
  9. private int mCircleX; // 圆心x坐标
  10. private int mCircleY; // 圆心y坐标
  11. private int mCircleRadius;// 圆的半径
  12. @Override
  13. protected void onDraw(Canvas canvas) {
  14. super.onDraw(canvas);
  15. mPaddingTop = getPaddingTop();
  16. mPaddingBottom = getPaddingBottom();
  17. mPaddingLeft = getPaddingLeft();
  18. mPaddingRight = getPaddingRight();
  19. // 可用宽度和宽度要考虑padding
  20. mUsableWidth = getWidth() - mPaddingRight - mPaddingLeft;
  21. mUsableHeight = getHeight() - mPaddingTop - mPaddingBottom;
  22. // 画笔起始点要考虑padding
  23. mUsableStartX = mPaddingLeft;
  24. mUsableStartY = mPaddingTop;
  25. // 确定可用区域的中心为圆心
  26. mCircleX = mUsableStartX + mUsableWidth / 2;
  27. mCircleY = mUsableStartY + mUsableHeight / 2;
  28. // 确定圆的半径,以可用宽度和高度两者较短的一半为圆的半径
  29. if (mUsableWidth <= mUsableHeight) {
  30. mCircleRadius = mUsableWidth / 2;
  31. } else {
  32. mCircleRadius = mUsableHeight / 2;
  33. }
  34. canvas.drawCircle(mCircleX, mCircleY, mCircleRadius, mPaint);
  35. }

在布局文件中设置 paddingLeft 为15dp,paddingRight 为30dp,为了更好看出间距,将控件的背景颜色设为了黑色,查看效果:

  1. <cn.codingblock.view.reset_view.MyView
  2. android:id="@+id/myview"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:layout_margin="10dp"
  6. android:paddingLeft="15dp"
  7. android:paddingRight="30dp"
  8. android:background="#000"/>

效果图:

可见,在 onDraw() 方法对padding处理之后,在布局文件中无论怎么设置padding,都能保证圆心在可用区域的中心。

  • 为自定义 View 添加自定义属性

首先在 res/values 路径下创建一个xml文件,添加一个设置圆的颜色的属性:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3. <declare-styleable name="MyView">
  4. <attr name="circle_color" format="color"/>
  5. </declare-styleable>
  6. </resources>

在构造方法中解析属性

  1. public MyView(Context context, @Nullable AttributeSet attrs) {
  2. super(context, attrs);
  3. TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.MyView);
  4. mColor = typeArray.getColor(R.styleable.MyView_circle_color, mColor);
  5. typeArray.recycle();
  6. init();
  7. }

最后在布局文件中这是属性就可以了,要注意的是,在使用自定义属性时要添加 xmlns:app="http://schemas.android.com/apk/res-auto" 才可以。

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:app="http://schemas.android.com/apk/res-auto"
  4. xmlns:tools="http://schemas.android.com/tools"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. android:orientation="vertical"
  8. tools:context="cn.codingblock.view.activity.MyViewActivity">
  9. <cn.codingblock.view.reset_view.MyView
  10. android:id="@+id/myview"
  11. android:layout_width="match_parent"
  12. android:layout_height="match_parent"
  13. android:layout_margin="10dp"
  14. android:paddingLeft="15dp"
  15. android:paddingRight="30dp"
  16. app:circle_color="#ad42ce"
  17. android:background="#000"/>
  18. </LinearLayout>

改变颜色后的效果图如下:

为自定义View添加交互事件

MotionEvent 触摸事件

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. switch (event.getAction()) {
  4. case MotionEvent.ACTION_DOWN:
  5. Log.i(TAG, "onTouchEvent: ACTION_DOWN");
  6. break;
  7. case MotionEvent.ACTION_UP:
  8. Log.i(TAG, "onTouchEvent: ACTION_UP");
  9. break;
  10. case MotionEvent.ACTION_MOVE:
  11. Log.i(TAG, "onTouchEvent: ACTION_MOVE");
  12. break;
  13. }
  14. return super.onTouchEvent(event);
  15. }

在自定义 View 中,重写 onTouchEvent() 方法,获取 MotionEvent,正如上面代码所写,MotionEvent 比较常用的事件有三种 ACTION_DOWN、ACTION_MOVE、ACTION_UP 分别对应手指按下-移动-离开。

接下来对上面的圆形demo添加一个小事件,就是每当手指点击一下屏幕,圆形就随机换一种颜色:

  1. private Random mRandom = new Random(100);
  2. private int[] mColors = new int[] {
  3. Color.parseColor("#ff0000"),
  4. Color.parseColor("#ffffff"),
  5. Color.parseColor("#ff00ff"),
  6. Color.parseColor("#ffff00"),
  7. Color.parseColor("#ff00ff"),
  8. Color.parseColor("#0000ff")
  9. };
  10. @Override
  11. public boolean onTouchEvent(MotionEvent event) {
  12. switch (event.getAction()) {
  13. case MotionEvent.ACTION_DOWN:
  14. mColor = mColors[mRandom.nextInt(6)];
  15. mPaint.setColor(mColor);
  16. invalidate(); // 通知控件重绘
  17. break;
  18. case MotionEvent.ACTION_UP:
  19. Log.i(TAG, "onTouchEvent: ACTION_UP");
  20. break;
  21. case MotionEvent.ACTION_MOVE:
  22. Log.i(TAG, "onTouchEvent: ACTION_MOVE");
  23. break;
  24. }
  25. return super.onTouchEvent(event);
  26. }

效果如下:

大家也可以在此基础上稍微再扩展一下,例如:通过 event.getX() 和 event.getY() 获取触摸点的坐标,判断出点是否落在了圆形区域内,从而使只有点手指点到圆形区域内才改变颜色,否则不改变。感兴趣的童鞋可自行动手试一试。

在上面代码中通知 View 重绘时使用了 invalidate() 方法,其实 postInvalidate() 也可以通知 View 重绘,那么这两者有什么区别呢?

其实简单来说,invalidate() 只能在 UI 线程中使用,而 postInvalidate() 可以在子线程中使用。

ScaleGestureDetector 缩放手指检测

除了上面最普通的 MotionEvent 事件之外,Android 还提供了很多有趣的事件,就想 GestureDetector(手势检测)、VelocityTracker(速度追踪)等等,用起来也都很方便,其实只要你愿意,这些事件也完全可以在 onTouchEvent() 方法中实现,接下来在为上述的圆形 Demo 添加一个缩放的功能,也就是使用 ScaleGestureDetector 实现,效果跟平时在手机查看照片时我们用两根手指来放大/缩小图片一样。

ScaleGestureDetector 在使用起来也很简单,首先需要初始化并为其添加一个放缩手势监听器,并且需要在 onTouchEvent() 方法内,通过 ScaleGestureDetector.onTouchEvent(event) 来让 ScaleGestureDetector 接管触摸事件,其余的事项请注意看代码中的注释。

在上述代码的基础上新增如下代码:

  1. private Context mContext;
  2. private ScaleGestureDetector mScaleGestureDetector; // 缩放手势检测
  3. private float mScaleRate = 1; // 缩放比率
  4. private void init() {
  5. mPaint.setAntiAlias(true); // 消除锯齿
  6. mPaint.setColor(mColor); // 为画笔设置颜色
  7. // 初始化 ScaleGestureDetector 并添加缩放手势监听器
  8. mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener);
  9. }
  10. @Override
  11. protected void onDraw(Canvas canvas) {
  12. super.onDraw(canvas);
  13. mPaddingTop = getPaddingTop();
  14. mPaddingBottom = getPaddingBottom();
  15. mPaddingLeft = getPaddingLeft();
  16. mPaddingRight = getPaddingRight();
  17. // 可用宽度和宽度要考虑padding
  18. mUsableWidth = getWidth() - mPaddingRight - mPaddingLeft;
  19. mUsableHeight = getHeight() - mPaddingTop - mPaddingBottom;
  20. // 画笔起始点要考虑padding
  21. mUsableStartX = mPaddingLeft;
  22. mUsableStartY = mPaddingTop;
  23. // 确定可用区域的中心为圆心
  24. mCircleX = mUsableStartX + mUsableWidth / 2;
  25. mCircleY = mUsableStartY + mUsableHeight / 2;
  26. // 确定圆的半径,以可用宽度和高度两者较短的一半为圆的半径
  27. if (mUsableWidth <= mUsableHeight) {
  28. mCircleRadius = mUsableWidth / 2;
  29. } else {
  30. mCircleRadius = mUsableHeight / 2;
  31. }
  32. // 让半径乘以缩放倍率
  33. mCircleRadius *= mScaleRate;
  34. canvas.drawCircle(mCircleX, mCircleY, mCircleRadius, mPaint);
  35. }
  36. @Override
  37. public boolean onTouchEvent(MotionEvent event) {
  38. switch (event.getAction()) {
  39. case MotionEvent.ACTION_DOWN:
  40. mColor = mColors[mRandom.nextInt(6)];
  41. mPaint.setColor(mColor);
  42. invalidate(); // 通知控件重绘
  43. break;
  44. case MotionEvent.ACTION_UP:
  45. Log.i(TAG, "onTouchEvent: ACTION_UP");
  46. break;
  47. case MotionEvent.ACTION_MOVE:
  48. Log.i(TAG, "onTouchEvent: ACTION_MOVE");
  49. break;
  50. }
  51. // 让缩放手势检测器接管触摸事件
  52. if (mScaleGestureDetector.onTouchEvent(event)) {
  53. return true;
  54. }
  55. return super.onTouchEvent(event);
  56. }
  57. private ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = new ScaleGestureDetector.OnScaleGestureListener() {
  58. @Override
  59. public boolean onScale(ScaleGestureDetector detector) {
  60. Log.i(TAG, "onScale: " + detector.getScaleFactor());
  61. // 获取缩放比例因子并累乘到缩放倍率上
  62. mScaleRate *= detector.getScaleFactor();
  63. postInvalidate();
  64. return true;
  65. }
  66. @Override
  67. public boolean onScaleBegin(ScaleGestureDetector detector) {
  68. Log.i(TAG, "onScaleBegin: " + detector.getScaleFactor());
  69. return true;
  70. }
  71. @Override
  72. public void onScaleEnd(ScaleGestureDetector detector) {
  73. Log.i(TAG, "onScaleEnd: " + detector.getScaleFactor());
  74. }
  75. };

上面代码需要注意的是,在 ScaleGestureDetector 捕获到事件后要正确的将事件消费掉(注意代码中返回 true 的地方),不然缩放手势无法正常工作。

自定义 View 在 Android 中一直以来都是很重要的一部分,在平时的开发想要做出一个个性炫酷的交互界面是离不开自定义 View,自定义 View 说难不难,说简单也不简单,总之,千里之行,始于足下,只要我们掌握好自定义 View 的基础知识,再复杂的界面也可以一步步完成。


最后想说的是,本系列文章为博主对Android知识进行再次梳理,查缺补漏的学习过程,一方面是对自己遗忘的东西加以复习重新掌握,另一方面相信在重新学习的过程中定会有巨大的新收获,如果你也有跟我同样的想法,不妨关注我一起学习,互相探讨,共同进步!

参考文献:

  • 《Android开发艺术探索》
  • 《Android开发进阶从小工到专家》

Android查缺补漏(View篇)--自定义 View 的基本流程的更多相关文章

  1. Android查缺补漏(View篇)--事件分发机制源码分析

    在上一篇博文中分析了事件分发的流程及规则,本篇会从源码的角度更进一步理解事件分发机制的原理,如果对事件分发规则还不太清楚的童鞋,建议先看一下上一篇博文 <Android查缺补漏(View篇)-- ...

  2. Android查缺补漏(IPC篇)-- Bundle、文件共享、ContentProvider、Messenger四种进程间通讯介绍

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8387752.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  3. Android查缺补漏(IPC篇)-- 款进程通讯之AIDL详解

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  4. Android查缺补漏(IPC篇)-- 进程间通讯之AIDL详解

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8436529.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  5. Android查缺补漏(IPC篇)-- 进程间通讯基础知识热身

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8479282.html 在Android中进程间通信是比较难的一部分,同时又非常 ...

  6. Android查缺补漏(IPC篇)-- 进程间通讯之Socket简介及示例

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8425736.html 进程间通讯篇系列文章目录: Android查缺补漏(IP ...

  7. Android查缺补漏(线程篇)-- IntentService的源码浅析

    本文作者:CodingBlock 文章链接:http://www.cnblogs.com/codingblock/p/8975114.html 在Android中有两个比较容易弄混的概念,Servic ...

  8. Android查缺补漏(View篇)--自定义View利器Canvas和Paint详解

    上篇文章介绍了自定义View的创建流程,从宏观上给出了一个自定义View的创建步骤,本篇是上一篇文章的延续,介绍了自定义View中两个必不可少的工具Canvas和Paint,从细节上更进一步的讲解自定 ...

  9. Android查缺补漏(View篇)--在 Activity 的 onCreate() 方法中为什么获取 View 的宽和高为0?

    在 Activity 的 onCreate() 方法中为什么获取 View 的宽和高为0 ? @Override protected void onCreate(Bundle savedInstanc ...

随机推荐

  1. [转载] java的动态代理机制详解

    转载自http://www.cnblogs.com/xiaoluo501395377/p/3383130.html 代理模式 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代 ...

  2. 五、VueJs 填坑日记之将接口用webpack代理到本地

    上一篇博文,我们已经顺利的从cnodejs.org请求到了数据,但是大家可以注意到我们的/src/api/index.js的第一句就是: // 配置API接口地址 var root = 'https: ...

  3. IIS发布网站浏览之后看到的是文件目录 & Internal Server Error 处理程序“ExtensionlessUrlHandler-ISAPI-4.0_64bit”在其模块列表中有一个错误模块“IsapiModule” 解决方法 & App_global.asax.pduxejp_.dll”--“拒绝访问。 ”

    Q:IIS发布网站浏览之后看到的是文件目录 A:它出现了一个说到.NET4.0 更高框架什么的错误,所以我将 .NTE CRL版本由4.0改为2.0了,改为2.0后就出现了只能浏览文件目录了.改为4. ...

  4. Hangfire在ASP.NET CORE中的简单实现

    hangfire是执行后台任务的利器,具体请看官网介绍:https://www.hangfire.io/ 新建一个asp.net core mvc 项目 引入nuget包 Hangfire.AspNe ...

  5. [LeetCode] N皇后问题

    LeetCode上面关于N皇后有两道题目:51 N-Queens:https://leetcode.com/problems/n-queens/description/ 52 N-Queens II: ...

  6. PE文件格式分析

    PE文件格式分析 PE 的意思是 Portable Executable(可移植的执行体).它是 Win32环境自身所带的执行文件格式.它的一些特性继承自Unix的Coff(common object ...

  7. iOS 图片本地存储、本地获取、本地删除

    在iOS开发中.经常用到图片的本地化. iOS 图片本地存储.本地获取.本地删除,可以通过以下类方法实现. p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: ...

  8. POJ1006-Biorhythms

    Biorhythms Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 129706   Accepted: 41287 Des ...

  9. mysql实现full join

    呵呵,,,有个坑,,mysql默认不支持full join 是吧. 什么是full join呢就是left+right join  可以使用union联表解决这个问题 union 链接 http:// ...

  10. 自定义spring mvc的json视图

    场景 前端(安卓,Ios,web前端)和后端进行了数据的格式规范的讨论,确定了json的数据格式: { "code":"200", "data&quo ...