Creating custom and compound Views in Android - Tutorial(通译)

Creating custom and compound Views in Android - Tutorial(翻译)

Creating custom and compound Views in Android - Tutorial(翻译)

译前的:
之前做了三篇学习笔记,从知乎上面看到了这篇英文的推荐,总的来说可以是一篇导读,没有相关的学习,看这篇,可以作为一个学习脉络导向;有相关的学习底子,可以作为一个基础夯实、思维理清。没想到一翻译就是四个多小时…英语渣,很多词句都不太准确,幸好有之前的学习基础打底…

1. Custom Views 自定义视图

1.1 Default views 默认视图

Android框架提供了多种默认视图,但是开发者们也可以在他们的应用中创造、使用他们自己定义的视图。视图的基类(父类)是View
View的子类有三大块——ImageViewTextViewViewGroup

1.2Android在视图层次结构上的绘制

一旦一个Activity获得焦点,它必然会向Android系统提供它布局的根结点,之后Android系统会开始绘制步骤。
绘制(Drawing)是从布局的根结点开始的,布局层次的绘制顺序为声明的顺序,例如,父view的绘制先于它的子view,而子view的绘制顺序也是按照声明的顺序。
绘制布局(layout)有两个阶段:

  • 计算阶段(measuring)——实现了measrue(int,int)方法并且是在控件层次中自上而下的遍历顺序(and is a top-down traversal of the view hierarchy)。每个视图存储它自己的尺寸数据。
  • 布局阶段(layout)——实现了layout(int,int,int,int)方法,也同样是自上而下的顺序。在这个阶段,每个布局管理器通过在上一阶段计算好的尺寸数据,来安置他们所有的子view的位置。

note:
计算阶段和布局阶段通常都是一起发生的。
布局管理器可以多次调用计算阶段进行多次计算。例如LinearLayout支持weight属性(根据权重,分配view之间的空余空间),像RelativeLayout多次计算子view来解决布局文件中设置的约束。

一个view或者activity的计算阶段和布局阶段可以通过调用requestLayout()被重新触发。
在计算和布局阶段之后,view们都会自己绘制自己。这个操作可以被定义在View类中的invalidate()方法触发(译者注:invalidate()方法适用于ui线程中,而在非ui线程中,要重新调用onDraw()方法,只能用postInvalidate()方法)。
更详细深入的说明,参见Android Graphical Architecture。

1.3 创造view的理由

视图是典型的用于提供用户体验的组件,不可能总是使用默认提供的视图。自定义view的使用允许开发者展现最优的展示效果。例如,自定义布局可以让开发者在他的特定需求下作出最优的布局管理。

1.4 view的职责

view需要测量尺寸、对子view的位置进行布局、绘制自身及其子view(例如ViewGroup),除此之外,还需要保存ui的状态和负责触摸事件的处理。

1.5 自定义view的步骤

自定义视图是典型的复合视图或者自己定义的视图。一般自定义视图的方式为:
1. view的组合
2. 自定义视图

  • 扩展现有的view
  • 扩展父类View

1.6 在布局文件中使用新的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" >

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button" />

    <de.vogella.android.ownview.MyDrawView
        android:id="@+id/myDrawView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

小技巧:
和android命名空间类似,你也可以再布局文件中声明你自己的命名空间。

1.7 创建screenshots (Images) of views

每个View类都支持创建一个内容为当前显示的图片对象。下面的代码就是例子。

# Build the Drawing Cache
view.buildDrawingCache();

# Create Bitmap
Bitmap cache = view.getDrawingCache();

# Save Bitmap
saveBitmap(cache);
view.destroyDrawingCache();

2 复合视图

复合视图(也叫组合控件)是指基于现有的view和预先定义好的view在ViewGroup中进行定义。
复合视图也允许你增加自定义的api和查询状态。
为了实现这样的控制,你需要定义一个布局文件并将其和复合视图进行联系。复合视图的实现中,你要预先定义好视图之间的交互。首先定义一个布局文件,扩展相应的ViewGroup的子类,在这个子类的实现中,你需要将其和之前的布局文件联系起来,再实现相关的视图逻辑。

小技巧:
由于显示的原因,你或许想将你的复合视图重写为扩展了View的自定义视图,这或许能够有效的降低你的视图的布局层次。这样情况下如果你正确的实现了,视图层次的遍历会更少,绘制视图会变得更快。

3 创建自定义视图

3.1 创建自定义视图

通过继承View类或者它的子类,你可以实现自己的view。
绘制view是通过使用onDraw()方法,在这个方法中,你可以获得一个可以进行绘制操作的Canvas类,例如绘制直线、圆形、文本或者位图。如果view需要被重绘,你应该调用invalidate()方法。

小技巧:
如果你定义你自己的视图,在定义之前看一下ViewConfiguration这个类,它里面包含着数个定义视图的常量。

绘制视图通常会用到2D Canvas API

3.2 测量(Measurement)

布局管理器调用onMeasure()方法,视图从布局管理器中获取到布局属性参数。一个布局管理器负责确定它所有子view的尺寸。
视图必须调用setMeasureDimenstion(int,int)方法来设置最后的结果。

3.3 定义自定义布局管理(Layout managers)

你可以实现通过继承ViewGroup类来实现自定义的布局管理。这使得你能够实现更加有效的布局管理或者实现当前安卓平台上缺少的一些布局。
一个自定义布局管理需要重写onMeasrue()onLayout方法,指定其子view的计算策略。举例来说,it can leave out the time consuming support of layout_weight of the LinearLayout class.(这句话没看懂什么意思…)
通过重写ViewGroup学习onMeasure()和onLayout()方法

小技巧:
可以使用ViewGroup类中的measureChildWithMargins()方法来计算子view的尺寸。

在继承了ViewGroup类的自定义类中用内部类存储额外的布局参数,是一种很好的方式。例如ViewGroup.LayoutParams的实现了一些通常的布局参数,而LinearLayout.LayoutParams则是内含一些特定使用在该布局上的参数,像layout_weight这个属性。

4 生命周期

4.1 和窗口有关的生命周期事件

视图显示,那么该视图应当是包含于于一个布局层次之中,而该布局应当是隶属于当前的窗口的。view是拥有多个和生命周期有关的钩子函数。一旦当前窗口可被获得,方法onAttachedToWindow()就会被调用。
在view从其父view中被移除(前提是这个父view隶属于一个窗口),方法onDetachedFromWindow()会被调用。通常发生于这样的情景:如果一个activity被回收(例如通过调用finish()方法)或者一个在ListView中的view被回收,方法onDetachedFromWindow()可以用来停止动画、清理被view使用的资源。

4.2 遍历生命周期事件

这些事件包括 动画(Animate)、测量(Measure)、布局(Layout)和绘制(Draw)。
所有的view都必须了解如何测量和布局自己。view可以通过调用requestLayout()方法来使得父view重新测量、布局自己(即父view需要重新调用父view的onMeasure()onLayout方法)。这个操作获取也会导致布局中的其他view同样调用requestLayout()向他们的父view要求重新测量和布局。

小技巧:
这样的递归调用也是为什么不建议你嵌套多层布局,如果很多层次的都需要重新计算,测量和布局方法调用代价将很高。

方法onMeasure()用于定义自身及其子view的尺寸。在函数接收之前,必须通过setMeasuredDimension()这个方法来设置尺寸。
方法onLayout()基于方法调用onMeasure()的结果来放置子view。这种调用通常只发生一次。 This call happens typically once, which onMeasure() can happens once.(翻译感觉有问题…)

4.3 Activity的生命周期

view没有和activity的声明周期事件相联系的途径。如果view希望获知这些事件的信息,你应该在相关的view中声明一个接口,并且在activity中相应的事件中调用接口。

5 为你的自定义view定义额外的属性

你可以为你的复合视图或者自定义视图定义额外的属性。在res/values路径下,新建名为attrs.xml的属性文件。下面的代码示例如何定义一个名为ColorOptionView的自定义视图。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ColorOptionsView">
        <attr name="titleText" format="string" localization="suggested" />
        <attr name="valueColor" format="color" />
    </declare-styleable>

</resources> 

为了能够在你的布局文件中使用你自定义出来的属性,你需要在xml文件首部去定义他们。下面的代码通过xmlns:custom来声明命名空间。这些属性也被分配给了视图。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
<!-- define new name space for your attributes -->
    xmlns:custom="http://schemas.android.com/apk/res/com.vogella.android.view.compoundview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
>
<!-- Assume that this is your new component. It uses your new attributes -->
        <com.vogella.android.view.compoundview.ColorOptionsView
            android:layout_width="match_parent"
            android:layout_height="?android:attr/listPreferredItemHeight"
            custom:titleText="Background color"
            custom:valueColor="@android:color/holo_green_light"
             />

</LinearLayout> 

而下面的代码演示如何在代码中获取这些属性。

package com.vogella.android.view.compoundview;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

public class ColorOptionsView extends View {

  private View mValue;
  private ImageView mImage;

  public ColorOptionsView(Context context, AttributeSet attrs) {
    super(context, attrs);

    TypedArray a = context.obtainStyledAttributes(attrs,
        R.styleable.Options, 0, 0);
    String titleText = a.getString(R.styleable.Options_titleText);
    int valueColor = a.getColor(R.styleable.Options_valueColor,
        android.R.color.holo_blue_light);
    a.recycle();

    // more stuff
  }
} 

6 练习

这个就不翻译了…

7 Canvas API

7.1 综述

canves API允许绘制多种几何效果。
需要在位图上进行绘制。Canvas类提供了多种在位图上的绘图方法,并且搭配Paint类,可以有多种不同的风格样式。

7.2 Canvas 类

Canvas对象包含了作为画布的位图。同样也提供了绘制的方法。例如drawARGB()方法来画不同的颜色(好别扭)、drawBitmap()方法来绘制位图、drawText()方法来绘制文本,drawRoundRect()来绘制圆角矩形等等等等。

7.3 Paint 类

使用Paint类来在Canvas上绘图。
使用Paint类来指定颜色、字体和特定效果的绘图操作(我想这里指的应该是不同的style)。
方法setStyle()来指定是否仅仅绘制外框线(Paint.Style.STROKE)、填充(Paint.Style.FILL)和二者都有的样式(Paint.Style.STROKE_AND_FILL)。
使用Paint类的setAlpha()来设置透明度。
通过渲染器(Shader)类,可以是的Patin用多种颜色进行填充。

7.4 Shader 类

渲染器用来为Paint对象定义将要被绘制的内容,例如你可以使用BitmapShader类来定义要被渲染位图。举例来讲,你可以用来绘制圆形的图片。简单为你的Paint对象定义一个BitmapShader并且使用drawRoundRect()方法来画出圆角矩形的图片。
其他的由android平台提供的渲染器是LinearGradientRadiaGradientSweepGradient,用于绘制渐变色。
使用一个Shader并通过setShader()方法将其赋予给你的Paint对象。
通过Shader类的tile mode来定义当要渲染区域大于渲染器中原本的位图或者内容区域时,这些多余的区域的填充类型。常量Shader.TileMode.CLAMP定义使用渲染元素的边界部分来填充额外的区域。常量Shader.TileMode.MIRROR定义使用镜像的方式来填充而Shader.TileMode.REPEAT定义单纯使用重复的内容进行多余部分的填充。

8 持有视图数据

大多数标准视图能够保存他们的状态,因此可以被系统持有。android系统调用onSaveInstanceState()onRestoreInstanceState(Parcable)方法来保存和获取视图状态。
约定俗成的是使用View.BasedSaveStage的派生类作为view中的静态内部类来保存数据。
android系统基于ID搜索布局中的视图,传递存有它状态的Bundle对象给view。
你应当在使用离开的时候保存和复位用户界面的状态。

9 相关链接

  • 原文链接
  • 之前的学习笔记

Android自定义view学习笔记01
Android自定义view学习笔记02
Android自定义view学习笔记03