乙巳🐍年

acc8226 的博客

几组概念

分辨率
屏幕上物理像素的总数。添加对多种屏幕的支持时, 应用不会直接使用分辨率;而只应关注通用尺寸和密度组指定的屏幕尺寸及密度。

屏幕尺寸: 按屏幕对角测量的实际物理尺寸。目前市面上说的几英寸是对角线的英寸数
为简便起见,Android 将所有实际屏幕尺寸分组为四种通用尺寸:小、 正常、大和超大。(太宽泛了, 现在已不建议使用)

DPI(Dots Per Inch):每英寸点数,表示指屏幕密度。是测量空间点密度的单位,最初应用于打印技术中,它表示每英寸能打印上的墨滴数量。较小的 DPI 会产生不清晰的图片。
后来 DPI 的概念也被应用到了计算机屏幕上,计算机屏幕一般采用 PPI(Pixels Per Inch)来表示一英寸屏幕上显示的像素点的数量,现在 DPI 也被引入。

为简便起见,Android 将所有屏幕密度分组为六种通用密度

屏幕像素密度 ldpi mdpi hdpi xhdpi xxhdpi xxxhdpi
描述 低密度屏幕 中等密度 高密度屏幕 超高密度屏幕 - -
约为 ~120dpi ~160dpi ~240dpi ~320dpi ~480dpi ~640dpi
之间的缩放比 3 4 6 8 12 16
  0.75x 1.0x 1.5x 2.0x 3.0x 4.0x

PPI(Pixels Per Inch):图像分辨率;是每英寸图像内有多少个像素点,分辨率的单位为 ppi,通常叫做像素每英寸。图像分辨率一般被用于 ps 中,用来改变图像的清晰度。

密度无关像素 (dp)
在定义 UI 布局时应使用的虚拟像素单位,用于以密度无关方式表示布局维度或位置。
密度无关像素等于 160 dpi 屏幕上的一个物理像素,这是 系统为“中”密度屏幕假设的基线密度。在运行时,系统 根据使用中屏幕的实际密度按需要以透明方式处理 dp 单位的任何缩放 。dp 单位转换为屏幕像素很简单: px = dp * (dpi / 160)
例如,在 240 dpi 屏幕上,1 dp 等于 1.5 物理像素。在定义应用的 UI 时应始终使用 dp 单位 ,以确保在不同密度的屏幕上正常显示 UI。

支持每种密度的 位图可绘制对象的相对大小

适配方案

密度独立性

应用显示在密度不同的屏幕上时,如果它保持用户界面元素的物理尺寸(从 用户的视角),便可实现“密度独立性” 。
Android 系统可帮助您的应用以两种方式实现密度独立性:

  • 系统根据当前屏幕密度扩展 dp 单位数
  • 系统在必要时可根据当前屏幕密度将可绘制对象资源扩展到适当的大小
    • nodpi:它可用于您不希望缩放以匹配设备密度的位图资源。例如.9图推荐放在此目录
    • anydpi:此限定符适合所有屏幕密度,其优先级高于其他限定符。 这对于矢量可绘制对象很有用。 此项为 API 级别 21 中新增配置

最佳做法

  • 使用新尺寸限定符
    smallestWidth (sw<N>dp)

屏幕的基本尺寸,由可用屏幕区域的最小尺寸指定。 具体来说,设备的 smallestWidth 是屏幕可用高度和宽度的最小尺寸(您也可以将其视为屏幕的“最小可能宽度”)。无论屏幕的当前方向如何,您均可使用此限定符确保应用 UI 的可用宽度至少为 <N>dp

例如,如果布局要求屏幕区域的最小尺寸始终至少为 600 dp,则可使用此限定符创建布局资源 res/layout-sw600dp/。仅当可用屏幕的最小尺寸至少为 600dp 时,系统才会使用这些资源,而不考虑 600dp 所代表的边是用户所认为的高度还是宽度。smallestWidth 是设备的固定屏幕尺寸特性;设备的 smallestWidth 不会随屏幕方向的变化而改变

设备的 smallestWidth 将屏幕装饰元素和系统 UI 考虑在内。例如,如果设备的屏幕上有一些永久性 UI 元素占据沿 smallestWidth 轴的空间,则系统会声明 smallestWidth 小于实际屏幕尺寸,因为这些屏幕像素不适用于您的 UI。

这可替代通用化的屏幕尺寸限定符(小、正常、大、超大), 可让您为 UI 可用的有效尺寸定义不连续的数值。 使用 smallestWidth 定义一般屏幕尺寸很有用,因为宽度 通常是设计布局时的驱动因素。UI 经常会垂直滚动,但 对其水平需要的最小空间具有非常硬性的限制。可用的宽度也是 确定是否对手机使用单窗格布局或是对平板电脑使用多窗格布局的关键因素。因此,您可能最关注每部 设备上的最小可能宽度。
最小宽度限定符可让您通过指定某个最小宽度(以 dp 为单位)来定位屏幕。例如,标准 7 英寸平板电脑的最小宽度为 600 dp,因此如果您要在此类屏幕上的用户界面中使用双面板(但在较小的屏幕上只显示列表),您可以使用上文中所述的单面板和双面板这两种布局,但您应使用 sw600dp 指明双面板布局仅适用于最小宽度为 600 dp 的屏幕,而不是使用 large 尺寸限定符。

  • 在 XML 布局文件中指定尺寸时使用 wrap_content、match_parent 或 dp 单位 。
  • 不要在应用代码中使用硬编码的像素值
  • 不要使用 AbsoluteLayout(已弃用), 而是考虑线性布局使用权重分配宽高, support库中约束布局, 可以是布局更加扁平化
  • 为不同屏幕密度提供替代位图可绘制对象

图标的适配

在进行开发的时候,我们需要把合适大小的图片放在合适的文件夹里面。下面以图标设计为例进行介绍。

在设计图标时,对于五种主流的像素密度(MDPI、HDPI、XHDPI、XXHDPI 和XXXHDPI)应按照 2:3:4:6:8 的比例进行缩放。
虽然 Android 也支持低像素密度 (LDPI) 的屏幕,但无需为此费神,系统会自动将 HDPI 尺寸的图标缩小到 1/2 进行匹配。
建议以高分辨率作为设计大小,然后按照倍数对应缩小到小分辨率的图片。
一般情况下,我们只需要提供3套切图资源就可以满足安卓工程师的适配,分别是 HDPI、XHDPI、 XXHDPI 3套切图资源。
推荐使用的办法就是只提供最大尺寸的切图,xxhdpi 的高清图, 然后可以交给安卓工程师自己去缩放适配其他分辨率。

图标的各个屏幕密度的对应尺寸

.9图自动拉伸

ImageView的ScaleType 属性

设置 不同的 ScaleType 会得到不同的显示效果,一般情况下,设置为 centerCrop 能获得较好的适配效果。fixXY 可能导致变形.

动态设置

  • 有一些情况下,我们需要动态的设置控件大小或者是位置,比如说 popwindow 的显示位置和偏移量等,这个时候我们可以动态的获取当前的屏幕属性,然后设置合适的数值
  • 使用官方百分比布局
1
2
3
dependencies{
compile'com.android.support:percent:25.1.0'
}

使用布局别名

最小宽度限定符仅适用于 Android 3.2 及更高版本。因此,如果我们仍需使用与较低版本兼容的概括尺寸范围(小、正常、大和特大)。例如,如果要将用户界面设计成在手机上显示单面板,但在 7 英寸平板电脑、电视和其他较大的设备上显示多面板,那么我们就需要提供以下文件:
res/values-large/layout.xml:
res/values-sw600dp/layout.xml:

参考

View 的工作流程主要是指 measure、layout、draw 这三大流程,即测量、布局和绘制,其中 measure 确定 View 的测量宽/高,layout 确定 View 的最终宽/高和四个顶点的位置,而 draw 则将View绘制到屏幕上。

measure 的过程

measure 过程要分情况来看,如果只是一个原始的View,那么通过measure方法就完成了其测量过程,如果是一个ViewGroup,除了完成自己的测量过程外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个流程,下面针对这两种情况分别讨论。

  1. View 的 measure过程 View 的 measure 过程由其 measure 方法来完成,measure方法是一个final类型的方法,这意味着子类不能重写此方法,在 View的 measure 方法中会去调用View的onMeasure方法,因此只需要看onMeasure 的实现即可,View 的 onMeasure 方法如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

setMeasuredDimension 方法会设置 View 宽/高的测量值,因此我们只需要看 View.getDefaultSize 这个静态方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

可以看出,getDefaultSize 这个方法的逻辑很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY 这两种情况。简单地理解,其实getDefaultSize返回的大小就是measureSpec中的specSize,而这个specSize就是View测量后的大小,这里多次提到测量后的大小,是因为View最终的大小是在layout阶段确定的,所以这里必须要加以区分,但是几乎所有情况下View的测量大小和最终大小是相等的。

至于UNSPECIFIED这种情况,一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize的第一个参数size,即宽/高分别为 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 这两个方法的返回值,看一下它们的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Returns the suggested minimum width that the view should use. This
* returns the maximum of the view's minimum width
* and the background's minimum width
* ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned width is within the requirements of the parent.
*
* @return The suggested minimum width of the view.
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

这里只分析 getSuggestedMinimumWidth 方法的实现,getSuggestedMinimumHeight 和它的实现原理是一样的。从getSuggestedMinimumWidth 的代码可以看出,如果View没有设置背景,那么 View 的宽度为 mMinWidth,而mMinWidth对应于 android:minWidth 这个属性所指定的值,因此View的宽度即为 android:minWidth 属性所指定的值。这个属性如果不指定,那么 mMinWidth则默认为 0;如果 View 指定了背景,则View的宽度为max(mMinWidth,mBackground.getMinimumWidth())。mMinWidth 的含义我们已经知道了,那么mBackground.getMinimumWidth()是什么呢?我们看一下Drawable的getMinimumWidth方法,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Drawable {

...

/**
* Returns the minimum width suggested by this Drawable. If a View uses this
* Drawable as a background, it is suggested that the View use at least this
* value for its width. (There will be some scenarios where this will not be
* possible.) This value should INCLUDE any padding.
*
* @return The minimum width suggested by this Drawable. If this Drawable
* doesn't have a suggested minimum width, 0 is returned.
*/
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
...
}

可以看出,getMinimumWidth 返回的就是 Drawable 的原始宽度,前提是这个 Drawable 有原始宽度,否则就返回0。那么Drawable在什么情况下有原始宽度呢?这里先举个例子说明一下,ShapeDrawable 无原始宽/高,而BitmapDrawable有原始宽/高(图片的尺寸),详细内容会在第6章进行介绍。

这里再总结一下 getSuggestedMinimumWidth 的逻辑:如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 的返回值就是View在UNSPECIFIED情况下的测量宽/高。

从getDefaultSize方法的实现来看,View的宽/高由specSize决定,所以我们可以得出如下结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。为什么呢?这个原因需要结合上述代码和表1才能更好地理解。从上述代码中我们知道,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST模式,在这种模式下,它的宽/高等于specSize;查表4-1可知,这种情况下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器当前剩余的空间大小。很显然,View的宽/高就等于父容器当前剩余的空间大小,这种效果和在布局中使用match_parent完全一致。如何解决这个问题呢?也很简单,代码如下所示。

表1 普通View的MeasureSpec的创建规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec):
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec):
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec):
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec):

if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
} else if (widthSpecMode == AT_MOST) {
setMeasureDimension(mWidth,heightSpecSize);
} else if (heightSpecMode == AT_MOST){
setMeasureDimension(widthSpecSize,mHeight);
}
}

在上面的代码中,我们只需要给View指定一个默认的内部宽/高(mWidth和mHeight),并在wrap_content时设置此宽/高即可。对于非wrap_content情形,我们沿用系统的测量值即可,至于这个默认的内部宽/高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可。如果查看TextView、ImageView等的源码就可以知道,针对wrap_content情形,它们的onMeasure方法均做了特殊处理,读者可以自行查看它们的源码。

2. ViewGroup的measure过程

对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但是它提供了一个叫measureChildren的方法,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}

从上述代码来看,ViewGroup 在 measure时,会对每一个子元素进行measure,ViewGroup.measureChild这个方法的实现也很好理解,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

很显然,measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec直接传递给View的measure方法来进行测量。getChildMeasureSpec的工作过程已经在上面进行了详细分析,通过表1可以更清楚地了解它的逻辑。我们知道,ViewGroup并没有定义其测量的具体过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现,比如LinearLayout、RelativeLayout等,为什么ViewGroup不像View一样对其onMeasure方法做统一的实现呢?那是因为不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,比如LinearLayout和RelativeLayout这两者的布局特性显然不同,因此ViewGroup无法做统一实现。下面就通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程,其他Layout类型读者可以自行分析。

首先来看LinearLayout的onMeasure方法,如下所示。

1
2
3
4
5
6
7
8
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}

上述代码很简单,我们选择一个来看一下,比如选择查看竖直布局的LinearLayout的测量过程,即measureVertical方法,measureVertical的源码比较长,下面只描述其大概逻辑,首先看一段代码:

从上面这段代码可以看出,系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。当子元素测量完毕后,LinearLayout会测量自己的大小,源码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
      // Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;

int heightSize = mTotalLength;

// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);

这里对上述代码进行说明,当子元素测量完毕后,LinearLayout会根据子元素的情况来测量自己的大小。针对竖直的LinearLayout而言,它在水平方向的测量过程遵循View的测量过程,在竖直方向的测量过程则和View有所不同。具体来说是指,如果它的布局中高度采用的是match_parent或者具体数值,那么它的测量过程和View一致,即高度为specSize;如果它的布局中高度采用的是wrap_content,那么它的高度是所有子元素所占用的高度总和,但是仍然不能超过它的父容器的剩余空间,当然它的最终高度还需要考虑其在竖直方向的padding,这个过程可以进一步参看如下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Utility to reconcile a desired size and state, with constraints imposed
* by a MeasureSpec. Will take the desired size, unless a different size
* is imposed by the constraints. The returned value is a compound integer,
* with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and
* optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the
* resulting size is smaller than the size the view wants to be.
*
* @param size How big the view wants to be.
* @param measureSpec Constraints imposed by the parent.
* @param childMeasuredState Size information bit mask for the view's
* children.
* @return Size information bit mask as defined by
* {@link #MEASURED_SIZE_MASK} and
* {@link #MEASURED_STATE_TOO_SMALL}.
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}

View的measure过程是三大流程中最复杂的一个,measure完成以后,通过getMeasuredWidth/Height方法就可以正确地获取到View的测量宽/高。需要注意的是,在某些极端情况下,系统可能需要多次measure才能确定最终的测量宽/高,在这种情形下,在onMeasure方法中拿到的测量宽/高很可能是不准确的。一个比较好的习惯是在onLayout方法中去获取View的测量宽/高或者最终宽/高

上面已经对View的measure过程进行了详细的分析,现在考虑一种情况,比如我们想在Activity已启动的时候就做一件任务,但是这一件任务需要获取某个View的宽/高。读者可能会说,这很简单啊,在onCreate或者onResume里面去获取这个View的宽/高不就行了?读者可以自行试一下,实际上在onCreate、onStart、onResume中均无法正确得到某个View的宽/高信息,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此无法保证Activity执行了onCreate、onStart、onResume时某个View已经测量完毕了,如果View还没有测量完毕,那么获得的宽/高就是0。有没有什么方法能解决这个问题呢?答案是有的,这里给出四种方法来解决这个问题:

  1. Activity/View#onWindowFocusChangedonWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽/高已经准备好了,这个时候去获取宽/高是没问题的。需要注意的是,onWindowFocusChanged会被调用多次,当Activity的窗口得到焦点和失去焦点时均会被调用一次。具体来说,当Activity继续执行和暂停执行时,onWindowFocusChanged均会被调用,如果频繁地进行onResumeonPause,那么onWindowFocusChanged也会被频繁地调用。典型代码如下:
1
2
3
4
5
6
7
public void onWindowFocusChanged(boolean hasFocus){
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
}

2. view.post(runnable)

通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。典型代码如下:

1
2
3
4
5
6
7
8
9
10
protected void onStart(){
super.onStart();
view.post(new Runnable(){
@override
public void run(){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}

3. ViewTreeObserver。使用 ViewTreeObserver 的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当View树的状态发生改变或者View树内部的View的可见性发现改变时,onGlobalLayout 方法将被回调,因此这是获取View的宽/高一个很好的时机。需要注意的是,伴随着View树的状态改变等,onGlobalLayout会被调用多次。典型代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void onStart(){
super.onStart();

ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
@SuppressWarnings("deprecation");
@override
public void onGlobalLayout(){
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
  1. view.measure(int widthMeasureSpec,int heightMea-sureSpec)。通过手动对View进行measure来得到View的宽/高。这种方法比较复杂,这里要分情况处理,根据View的LayoutParams来分:
  • match_parent直接放弃,无法measure出具体的宽/高。原因很简单,根据View的measure过程,如表1所示,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量出View的大小。
  • 具体的数值(dp/px)比如宽/高都是100px,如下measure:
1
2
3
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec, heightMeasureSpec);
  • wrap_content如下measure:
1
2
3
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
v_view1.measure(widthMeasureSpec, heightMeasureSpec);

注意到(1 << 30)-1,通过分析MeasureSpec的实现可以知道,View的尺寸使用30位二进制表示,也就是说最大是30个1(即2^30 – 1),也就是(1 << 30) – 1,在最大化模式下,我们用View理论上能支持的最大值去构造MeasureSpec是合理的。

关于View的measure,网络上有两个错误的用法。为什么说是错误的,首先其违背了系统的内部实现规范(因为无法通过错误的MeasureSpec去得出合法的SpecMode,从而导致measure过程出错),其次不能保证一定能measure出正确的结果。

  • 第一种错误用法:
1
2
3
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(-1, MeasureSpec.UNSPECIFIED);
view.measure(widthMeasureSpec, heightMeasureSpec);
  • 第二种错误用法
1
view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)

layout 的过程

Layout 的作用是 ViewGroup 用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其 layout 方法,在layout方法中onLayout方法又会被调用。Layout过程和measure过程相比就简单多了,layout方法确定 View本身的位置,而onLayout方法则会确定所有子元素的位置,先看View的layout方法。

draw 的过程

Draw 过程就比较简单了,它的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:

  1. 绘制背景background.draw(canvas)。
  2. 绘制自己(onDraw)。
  3. 绘制children(dispatchDraw)。
  4. 绘制装饰(onDrawScrollBars)。

这一点通过draw方法的源码可以明显看出来,如下所示。

参考书目

每天都在无聊的等公交地铁, 终于知道了时间的宝贵, 于是买了个 499 的 Kindle(第7代)。

亚马逊 kindle 目前已经退出了中国市场,所以在线书籍赶紧存本地吧。

固件下载

一般 Kindle 在连接 WiFi 的状态下,收到亚马逊的升级推送后会自动> 升级到最新版本固件,无需手动干预,但时间不确定,少则两三天,多则几个月。若没有或等不及自动升级,也可以在此下载和自己的 Kindle 设备型号相对应的固件版本手动升级。

亚马逊 帮助-Kindle 电子阅读器软件更新
https://www.amazon.cn/gp/help/customer/display.html/ref=hp_200127470_paperwhite?nodeId=201605570

注意,如果使用迅雷通过官网链接下载时速度过慢,请把链接的 https 改为 http(去掉“s”)。

阅读全文 »

在 Android 的知识体系中,View 扮演着很重要的角色,简单来理解,View 是 Android 在视觉上的呈现。在界面上 Android 提供了一套 GUI 库,里面有很多控件,但是很多时候我们并不满足于系统提供的控件,因为这样就意味这应用界面的同类化比较严重。那么怎么才能做出与众不同的效果呢?答案是自定义 View,也可以叫自定义控件,通过自定义 View 我们可以实现各种五花八门的效果。但是自定义 View 是有一定难度的,尤其是复杂的自定义 View,大部分时候我们仅仅了解基本控件的使用方法是无法做出复杂的自定义控件的。为了更好地自定义 View,还需要掌握 View 的底层工作原理,比如 View 的测量流程、布局流程以及绘制流程,掌握这几个基本流程后,我们就对 View 的底层更加了解,这样我们就可以做出一个比较完善的自定义 View。

初识 ViewRoot 和 DecorView

在正式介绍 View 的三大流程之前,我们必须先介绍一些基本概念,这样才能更好地理解 View 的 measure、layout 和 draw 过程,本节主要介绍 ViewRoot 和 DecorView 的概念。

ViewRoot 对应于 ViewRootImpl 类,它是连接 Window-Manager 和 DecorView 的纽带,View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联,这个过程可参看如下源码:

1
2
root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);

View 的绘制流程是从 ViewRoot 的 performTraversals 方法开始的,它经过 measure、layout 和 draw 三个过程才能最终将一个 View 绘制出来,其中 measure 用来测量 View 的宽和高,layout 用来确定 View 在父容器中的放置位置,而 draw 则负责将 View 绘制在屏幕上。针对 performTraversals 的大致流程,可用流程图1来表示。

图1  performTraversals的工作流程图

如图1 所示,performTraversals 会依次调用 performMea-sure、performLayout 和 performDraw 三个方法,这三个方法分别完成顶级 View 的 measure、layout 和 draw 这三大流程,其中在 performMeasure 中会调用 measure 方法,在measure 方法中又会调用 onMeasure方法,在 onMeasure 方法中则会对所有的子元素进行 measure 过程,这个时候 measure 流程就从父容器传递到子元素中了,这样就完成了一次 measure 过程。接着子元素会重复父容器的 measure 过程,如此反复就完成了整个 View 树的遍历。同理,performLayout 和 performDraw 的传递流程和 performMeasure 是类似的,唯一不同的是,performDraw 的传递过程是在 draw 方法中通过 dispatchDraw 来实现的,不过这并没有本质区别。

measure 过程决定了 View 的宽/高,Measure 完成以后,可以通过getMeasuredWidthgetMeasuredHeight方法来获取到View 测量后的宽/高,在几乎所有的情况下它都等同于 View 最终的宽/高,但是特殊情况除外,这点在本章后面会进行说明。Layout过程决定了 View 的四个顶点的坐标和实际的 View 的宽/高,完成以后,可以通过 getTopgetBottomgetLeftgetRight来拿到 View 的四个顶点的位置,并可以通过getWidth 和 getHeight 方法来拿到 View 的最终宽/高。Draw 过程则决定了 View 的显示,只有 draw 方法完成以后 View的内容才能呈现在屏幕上。

如图2 所示,DecorView 作为顶级 View,一般情况下它内部会包含一个竖直方向的 LinearLayout,在这个 LinearLayout 里面有上下两个部分(具体情况和 Android 版本及主题有关),上面是标题栏,下面是内容栏。在 Activity 中我们通过 setContentView 所设置的布局文件其实就是被加到内容栏之中的,而内容栏的 id 是 content,因此可以理解为 Activity 指定布局的方法不叫 setview 而叫 setContentView,因为我们的布局的确加到了 id 为 content 的 FrameLayout 中。如何得到 content 呢?可以这样:ViewGroup content = findViewById (R.android.id.content)。如何得到我们设置的 View 呢?可以这样:content.getChildAt(0)。同时,通过源码我们可以知道,DecorView 其实是一个FrameLayout,View 层的事件都先经过 DecorView,然后才传递给我们的 View。

图2  顶级View:DecorView 的结构

2 理解MeasureSpec

为了更好地理解 View 的测量过程,我们还需要理解 MeasureSpec。从名字上来看,MeasureSpec 看起来像“测量规格”或者“测量说明书”,不管怎么翻译,它看起来都好像是或多或少地决定了 View 的测量过程。通过源码可以发现,MeasureSpec 的确参与了 View 的 measure 过程。读者可能有疑问,MeasureSpec 是干什么的呢?确切来说,MeasureSpec在很大程度上决定了一个View的尺寸规格,之所以说是很大程度上是因为这个过程还受父容器的影响,因为父容器影响View的MeasureSpec的创建过程。在测量过程中,系统会将 View 的LayoutParams 根据父容器所施加的规则转换成对应的MeasureSpec,然后再根据这个 measureSpec 来测量出 View 的宽/高。上面提到过,这里的宽/高是测量宽/高,不一定等于View的最终宽/高。MeasureSpec看起来有点复杂,其实它的实现是很简单的,下面会详细地分析MeasureSpec。

2.1 MeasureSpec

MeasureSpec 代表一个 32 位 int 值,高 2 位代表 Spec-Mode,低 30 位代表 SpecSize,SpecMode 是指测量模式,而SpecSize 是指在某种测量模式下的规格大小。下面先看一下 MeasureSpec 内部的一些常量的定义,通过下面的代码,应该不难理解 MeasureSpec 的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;

/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}

/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;

/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;

/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;


/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}

/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
...
...
}

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一个int值,一组Spec-Mode和SpecSize可以打包为一个MeasureSpec,而一个Mea-sureSpec可以通过解包的形式来得出其原始的SpecMode和SpecSize,需要注意的是这里提到的MeasureSpec是指Mea-sureSpec所代表的int值,而并非MeasureSpec本身。

SpecMode有三类,每一类都表示特殊的含义,如下所示。

  • UNSPECIFIED 父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
  • EXACTLY 父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对应于LayoutParams中的match_parent和具体的数值这两种模式。
  • AT_MOST 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值,具体是什么值要看不同View的具体实现。它对应于LayoutParams中的wrap_content。

2.2 MeasureSpec和LayoutParams的对应关系

上面提到,系统内部是通过MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置LayoutParams。在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams 需要和父容器一起才能决定View的MeasureSpec,从而进一步决定 View 的宽/高。另外,对于顶级 View(即DecorView)和普通View来说,MeasureSpec 的转换过程略有不同。对于 DecorView,其MeasureSpec由窗口的尺寸和其自身的 LayoutParams 来共同确定;对于普通View,其MeasureSpec由父容器的 MeasureSpec 和自身的 LayoutParams 来共同决定,MeasureSpec 一旦确定后,onMeasure 中就可以确定 View 的测量宽/高。

对于DecorView来说,在ViewRootImpl中的measureHierarchy方法中有如下一段代码,它展示了DecorView的MeasureSpec的创建过程,其中desiredWindowWidth和de-sired-WindowHeight是屏幕的尺寸:

1
2
3
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);//desireWindowWidth 是屏幕的宽度
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

上面代码调用的getRootMeasureSpec()方法的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension){
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
mesureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

通过上述代码,DecorView 的 MeasureSpec 的产生过程就很明确了,具体来说其遵守如下规则,根据它的LayoutParams中的宽/高的参数来划分。

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小;
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

对于普通View来说,这里是指我们布局中的View,View的measure过程由ViewGroup传递而来,先看一下ViewGroup的measureChildWithMargins方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

上述方法会对子元素进行measure,在调用子元素的measure方法之前会先通过getChildMeasureSpec方法来得到子元素的MeasureSpec。从代码来看,很显然,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的margin及padding有关,具体情况可以看一下ViewGroup的getChildMeasureSpec方法,清楚展示了普通View的MeasureSpec的创建规则如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view
* @param padding The padding of this view for the current dimension and
* margins, if applicable (父容器中已占用的空间大小)
* @param childDimension How big the child wants to be in the current
* dimension
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

//子元素可用的大小为父容器的尺寸减去padding
int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

表1 普通 View 的 MeasureSpec 的创建规则

这里再做一下说明。前面已经提到,对于普通 View,其MeasureSpec 由父容器的 MeasureSpec 和自身的LayoutParams 来共同决定,那么针对不同的父容器和 View 本身不同的 LayoutParams,View 就可以有多种 MeasureSpec。这里简单说一下,当 View 采用固定宽/高的时候,不管父容器的 MeasureSpec 是什么,View 的 MeasureSpec 都是精确模式并且其大小遵循 Layoutparams 中的大小。当 View 的宽/高是match_parent 时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式,那么 View 也是最大模式并且其大小不会超过父容器的剩余空间。当 View 的宽/高是 wrap_content 时,不管父容器的模式是精准还是最大化,View 的模式总是最大化并且大小不能超过父容器的剩余空间。可能读者会发现,在我们的分析中漏掉了UNSPECIFIED 模式,那是因为这个模式主要用于系统内部多次 Measure 的情形,一般来说,我们不需要关注此模式。

参考书目

本文为《合集-PC日常用软件》的姊妹篇。

前言

  1. 记住安全永远是第一要务。下载软件也尽量从应用商店,这样更安全,也避免从其他渠道下载到恶意软件。
  2. 不要让陌生人开启做出来电转接 ##*21 这种有害行为。绝对不要共享屏幕,不要转账。预防电信网络诈骗。千万不能好奇。一定要做到不贪心不恐惧

注:

  • 以下观点谨代表个人当时情况下使用 app 后个人主观感受
  • 涵盖手机、平板、TV 三大平台,目前主要以安卓手机为主
  • 在挑选 app 上尽量跨平台、无广告和体积小巧优先,一定程度保证使用体验一致
  • “官网收录”收录了一些 app 的官网,仅供参考,不做推荐

安卓还是 iOS、鸿蒙 next

如果只考虑必要需求,现阶段低于 2000 元以下只选安卓

手机还是平板

一般建议有手机后才考虑买平板,平板会有良好的影音体验。

  • 看视频、漫画更爽
  • 阅读 PDF、小说更方便
  • 可随时处理 Office 办公、邮件
  • 玩游戏更震撼
阅读全文 »
0%