​这周敲完了《第一行代码》的最后一个项目酷欧天气,然后开始,第二本书《Android群英传》的敲代码之旅。虽然说项目驱动才是最好的学习方式,但照书敲然后思考,也不失为一种好办法。算是迈出Android进阶的第一步吧。

Android控件架构

​Android里的控件,大致被分为两类,ViewGroup 和 View

​从字面上来看,ViewGroup就是一组View的集合,可以通过它来管理控制其包含的View。通过它,界面上的所有控件也就能够形成一个树形结构,也就是常说的控件树,上层控件负责下层子控件的测量与绘制,并且传递交互事件。我们在Activity里经常使用的findViewById()方法,就是在控件树中通过DFS来查找的(数据结构还是很有用的)。而对于每一颗控件树,都有一个ViewParent对象,这就是整颗树的控制核心,所有的交互管理事件都由它来进行统一调度和分配。大概就是下面这张图的样子:

ViewTree

​还记得写Activity的时候都要用setContentView()方法来加载布局,之后才会显示其中的内容,那这个方法究竟干了什么?首先要看Android的界面架构图:

ActivityStructure

每个Activity对象都包含一个Window对象,一般由PhoneWindow来实现。PhonwWindow将DecorView设置为整个应用窗口的根View。DecorView封装了一些窗口操作的通用方法,DecorView将显示的具体内容呈现在了PhoneWindow上,其中所有View的监听事件都是通过WindowManagerService来接收,并且通过Activity对象来回调相应的onClickListener。

​ —— 《Android群英传》

其实这里最最简单的例子就是Button的点击监听,屏幕将点击事件传递给WindowManagerService,然后调用了我们在Button上写的setOnClickListener()方法来实现点击的逻辑。

这里书中提到了两个需要注意的点:

  1. requestWindowFeature()方法一定要在setContentView()方法之前调用,否则会无效(我就踩过这个坑),原因是 一般的Window布局由TitleView和ContentView构成,如果要设置一些特性(比如NO_TITLE),应该要在加载ContentView之前进行设置,否则,界面已经加载了TitleView,再调用方法也就会无效
  1. 程序在onCreate()方法中调用了setContentView()后,ActivityManagerService会回调onResume()方法,此时系统才会把整个DecorView添加到PhoneWindow中,并且让它显示出来。

View的绘制

​在自定义View之前,我们需要知道View是怎么绘制出来的。

​主要操作的有三个方法onMeasure()onDraw()onLayout()

​其实也很好理解,就和一个人要画画一样,他首先要知道画的东西的大小(onMeasure()负责测量大小),然后是画在画布的哪里(onLayout()指导他画在那里),再就是怎么画了(onDraw()方法来告诉它怎么画)。

onMeasure()方法

​Android系统为我们的测量提供了一个类MeasureSpec,通过它来帮助我们测量View。MeasureSpec是一个32位的int,其中高2位代表测量的模式,利用位运算可以提高效率。

测量的模式分为3种:

  1. EXACTLY 精确值模式,当使用固定数值或者指定为match_parent时使用

  2. AT_MOST 最大值模式,控件指定为warp_content时,随着控件大小变化而变化,不超过父控件允许的最大尺寸即可

  3. UNSPECIFIED 未明确模式,主要用在自定View的绘制中

    ​View类默认的onMeasure()方法只支持EXACTLY模式,这就意味着如果要使用其他模式就必须要重写该方法。如果要让自定义View支持warp_content属性,就要在onMeasure()方法中来指定warp_content时的大小。

    ​另外如果仔细研究onMeasure()方法,会发现系统最终调用setMeasuredDimension()方法来吧测量后的宽和高交给view。

    ​总而言之,我们通过MeasureSpec这一个类能够获取View的测量模式和绘制的大小,从而控制其显示时候的大小。

onLayout()方法

​控件的onLayout()方法一般都是通过实现其父控件(一般也就是一个ViewGroup)的onLayout()来实现的。

1
2
3
@Override
protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);

​在自定义View中,onLayout()配合onMeasure()方法一起使用,可以实现自定义View的复杂布局。自定义View首先调用onMeasure进行测量,然后调用onLayout方法,动态获取子View和子View的测量大小,然后进行layout布局。

onDraw()方法

​画画首先需要的材料就是画布和画笔。而在Android之中正好就有两个系统2D绘图API对象Canvas和Paint。

​Canvas就是我们的画布,不过我们在创建的时候,需要传入一个Bitmap对象

1
Canvas canvas = new Canvas(bitmap);

​传进去的Bitmap对象和Canvas画布是紧密相连的,这个过程我们称之为装载画布。Canvas.drawXXX方法都发生在这个bitmap上,bitmap储存所有绘制在Canvas上的像素信息。

bitmap承载Canvas的一系列绘图操作,图形的改变是通过Bitmap的改变,然后让View重新绘制来实现的

​Paint类就是一只画笔,接下来我们来结合一个自定义View的实例来看Paint和相关方法的使用。

自定义View实例 流光TextView

实现自定义View大致有三种方法:

  1. 对现有控件进行拓展
  2. 通过组合来实现新的控件
  3. 重写View来实现全新的控件

这里就拿书本上的一个简单的例子,使用第一种方法拓展TextView,实现一个

流光字的TextView

既然是拓展,第一反应就是继承TextView,然后重写相关的方法。没错,就是这么简单。

1
2
3
4
5
6
7
8
9
10
11
12
13

public class MyTextView extends android.support.v7.widget.AppCompatTextView {
 private Paint mPaint;
    private Matrix mGradientMatrix;
    private LinearGradient mLinearGradient;
    private int mTranslate = 0;
    private int mViewWidth;
   
     // 构造方法 
     public MyTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

在这个类里我们定义了一些成员变量,暂时先不管,然后写了一个构造方法,很简单,这就是一个简单的继承嘛。如果我们不去重写相关的方法,那么这个类和TextView其实是没有任何区别的。

要实现流光字,利用Paint对象的Shader渲染器。通过设置不断变化的LinearGradient(线性渐变),然后用带有该属性的Paint来绘制文字(就好比在画笔上沾了一些特殊的颜料,画出来自然是带特效的)。我们先要在onSizeChanged()方法中做一些初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if(mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if(mViewWidth > 0) {
                mPaint = getPaint(); // 拿到我们的画笔
                mLinearGradient = new LinearGradient(0, 0, mViewWidth,
                        0, new int[] {Color.BLUE, 0xffffffff,
                Color.BLUE}, // 构造一个LinearGradient 原生的TextView是没有这个属性的
                        null,
                        Shader.TileMode.CLAMP);
                mPaint.setShader(mLinearGradient);
                mGradientMatrix = new Matrix();
            }
        }
    }

然后,我们在onDraw()方法中不断用矩阵平移渐变效果,就可以实现流光字了。(和我以前在ps里面制作流光字的讨论差不多)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

  @Override
  public void onDraw(Canvas canvas) {
       // 代码如果写在这里,就是发生在文字绘制之前
      super.onDraw(canvas); // 绘制文字
// 代码写在这里,发生在文字绘制之后
      if(mGradientMatrix != null) {
          mTranslate += mViewWidth / 5;
          if(mTranslate > 2 * mViewWidth) {
              mTranslate = -mViewWidth; 
          }
          mGradientMatrix.setTranslate(mTranslate, 0); 
          mLinearGradient.setLocalMatrix(mGradientMatrix); // 设置位移矩阵
          postInvalidateDelayed(100); // 100ms之后刷新View
      }
  }

效果还是很不错哒,如下所示:

流光TextView效果图

初探旅程就到这,还有两种方法,等待探索。

Categories:

Updated: