自定义 View - 基础

先扯一点题外话,就现在的 Android 市场来说,可以说是不容乐观的,只不过是相对的,Android 的坑位有限,人又相对比较多,加上资本寒冬,像我一样的菜鸟是最为令人担忧的。那么能怎么办呢?只有进阶到高级才行,才能混的下去,高级 Android 工程师的市场还是很广阔的,所以一起努力吧,少年们!

想进阶到高级,自定义 View 这部分是必须要攻克的,这篇也算是开篇,主要翻译一下官方文档,在补一下基础部分,像坐标系,位置获取方式,颜色使用方式等,下面就动起来吧!

main

1. View 简介

1.1 View 介绍


public class View 
extends Object implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource

View 继承 Object,并实现了一些接口,现在对这些有一个印象就行,等具体分析源码时,再来看。

This class represents the basic building block for user interface components. A View occupies a rectangular area on the screen and is responsible for drawing and event handling. View is the base class for widgets, which are used to create interactive UI components (buttons, text fields, etc.). The ViewGroup subclass is the base class for layouts, which are invisible containers > that hold other Views (or other ViewGroups) and define their layout properties.

View 的定义:View 是用户交互组件的基本构建块。View 占据屏幕上的一个矩形区域,并且负责绘制和处理事件。View 是组件的基类,这些组件用来产生交互功能(如按钮。文本框等)。ViewGroup 是 View 的子类,它是用来管理布局的基类,它是不可见的,用来装载其他的子 View 或者其他的 ViewGroup,并且可以设置布局的属性。

通过这段话对 View 有了一个初步的认识,平时我们在 XML 中定义的布局就是一个 ViewGroup,界面的顶级 View,也是一个 ViewGroup(DecorView 是一个 FrameLayout)

Using Views

窗口中的所有视图都排列在 View 的树结构中,可以通过代码或通过在一个或多个XML布局文件中指定视图来添加 View 树结构中。 有许多专门的视图子类充当控件或能够显示文本,图像或其他内容。

Once you have created a tree of views, there are typically a few types of common operations you may wish to perform:

一旦创建了一个树结构,会有一系列的通用操作,主要包括以下几点:

  • Set properties(设置属性,如在给 TextView 设置文本)

  • Set focus(设置焦点)

  • Set up listeners(设置监听,如给 Button 设置 View.OnClickListener)

  • Set visibility(设置 View 是否可见 setVisibility(int))

Implementing a Custom View(自定义 View)

官方文档中这部分实际上就是给了一个大体的说明个,并不是详细的一个自定义 View 的教程。

To implement a custom view, you will usually begin by providing overrides for some of the standard methods that the framework calls on all views. You do not need to override all of these methods. In fact, you can start by just overriding onDraw(android.graphics.Canvas).

为了实现一个子自定义 View,你将要重写 View 所调用的框架中的标准方法,不需要全部重写,事实上,仅仅调用 onDraw 方法就行。也就是说,通过调用 onDraw 方法就可以完成简单的自定义 View,对于复杂的,需要我们重写 View 的比较重要的三个方法,即 onMeasure、onLayout、onDraw。

官方给出了一个表格,这个表格是对 View 中主要的方法进行了一个归类,可以作为自定义 View 操作的一个主要参考和指导。这里就不翻译了,相信自己可以看明白。

1.2 View 属性

这里的属性并不是 View 可以设置属性,而是官方文档介绍的 View 相关的一些东西,下面一起来看下。建议对这部分通读一下,对 View 会有一个整体的感知。

IDs

视图可能会有一个与它相关的 integer 类型的 id,即我们使用布局时定义的那个 id。这些 id 是在布局 XML 文件中设置的,通过这些 id 能够找到树结构中的 对应的 View,使用的方式也是我们很熟悉的:

<Button
android:id="@+id/my_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/my_button_text"/>

在布局文件中定义一个 Button,设置 id,然后就可以通过 id 获取这个 Button。


Button myButton = findViewById(R.id.my_button);

这些 id 并不一定是唯一的,但是在一个树结构中最好还是保持唯一性,因为当你找一个文件时,会很方便,否当你点击查找时,可能会弹出很多文件,导致不好筛选。

Position

一个 View 的几何形状是矩形的。一个 View 有位置表示,通常是通过左上形式的坐标系,也就是 X 轴右向是正的,Y 轴向下是正的,并且有两个尺寸,宽和高。位置信息和尺寸信息都是以像素为单位的。

获取位置信息是通过 getLeft() 和 getTop() 这两个方法。getLeft() 方法视图的 X 坐标,getTop() 方法返回的是 Y 坐标。这两个坐标返回的值都是相对一父布局的。例如,getLeft() 返回值是 20,意味着这个 view 位于它的直接父布局左边缘的右侧 20 个像素的位置。

另外,一些很方便的方法可以用来计算位置信息,可以避免自己计算的过程。getRight() 和 getBottom()。

getRight() = getLeft() + getWidth()。

getBottom() = getTop() + getHeight()。

这些在后面介绍坐标系时会详细介绍下。

Size, padding and margins

一个 view 的尺寸大小表达为宽和高,它通常拥有两对宽和高。

第一对:测量的宽和高,这些尺寸定义一个 view 在父布局中占据的位置有多大,通过调用 getMeasuredWidth() 和 getMeasuredHeight() 这两个方法可以获取。

第二对:就是简单宽和高,有时也叫绘制的宽和高。这些尺寸定义 view 早屏幕上实际的尺寸,时机是在绘制时,在 layout 之后。这两个尺寸和 测量的宽和高可能是一样的,通过调用 getWidth() 和 getHeight() 这两个方法获取。

一个 view 可以定义 padding,但是它不提供 margins。margins 是 ViewGroup 提供的布局限制的。

Layout

布局的过程包括两个:测量和布局。测量的过程是在 measure(int, int) 方法中实现的,并且是自上向下遍历树结构的过程。每个 view 都会把尺寸规则在这个递归过程中传递出去。在测量结束之后,每个 view 都会储存它的测量值。第二个过程发生在 layout(int, int, int, int) 这个方法中,并且它和测量过程类似。在这个过程中,每个父布局都会通过在测量过程中得到的尺寸来给子布局分配位置。

但完成测量过程时,即在 measure() 方法返回时,它的 getMeasuredWidth() 和 getMeasuredHeight() 一定是能够获取到值的,它的子布局也是一样。 一个子 view 的测量宽度和测量高度必须和父布局的限制保持一致,例如父布局设置了大小,子布局不能的大小不能超过父布局的大小。这可以保证在测量结束时,所有的父布局能够接受子布局的测量尺寸。一个父布局可能测量多次。例如父布局可能测量每个未指定尺寸的子布局,找到他们需要设置为多大,然后再次调用 measure() 方法来判断这些子布局的总大小是否过大还是过小。

测量过程中使用两个类来表述尺寸,View.MeasureSpec 用来告诉父布局如何测量和布局。LayoutParams 类用来描述 view 的宽和高有多大,对于每个 view,可以指定下面中一种规格:

(1)具体的数值

(2)MATCH_PARENT,表示 view 的大小和父布局大小相同(需要减去padding的大小)

(3)WRAP_CONTENT,表示刚好包含内容的大小(需要加上padding的大小)

不同的 ViewGroup 有不同的 LayoutParams 的子类。例如,AbsoluuteLayout 布局有自己的 LayoutParams,包含 X 和 Y 的值。

测量规格用来从父布局传到子布局当中,有下面三种规格:

(1)UNSPECIFIED: 用于父布局来决定子布局的尺寸的规格,例如,LinearLayout 可能调用 measure(),测量它的子布局,子布局的高度测量规格是 UNSPECIFIED,宽度是准确的值,为240,通过这两个规格来告诉子布局的宽度和高度,主要高度部分,父布局会计算好,帮助子布局确定尺寸。

(2)EXACTLY: 用于父布局来给子布局设置一个准确的尺寸值。子布局使用这个尺寸,并且保证所有的控件都在这个尺寸内。

(3)AT_MOST: 用于父布局给子布局设置一个最大的约束。子布局必须保证它和所有的控件都在这个尺寸范围内

To initiate a layout, call requestLayout(). This method is typically called by a view on itself when it believes that is can no longer fit within its current bounds.

为了初始化一个布局,可以通过调用 requestLayout() 方法。这个方法通常被一个 view 自己调用,调用的时机是它不在它当前的区域时,换句话说,也就是 view 的位置发生变化时,可以通过这个方法进行重新布局。

Drawing

绘制的过程,通过遍历树结构,并且记录需要更新的view的绘制命令来进行的,在这之后,整个树的绘图命令被发送到屏幕,剪切到新损坏的区域,意思是就是视图会被显示到屏幕上待显示的区域。

这个树结构记录并按顺序绘制,父布局先绘制,子布局后绘制,子控件绘制的顺序也是按照他们在树结构中出现的先后顺序来。如果为一个 view 设置了背景(Drawable),那么 这个 View 会先绘制背景,然后在绘制本身。子 view 的绘制顺序可以进行自定义,通过 setZ(float) 方法设置 Z 值。

强制一个 view 重绘,可以通过调用 invalidate() 方法。

Event Handling and Threading

一个 View 的基本循环如下:

一个事件传递过来,并分发给合适的 view,这个 view 处理这个事件并回调相应的监听方法。

在处理事件的过程当中,视图的区域(尺寸)可能发生改变,这时视图会调用 requestLayout() 这个方法。

类似的,如果在处理事件的过程中,视图的外观发生改变,这时会调用 invalidate() 方法。

无论是 requestLayout() 还是 invalidate() 被调用,系统框架会合适的处理视图结构树的测量,布局,绘制。

注意:整个树结构是单线程的,当有方法作用在 view 上时,必须在 UI 线程(主线程)上进行。如果想在其他线程工作并且其他线程想要更新视图的状态,可以使使用 Handler。

Focus Handling

系统框架将会处理用户输入时的焦点。包括当 view 被移除或者隐藏时,一个新的 view 变得可见时。通过 isFocusable() 方法可以知道视图是否处于获取焦点状态。为了改变一个 view 能够获取焦点,通过调用 setFocusable(boolean)方法。当处于触摸状态的view,可以通过调用 isFocusableInTouchMode() 方法判断是否处于获取焦点状态,可以通过调用 setFocusableInTouchMode(boolean) 方法来改变焦点状态。

拥有焦点的时刻是通过一个算法,这个算法来找到最近的一个在指定方向的控件。在一些情况下,默认的算法并不能够匹配开发者的需求。在这种情况下,可以提供一个准确的复写来完成,在XML 文件中定义属性。包括以下几个属性:

  • nextFocusDown
  • nextFocusLeft
  • nextFocusRight
  • nextFocusUp

当需要使一个 view 获取焦点是,可以通过调用 requestFocus() 方法。

Touch Mode

当一个用户控制一个交互动作时,通过像 D-pad 这样的方向键,有需要给定这些控件以焦点,如 Button 按钮,所用用户能够看到什么可以输入。如果设备有触摸功能,用户可以通过触摸开始进行交互操作,就没有必要时刻保持高亮,或者给指定的 view 保持焦点,这种方式就是所谓的 触摸模式。

对于一个触摸设备,一旦用户触摸屏幕,设备将进入触摸状态,从这个时刻起,只有 哪些处于 isFocusableInTouchMode() 方法返回 true的 view才是获取焦点状态的,如文本编辑框。其他的 view 可以触摸,如 button,但是不占有焦点,它们只会触发监听事件。

任意时间,用户点击了方向键,如 D-pad,这个设备键退出触摸模式,并找到一个 view,并会获取焦点,所以用户能够再次和用户交互入口进行交互交互动作,不需要再次触摸屏幕。

触摸状态由 Activity 来维护,通过调用 isInTouchMode() 方法来判断设备当前是否处于触摸状态。

Scrolling

框架为 view 提供基本的滑动操作,可以滑动内部的内容。包括记录 X 和 Y 向 的滑动,可以查看 scrollBy(int, int), scr

Tags

不像 ID, tags 不是用于识别 view 的,tags 用于标记和这个 view 相关的额外信息的。常用于存储和这个view相关的信息,它比使用单独的结构来存储更加方便。

tags 可以在 XML 文件中使用字符序列定义一个单独的 tag,标签是 android:tag,或者使用多个标签,以 作为子元素。

<View ...
android:tag="@string/mytag_value" />
<View ...>
<tag android:id="@+id/mytag"
android:value="@string/mytag_value" />
</View>

Tags 也可以使用代码来实现,获取 view 之后,使用 setTag(Object) 或 setTag(int, Object) 方法。

Themes

默认情况下, view 使用 Context 提供的主题,然而,如果想使用一个不同的主题,可以在 XML 文件中 通过 android:theme 属性来定义或者通过在代码中传递一个 ContextThemeWrapper 给构造函数。

当 android:theme 属性使用在 XML 中时,指定的主题应用在 context 的上层中,同时 view 的子元素也使用这个主题。

在下面的这个例子中,LinearLayout 内部的子 view 都使用 Material dark 这个主题,然而,由于是在 LinearLayout 内部定义的主题,相当于叠加在上层的主题, android:colorAccent 的值,它是在上层主题中定义的,所以也会出现。

<LinearLayout
...
android:theme="@android:theme/ThemeOverlay.Material.Dark">
<View ...>
</LinearLayout>

Properties

View 类暴露一个 ALPHA 属性(透明度属性),同时也有一些平移旋转属性,如 TRANSLATION_X, TRANSLATION_Y。这些属性在类中都有 setter/getter 方法可以设置。这些属性可以设置和 view 渲染属性相关的状态。这些属性也可以用于动画相关的设置,可以在动画部分查看详细信息。

Animation

从 Android 3.0开始,给 view 添加动画的最好方式就是使用 android.animation 包下的 APIs.这些动画基类改变 view 的实际属性,如 透明度和 X轴 平移量。和 3.0之前的动画基类相比,仅仅是改变 view 在屏幕上的显示效果,并没有改变 view 的属性。特别的,通过 ViewPropertyAnimator 类使得这些 view 的属性使用起来更加简单和有效。

相对的,你可以使用 3.0之前的动画类来进行 view 的渲染。可以将一个动画附着到 view 上,使用 setAnimation(Animation) 或者 startAnimation(Animation) 方法。这个动画可以缩放,旋转,平移和透明度,并能够随时间改变 view 属性。如果这个动画被附着到一个 view 上,并且 view 上包含子类,这个动画会影响这个节点之下的所有子类。当开启动画之后,框架会开始重绘这个 view,直到 动画结束

Security

一个应用有必要确认一个动作,告知用户用户的内容,如保证权限请求,点击广告的操作等等。不幸的是,有很多应用尝试引导用户完成这些一些动作,并且在没有意识的情况下,通过在这个 view 下隐藏目的。作为补救措施,框架提供一些触摸的过滤机制,来提高 view 的安全性,来保证一些敏感的访问操作。

为了能够进行触摸的过滤操作,通过调用 setFilterTouchesWhenObscured(boolean) 方法,或者设置 android:filterTouchesWhenObscured 属性为 true,当可以进行过滤时,框架将会摒弃触摸操作,当view 的窗口被另外一个 窗口占据时。这样,view 将不会接受触接收触摸操作,当一个 toast ,dialog,或者其他的窗口出想在这个 view 的窗口的上面时。

为了更好地控制安全性的问题,考虑重写 onFilterTouchEventForSecurity(MotionEvent) 方法来实现你的安全策略,可以查看MotionEvent.FLAG_WINDOW_IS_OBSCURED 这个属性。

这个方法必须在开始创建这个 UI 元素的线程中调用,通常就是应用的主线程。

2. View 分类

上面把官方对 view 的描述做了一遍翻译,对 view 有了一个全面的了解,下面看下 view 的分类。

classes

上面一张图,就是 view 的树形结构,可以有很多层,然后通过一层一层遍历,最后绘制出来。当然,嵌套层次过多时,就会出现过度绘制,会变的卡顿,所以在设计布局时,避免多层嵌套使用。 view 的三个主要过程,measure、layout、draw 都是通过这个树结构从根节点一层一层递归遍历完成的。另外,根布局就是 DecorView,是 FrameLayout。

3. View 的位置描述

3.1 坐标系

屏幕坐标系和数学坐标系的区别:由于移动设备一般定义屏幕左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向, 所以在手机屏幕上的坐标系与数学中常见的坐标系是稍微有点差别的,详情如下:

cordinate

实际屏幕上的默认坐标系如下:

PS: 假设其中棕色部分为手机屏幕

view_phone

3.2 位置获取

(1)4个顶点的位置描述分别由4个值决定: (请记住:View的位置是相对于父控件而言的)

position

Top:子View上边界到父view上边界的距离

Left:子View左边界到父view左边界的距离

Bottom:子View下边距到父View上边界的距离

Right:子View右边界到父view左边界的距离

(2)获取4个顶点可以通过一下4个函数:

getTop();       //获取子View左上角距父View顶部的距离

getLeft();      //获取子View左上角距父View左侧的距离

getBottom();    //获取子View右下角距父View顶部的距离

getRight();     //获取子View右下角距父View左侧的距离

(3) MotionEvent中 getXX 和 getRaw 的区别:

event.getX();       //触摸点相对于其所在组件坐标系的坐标
event.getY();

event.getRawX();    //触摸点相对于屏幕默认坐标系的坐标
event.getRawY();

different

3.3 Android 中的角度与弧度

为了精确描述一个角的大小引入了角度与弧度的概念。

名称 定义
角度 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。当这段弧长正好等于圆周长的360分之一时,两条射线的夹角的大小为1度.
弧度 两条射线从圆心向圆周射出,形成一个夹角和夹角正对的一段弧。当这段弧长正好等于圆的半径时,两条射线的夹角大小为1弧度.

如图:

转换公式:

rad 是弧度, deg 是角度

公式 例子
rad = deg x π / 180 2π = 360 x π / 180
deg = rad x 180 / π 360 = 2π x 180 / π

维基百科的公式:

rad 是弧度, deg 是角度

在常见的数学坐标系中角度增大方向为逆时针,

在默认的屏幕坐标系中角度增大方向为顺时针。

4 Android 中 color

4.1 简介

安卓支持的颜色模式:

颜色模式 备注
ARGB8888 四通道高精度(32位)
ARGB4444 四通道低精度(16位)
RGB565 屏幕默认模式(16位)
Alpha8 仅有透明通道(8位)

PS:其中字母表示通道类型,数值表示该类型用多少位二进制来描述。如ARGB8888则表示有四个通道(ARGB),每个对应的通道均用8位来描述。

注意:我们常用的是ARGB8888和ARGB4444,而在所有的安卓设备屏幕上默认的模式都是RGB565,请留意这一点。

以ARGB8888为例介绍颜色定义:

类型 解释 0(0x00) 255(0xff)
A(Alpha) 透明度 透明 不透明
R(Red) 红色 无色 红色
G(Green) 绿色 无色 绿色
B(Blue) 蓝色 无色 蓝色

其中 A R G B 的取值范围均为0~255(即16进制的0x00~0xff)

A 从0x00到0xff表示从透明到不透明。

RGB 从0x00到0xff表示颜色从浅到深。

当RGB全取最小值(0或0x000000)时颜色为黑色,全取最大值(255或0xffffff)时颜色为白色

4.2 几种创建或使用颜色的方式

(1)java中定义颜色

int color = Color.GRAY;     //灰色

由于Color类提供的颜色仅为有限的几个,通常还是用ARGB值进行表示。

int color = Color.argb(127, 255, 0, 0);   //半透明红色

int color = 0xaaff0000;                   //带有透明度的红色

(2)在xml文件中定义颜色

在/res/values/color.xml 文件中如下定义:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="red">#ff0000</color>
<color name="green">#00ff00</color>
</resources>

详解: 在以上xml文件中定义了两个颜色,红色和蓝色,是没有alpha(透明)通道的。

定义颜色以‘#’开头,后面跟十六进制的值,有如下几种定义方式:

#f00            //低精度 - 不带透明通道红色
#af00           //低精度 - 带透明通道红色

#ff0000         //高精度 - 不带透明通道红色
#aaff0000       //高精度 - 带透明通道红色

(3)在java文件中引用xml中定义的颜色:

int color = getResources().getColor(R.color.mycolor);

int color = getColor(R.color.myColor);    //API 23 及以上支持该方法

(4)在xml文件(layout或style)中引用或者创建颜色

<!--在style文件中引用-->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/red</item>
</style>
android:background="@color/red"     //引用在/res/values/color.xml 中定义的颜色

android:background="#ff0000"        //创建并使用颜色

4.3 颜色拾取工具

颜色都是用RGB值定义的,而我们一般是无法直观的知道自己需要颜色的值,需要借用取色工具直接从图片或者其他地方获取颜色的RGB值。

(1)屏幕取色工具

取色调色工具,可以从屏幕取色或者使用调色板调制颜色,非常小而精简。

点击这里获取屏幕取色工具

(2)Picpick

功能更加强大的工具:PicPick。

PicPick具备了截取全屏、活动窗口、指定区域、固定区域、手绘区域功能,支持滚动截屏,屏幕取色,支持双显示器,具备白板、屏幕标尺、直角座标或极座标显示与测量,具备强大的图像编辑和标注功能。

点击这里获取PicPick

4.4 颜色混合模式(Alpha通道相关)

通过前面介绍我们知道颜色一般都是四个通道(ARGB)的,其中(RGB)控制的是颜色,而A(Alpha)控制的是透明度。

因为我们的显示屏是没法透明的,因此最终显示在屏幕上的颜色里可以认为没有Alpha通道。Alpha通道主要在两个图像混合的时候生效。

默认情况下,当一个颜色绘制到Canvas上时的混合模式是这样计算的:

(RGB通道) 最终颜色 = 绘制的颜色 + (1 - 绘制颜色的透明度) × Canvas上的原有颜色。

注意:

1.这里我们一般把每个通道的取值从0(0x00)到255(0xff)映射到0到1的浮点数表示。

2.这里等式右边的“绘制的颜色”、“Canvas上的原有颜色”都是经过预乘了自己的Alpha通道的值。如绘制颜色:0x88ffffff,那么参与运算时的每个颜色通道的值不是1.0,而是(1.0 * 0.5333 = 0.5333)。 (其中0.5333 = 0x88/0xff)

使用这种方式的混合,就会造成后绘制的内容以半透明的方式叠在上面的视觉效果。

其实还可以有不同的混合模式供我们选择,用Paint.setXfermode,指定不同的PorterDuff.Mode。

下表是各个PorterDuff模式的混合计算公式:(D指原本在Canvas上的内容dst,S指绘制输入的内容src,a指alpha通道,c指RGB各个通道)

混合模式 计算公式
ADD Saturate(S + D)
CLEAR [0, 0]
DARKEN [Sa + Da - SaDa, Sc(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)]
DST [Da, Dc]
DST_ATOP [Sa, Sa * Dc + Sc * (1 - Da)]
DST_IN [Sa * Da, Sa * Dc]
DST_OUT [Da * (1 - Sa), Dc * (1 - Sa)]
DST_OVER [Sa + (1 - Sa)Da, Rc = Dc + (1 - Da)Sc]
LIGHTEN [Sa + Da - SaDa, Sc(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)]
MULTIPLY [Sa * Da, Sc * Dc]
SCREEN [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc]
SRC [Sa, Sc]
SRC_ATOP [Da, Sc * Da + (1 - Sa) * Dc]
SRC_IN [Sa * Da, Sc * Da]
SRC_OUT [Sa * (1 - Da), Sc * (1 - Da)]
SRC_OVER [Sa + (1 - Sa)Da, Rc = Sc + (1 - Sa)Dc]
XOR [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc]

用示例图来查看使用不同模式时的混合效果如下(src表示输入的图,dst表示原Canvas上的内容):

4.4 颜色相关其他 (也比较重要,所以拷贝过来,为后面使用 Pain 做铺垫)

(1) 填充颜色

之前说过Canvas.draw*指定了绘制的区域。而区域里的填充颜色是由Paint来指定的。

Paint.setColor指定纯色。

Paint.setShader可指定:BitmapShader, LinearGradient, RadialGradient, SweepGradient, ComposeShader。

BitmapShader:图片填充。

LinearGradient, RadialGradient, SweepGradient:渐变填充。

ComposeShader:叠加前面的某两种。可选择PorterDuff混合模式。

如果既调用了setColor,又调用了setShader,则setShader生效。如果同时用setColor或setAlpha设置了透明度,则透明度也会生效。(会和Shader的透明度叠加)

如果使用drawBitmap输入一个只有alpha的图片(可用Bitmap.extractAlpha方法获得),则会以alpha图片为mask,绘制出shader/color的颜色。

(2)ColorFilter

通过ColorFilter可以对一次绘制的所有像素做一个通用处理。

Paint.setColorFilter: LightingColorFilter, PorterDuffColorFilter, ColorMatrixColorFilter。

这可以整体上改变这一次draw的内容,比如让颜色更暗、更亮等。

这里重点介绍下ColorMatrixColorFilter。

ColorMatrix是4x5矩阵,定义其每个元素如下:

{ a, b, c, d, e,

f, g, h, i, j,

k, l, m, n, o,

p, q, r, s, t }

则ColorMatrix的最终运算方式如下:

R’ = aR + bG + cB + dA + e;

G’ = fR + gG + hB + iA + j;

B’ = kR + lG + mB + nA + o;

A’ = pR + qG + rB + sA + t;

(3)绘图API架构

整个绘制流水线大概如下:(我们能定义的部分用蓝色表示)

draw_api

考虑动画实现的时候一般从两个角度来思考:

宏观角度:有几个变化量,分别是什么。动画从开始到结束的流程。

微观角度:从某一帧上去想,在变化量为某个数值时的图像,该怎么绘制。

把这两者分开去想,就会比较清晰。

参考

AndroidNote

自定义View基础 - 最易懂的自定义View原理系列(1)

View

ViewGroup

【安卓图形动画】

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦