CallteFoot's blog

Victory belongs to the most persevering


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

根据Uri获取文档的路径

发表于 2018-08-16 | 分类于 Android

根据Url获取文档的绝对路径,解决Android4.4以上版本Uri转换。
Android在4.4之后的版本(包括4.4)中,从相册中选取图片返回Uri进行了改动。所以无法通过该Uri来取得文件路径从而解码图片将其显示出来。
在4.3或以下可以直接用Intent.ACTION_GET_CONTENT打开相册;在4.4或以上,官方建议用ACTION_OPEN_DOCUMENT打开相册
在Android4.4之前得到的Uri为:

  • content://media/external/images/media/8302
  • content://media/external/video/media
  • content://media/external/images/media

而在Android4.4后得到的可能是以下:

  • content://com.android.providers.media.documents/document/image:8302
  • content://com.android.providers.downloads.documents/document/5

以下为Android4.4之后的适配:

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
 /**
*
* 专为Android4.4设计的从Uri获取文件绝对路径
*/
@SuppressLint("NewApi")
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
LogUtil.d("uri:" + uri);
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];

if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {

final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];

Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}

final String selection = "_id=?";
final String[] selectionArgs = new String[]{split[1]};
LogUtil.d("format uri:" + contentUri);
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}

return null;
}

/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {

Cursor cursor = null;
final String column = "_data";
final String[] projection = {column};

try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;

}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}

/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}

/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}

首先我们看一个获取Mp3文档的Uri,其格式类似:content://com.android.providers.media.documents/document/audio%3A39,
然后我根据代码进一步分析,首先看判断条件:isKitKat && DocumentsContract.isDocumentUri(context, uri),
这里判断了版本号和该Uri是否是文档类Uri,之所以要判断版本号是Uri的生成在Api19以后发送变化,通过官方文档DocumentsContract,我们也可以验证这点,DocumentsContract是在Api19加入的,其定义就是定义文档提供者与平台之间的协议,其主要作用就是关于文档Uri的一系列操作。
下面是其内部实现代码(代码都是在DocumentsContract类中):

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
private static final String PATH_DOCUMENT = "document";
private static final String PATH_TREE = "tree";
public static final String PROVIDER_INTERFACE = "android.content.action.DOCUMENTS_PROVIDER";

/**
* Test if the given URI represents a {@link Document} backed by a
* {@link DocumentsProvider}.
*
* @see #buildDocumentUri(String, String)
* @see #buildDocumentUriUsingTree(Uri, String)
*/
public static boolean isDocumentUri(Context context, @Nullable Uri uri) {
if (isContentUri(uri) && isDocumentsProvider(context, uri.getAuthority())) {
final List<String> paths = uri.getPathSegments();
if (paths.size() == 2) {
return PATH_DOCUMENT.equals(paths.get(0));
} else if (paths.size() == 4) {
return PATH_TREE.equals(paths.get(0)) && PATH_DOCUMENT.equals(paths.get(2));
}
}
return false;
}
/** {@hide} */
public static boolean isContentUri(@Nullable Uri uri) {
// public static final String SCHEME_CONTENT = "content"; !!add by custom
return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme());
}

private static boolean isDocumentsProvider(Context context, String authority) {
final Intent intent = new Intent(PROVIDER_INTERFACE);
final List<ResolveInfo> infos = context.getPackageManager()
.queryIntentContentProviders(intent, 0);
for (ResolveInfo info : infos) {
if (authority.equals(info.providerInfo.authority)) {
return true;
}
}
return false;
}

这里我们可以看出,前提条件是判断是否是contentUri&&documentProvider,然后在进一步判断其pathSegments
是否是document/或者tree/document/开头。其中isDocumentsProvider方法不是特别理解,希望大神指点下。

参考地址:
[1] 解决Android4.4以上版本Uri转换 https://blog.csdn.net/q445697127/article/details/40537945
[2] https://stackoverflow.com/questions/20067508/get-real-path-from-uri-android-kitkat-new-storage-access-framework

LinearLayout源码解读

发表于 2018-08-12 | 分类于 Android

LinearLayout基础

LinearLayout所具有的属性:

  • orientation:视图的布局方向,默认值:-1;
  • gravity:绘制起始点,默认值:-1;
  • baselineAligned:基准线对齐,其效果可以通过修改xml的属性值直接看到效果,默认值:true;
  • weightSum:子视图权重和,默认值:-1.0f;
  • baselineAlignedChildIndex:以第Index个子视图的基准线为对齐,该LinearLayout下的view以
    某个继承TextView的View的基线对齐,默认值:-1;
  • measureWithLargestChild:以最大子视图宽高,为其子视图的宽高,其起作用前提是为true,且LinearLayout在该方向的宽或高为warp_content,且子视图具有权重。默认值:false;
  • divider:分割线;
  • showDividers:分割线显示样式(middle|end|beginning|non),默认值:SHOW_DIVIDER_NONE;
  • dividerPadding:分割线内边距,默认值:0;

    解释:

  • 基准线
    其主要作用是在绘制字母的时候有个基线对齐,这个类似我们学习英语字母的时候用的四线谱:
    基线示意图
    其中红线就是基线(baseline),和下面我们书写英语字母的四线谱是不是很像,基线就是第三条。
    英语书写四线谱
  • 源码之垂直方向测量(void measureVertical(int widthMeasureSpec, int heightMeasureSpec))
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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
/**
* Measures the children when the orientation of this LinearLayout is set
* to {@link #VERTICAL}.
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
* @param heightMeasureSpec Vertical space requirements as imposed by the parent.
*
* @see #getOrientation()
* @see #setOrientation(int)
* @see #onMeasure(int, int)
*/
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// mTotalLength作为LinearLayout成员变量,其主要目的是在测量的时候通过累加得到所有子控件的高度和(Vertical)或者宽度和(Horizontal)
mTotalLength = 0;
// maxWidth用来记录所有子控件中控件宽度最大的值。
int maxWidth = 0;
// 子控件的测量状态,会在遍历子控件测量的时候通过combineMeasuredStates来合并上一个子控件测量状态与当前遍历到的子控件的测量状态,采取的是按位相或
int childState = 0;

/**
* 以下两个最大宽度跟上面的maxWidth最大的区别在于matchWidthLocally这个参数
* 当matchWidthLocally为真,那么以下两个变量只会跟当前子控件的左右margin和相比较取大值
* 否则,则跟maxWidth的计算方法一样
*/
// 子控件中layout_weight<=0的View的最大宽度
int alternativeMaxWidth = 0;
// 子控件中layout_weight>0的View的最大宽度
int weightedMaxWidth = 0;
// 是否子控件全是match_parent的标志位,用于判断是否需要重新测量
boolean allFillParent = true;
// 所有子控件的weight之和
float totalWeight = 0;

// 如您所见,得到所有子控件的数量,准确的说,它得到的是所有同级子控件的数量
// 在官方的注释中也有着对应的例子
// 比如TableRow,假如TableRow里面有N个控件,而LinearLayout(TableLayout也是继承LinearLayout哦)下有M个TableRow,那么这里返回的是M,而非M*N
// 但实际上,官方似乎也只是直接返回getChildCount(),起这个方法名的原因估计是为了让人更加的明白,毕竟如果是getChildCount()可能会让人误认为为什么没有返回所有(包括不同级)的子控件数量
final int count = getVirtualChildCount();

// 得到测量模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

// 当子控件为match_parent的时候,该值为ture,同时判定的还有上面所说的matchWidthLocally,这个变量决定了子控件的测量是父控件干预还是填充父控件(剩余的空白位置)。
boolean matchWidth = false;

boolean skippedMeasure = false;

final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;

int largestChildHeight = Integer.MIN_VALUE;

// See how tall everyone is. Also remember max width.

//查看每一个高,并记住最大宽度
for (int i = 0; i < count; ++i) {
//首先获取子View
final View child = getVirtualChildAt(i);
//如果子View是null就继续测量下一个子View
if (child == null) {
// 目前而言,measureNullChild()方法返回的永远是0,估计是设计者留下来以后或许有补充的。
mTotalLength += measureNullChild(i);
continue;
}
//如果子View是GONE的也不算在总高度里面,这里也能看出GONE和INVISIBLE的区别
if (child.getVisibility() == View.GONE) {
// 同上,返回的都是0。
// 事实上这里的意思应该是当前遍历到的View为Gone的时候,就跳过这个View,下一句的continue关键字也正是这个意思。
// 忽略当前的View,这也就是为什么Gone的控件不占用布局资源的原因。(毕竟根本没有分配空间)
i += getChildrenSkipCount(child, i);
continue;
}

// 根据showDivider的值(before/middle/end)来决定遍历到当前子控件时,高度是否需要加上divider的高度
// 比如showDivider为before,那么只会在第0个子控件测量时加上divider高度,其余情况下都不加
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
//有时候我们在代码里面通过Inflater服务,动态加载一个布局,然后去设置他的LayoutParams,
//如果不引用父容器的LayoutParams就会报一个强转错误,原因就在这个父容器在add,measure的时候都会
//把子View的LayoutParams强转成自己的类型
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
//得到每个子控件的LayoutParams后,累加权重和,后面用于跟weightSum相比较
totalWeight += lp.weight;


// 我们都知道,测量模式有三种:
// * UNSPECIFIED:父控件对子控件无约束,基本没有用到
// * Exactly:父控件对子控件强约束,子控件永远在父控件边界内,越界则裁剪。如果要记忆的话,可以记忆为有对应的具体数值或者是Match_parent
// * AT_Most:子控件为wrap_content的时候,测量值为AT_MOST。

// 下面的if/else分支都是跟weight相关
//这里就值得注意下了如果当前的LinearLayout是EXACTLY模式,且子view的高度为0,且权重大于0
//这个子view只有在LinearLayout高度有剩余的时候,才会根据权重的占比去平分剩余空间
//上文说的二次测量也就指的这部分
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// Optimization: don't bother measuring children who are going to use
// leftover space. These views will get measured again down below if
// there is any leftover space.
// 这个if里面需要满足三个条件:
// * LinearLayout的高度为match_parent(或者有具体值)
// * 子控件的高度为0
// * 子控件的weight>0

// 如果LinearLayout的垂直方向测量模式是EXACTLY,即确定值,且子视图的高度为0,weight大于0,
//则先将总高度加上子视图的topMargin和bottomMargin,并设置skippedMeasure(暂时跳过测量标识)为true

// 这其实就是我们通常情况下用weight时的写法,此时需要记住view的topMargin和bottomMargin(对于方向为)
// 测量到这里的时候,会给个标志位,稍后再处理。此时会计算总高度
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
// 到这个分支,则需要对不同的情况进行测量
int oldHeight = Integer.MIN_VALUE;

if (lp.height == 0 && lp.weight > 0) {
// heightMode is !!either UNSPECIFIED or AT_MOST!!, and this
// child wanted to stretch to fill available space.
// Translate that to WRAP_CONTENT so that it does not end up
// with a height of 0
// 满足这两个条件,意味着父类即LinearLayout是wrap_content,或者mode为UNSPECIFIED
// 那么此时将当前子控件的高度置为wrap_content
// 为何需要这么做,主要是因为当父类为wrap_content时,其大小实际上由子控件控制
// 我们都知道,自定义控件的时候,通常我们会指定测量模式为wrap_content时的默认大小
// 这里强制给定为wrap_content为的就是防止子控件高度为0.

//这里其实官方的注释讲了也挺清楚的,到了这步,当前的LinearLayout的模式
//肯定是UNSPECIFIED或者MOST,因为EXACTLY模式会进入上一个判断
//然后把子View的高度赋值成-1(WRAP_CONTENT)
// 如果垂直方向测量模式为UNSPECIFIED或AT_MOST,同时子视图想要尽量获取可用的剩余空间,
//把子视图的高度改为WRAP_CONTENT,这样子视图的最终高度就不会是0

oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}

// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
/**【1】*/
// 下面这句虽然最终调用的是ViewGroup通用的同名方法,但传入的height值是跟平时不一样的
// 这里可以看到,传入的height是跟weight有关,关于这里,稍后的文字描述会着重阐述

// 这个函数最后会调用child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
//测量出子视图要占用多大空间,并设置子视图的mMeasuredWidth和mMeasuredHeight
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);

// 重置子控件高度,然后进行精确赋值
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}

final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;

// getNextLocationOffset返回的永远是0,因此这里实际上是比较child测量前后的总高度,取大值。
//加上子View的margin值
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));

// 重新设置最大子视图高度
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}

/**
* If applicable, compute the additional offset to the child's baseline
* we'll need later when asked {@link #getBaseline}.
*/
// 计算子视图baseline的偏移量
if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}

// if we are trying to use a child index for our baseline, the above
// book keeping only works if there are no children above it with
// weight. fail fast to aid the developer.
// 如果要为baseline指定子视图索引,只有在此子视图之上的视图没有设置weight属性时才有效
if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}


// 下面开始测量宽度
boolean matchWidthLocally = false;

// 还记得我们变量里又说到过matchWidthLocally这个东东吗
// 当父类(LinearLayout)不是match_parent或者精确值的时候,但子控件却是一个match_parent
// 那么matchWidthLocally和matchWidth置为true
// 意味着这个控件将会占据父类(水平方向)的所有空间
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
// The width of the linear layout will scale, and at least one
// child said it wanted to match our width. Set a flag
// indicating that we need to remeasure at least that view when
// we know our width.
//如果LinearLayout宽度不是已确定的,如wrap_content,而子视图是MATCH_PARENT,
matchWidth = true;
matchWidthLocally = true;
}

// 计算子视图总宽度(包含左右外边距)
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
// 合并子元素的测量状态
childState = combineMeasuredStates(childState, child.getMeasuredState());

// 子视图宽度是否都为MATCH_PARENT
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
//如设置了weigh属性,则子视图的宽度需要在父视图确定后才能确定。这里并不是真实的宽度
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}

i += getChildrenSkipCount(child, i);
}
//for 循环结束

// 下面的这一段代码主要是为useLargestChild属性服务的,不在本文主要分析范围,略过
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}

if (useLargestChild &&
(heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
mTotalLength = 0;

// 如果设置了useLargestChild属性,且LinearLayout的垂直方向测量模式是AT_MOST或UNSPECIFIED,
//重新测量总高度,useLargestChild属性会使所有带weight属性的子视图具有最大子视图的最小尺寸
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);

if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}

if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}

final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// Account for negative margins
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
}
//在这两段代码之间还有些杂七杂八的处理,如果读者有兴趣可以自己阅读分析下
//当测量完子View的大小后,总高度会再加上padding的高度
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;

int heightSize = mTotalLength;
//如果设置了minimumheight属性,会根据当前使用高度和最小高度进行比较
//然后取两者中大的值,getSuggestedMinimumHeight为背景的最小高和视图设置的最小高的大值
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpe
// 把测量出来的高度与测量模式进行匹配,得到最终的高度,MeasureSpec实际上是一个32位的int,高两位是测量模式,
//剩下的就是大小,因此heightSize = heightSizeAndState & MEASURED_SIZE_MASK;作用就是用来得到大小的精确值(不含测量模式)
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

//到了这里,会再对带weight属性的子View进行一次测绘
//首先计算剩余高度

//算出剩余空间,假如之前是skipp的话,那么几乎可以肯定是有剩余空间(同时有weight)的
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
int delta = heightSize - mTotalLength;
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
//如果设置了weightSum就会使用你设置的weightSum,否则采用当前所有子View的权重和。所以如果要手动设置weightSum的时候,千万别计算错误哦
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

mTotalLength = 0;
//这里的代码就和第一次测量很像了
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);

if (child.getVisibility() == View.GONE) {
continue;
}

LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

float childExtra = lp.weight;
if (childExtra > 0) {
// 全篇最精华的一个地方。。。。拥有weight的时候计算方式,ps:执行到这里时,child依然还没进行自身的measure
//子控件的weight占比*剩余高度
// Child said it could absorb extra space -- give him his share
int share = (int) (childExtra * delta / weightSum);
// weightSum计余
weightSum -= childExtra;
//剩余高度减去分配出去的高度
delta -= share;

final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
mPaddingLeft + mPaddingRight +
lp.leftMargin + lp.rightMargin, lp.width);
//如果是当前LinearLayout的模式是EXACTLY
//那么这个子View是没有被测量过的,就需要测量一次
//如果不是EXACTLY的,在第一次循环里就被测量一些了
// TODO: Use a field like lp.isMeasured to figure out if this
// child has been previously measured
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
// child was measured once already above...
// base new measurement on stored values
//如果是非EXACTLY模式下的子View就再加上
//weight分配占比*剩余高度
// 上面已经测量过这个子视图,把上面测量的结果加上根据weight分配的大小
int childHeight = child.getMeasuredHeight() + share;
if (childHeight < 0) {
childHeight = 0;
}

//重新测量一次,因为高度发生了变化
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
} else {
// child was skipped in the loop above.
// Measure for this first time here

//如果是EXACTLY模式下的
//这里只会把weight占比所拥有的高度分配给你的子View
// 上面测量的时候被跳过,那么在这里进行测量
child.measure(childWidthMeasureSpec,
MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
MeasureSpec.EXACTLY));
}

// Child may now not fit in vertical dimension.
childState = combineMeasuredStates(childState, child.getMeasuredState()
& (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
}

final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);

boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
lp.width == LayoutParams.MATCH_PARENT;

alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);

allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
}
// 这里得到最终高度
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
// TODO: Should we recompute the heightSpec based on the new total length?
} else {
// 没有weight的情况下,只看useLargestChild参数,如果都无相关,那就走layout流程了,因此这里忽略
alternativeMaxWidth = Math.max(alternativeMaxWidth,weightedMaxWidth);


// We have no limit, so make all weighted views as tall as the largest child.
// Children will have already been measured once.
// 使所有具有weight属性 视图都和最大子视图一样高,子视图可能在上面已经被测量过一次
if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);

if (child == null || child.getVisibility() == View.GONE) {
continue;
}

final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();

float childExtra = lp.weight;
if (childExtra > 0) {
child.measure(
MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(largestChildHeight,
MeasureSpec.EXACTLY));
}
}
}
}

if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}

maxWidth += mPaddingLeft + mPaddingRight;

// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

// 设置测量完的宽高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);

if (matchWidth) {
// 使宽度一致
forceUniformWidth(count, heightMeasureSpec);
}
}

在垂直绘制中主要执行逻辑在两大块代码,第一个for循环,第二个if判断中的for循环,接下来我们分块分析该函数源码:

变量的定义

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
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {      
// mTotalLength作为LinearLayout成员变量,其主要目的是在测量的时候通过累加得到所有子控件的高度和(Vertical)或者宽度和(Horizontal)
mTotalLength = 0;
// maxWidth用来记录所有子控件中控件宽度最大的值。
int maxWidth = 0;
// 子控件的测量状态,会在遍历子控件测量的时候通过combineMeasuredStates来合并上一个子控件测量状态与当前遍历到的子控件的测量状态,采取的是按位相或
int childState = 0;

/**
* 以下两个最大宽度跟上面的maxWidth最大的区别在于matchWidthLocally这个参数
* 当matchWidthLocally为真,那么以下两个变量只会跟当前子控件的左右margin和相比较取大值
* 否则,则跟maxWidth的计算方法一样
*/
// 子控件中layout_weight<=0的View的最大宽度
int alternativeMaxWidth = 0;
// 子控件中layout_weight>0的View的最大宽度
int weightedMaxWidth = 0;
// 是否子控件全是match_parent的标志位,用于判断是否需要重新测量
boolean allFillParent = true;
// 所有子控件的weight之和
float totalWeight = 0;

// 如您所见,得到所有子控件的数量,准确的说,它得到的是所有同级子控件的数量
// 在官方的注释中也有着对应的例子
// 比如TableRow,假如TableRow里面有N个控件,而LinearLayout(TableLayout也是继承LinearLayout哦)下有M个TableRow,那么这里返回的是M,而非M*N
// 但实际上,官方似乎也只是直接返回getChildCount(),起这个方法名的原因估计是为了让人更加的明白,毕竟如果是getChildCount()可能会让人误认为为什么没有返回所有(包括不同级)的子控件数量
final int count = getVirtualChildCount();

// 得到测量模式
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

// 当子控件为match_parent的时候,该值为ture,同时判定的还有上面所说的matchWidthLocally,这个变量决定了子控件的测量是父控件干预还是填充父控件(剩余的空白位置)。
boolean matchWidth = false;

boolean skippedMeasure = false;

final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;

int largestChildHeight = Integer.MIN_VALUE;

// ...... 底下两个for循环
}

在变量定义中,我们主要留意三个方面:

  • mTotalLength:这个就是最终得到的整个LinearLayout的高度(子控件高度累加及自身padding)
  • 三个跟width相关的变量
  • weight相关的变量

第一个for代码块和baselineChildIndex处理

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// ...上面的一大堆局部变量
for (int i = 0; i < count; ++i) {

final View child = getVirtualChildAt(i);

if (child == null) {
// 目前而言,measureNullChild()方法返回的永远是0,估计是设计者留下来以后或许有补充的。
mTotalLength += measureNullChild(i);
continue;
}

if (child.getVisibility() == GONE) {
// 同上,返回的都是0。
// 事实上这里的意思应该是当前遍历到的View为Gone的时候,就跳过这个View,下一句的continue关键字也正是这个意思。
// 忽略当前的View,这也就是为什么Gone的控件不占用布局资源的原因。(毕竟根本没有分配空间)
i += getChildrenSkipCount(child, i);
continue;
}

// 根据showDivider的值(before/middle/end)来决定遍历到当前子控件时,高度是否需要加上divider的高度
// 比如showDivider为before,那么只会在第0个子控件测量时加上divider高度,其余情况下都不加
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerWidth;
}

final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
child.getLayoutParams();
// 得到每个子控件的LayoutParams后,累加权重和,后面用于跟weightSum相比较
totalWeight += lp.weight;

// 我们都知道,测量模式有三种:
// * UNSPECIFIED:父控件对子控件无约束
// * Exactly:父控件对子控件强约束,子控件永远在父控件边界内,越界则裁剪。如果要记忆的话,可以记忆为有对应的具体数值或者是Match_parent
// * AT_Most:子控件为wrap_content的时候,测量值为AT_MOST。

// 下面的if/else分支都是跟weight相关
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
// 这个if里面需要满足三个条件:
// * LinearLayout的高度为match_parent(或者有具体值)
// * 子控件的高度为0
// * 子控件的weight>0
// 这其实就是我们通常情况下用weight时的写法
// 测量到这里的时候,会给个标志位,稍后再处理。此时会计算总高度
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
// 到这个分支,则需要对不同的情况进行测量
int oldHeight = Integer.MIN_VALUE;

if (lp.height == 0 && lp.weight > 0) {
// 满足这两个条件,意味着父类即LinearLayout是wrap_content,或者mode为UNSPECIFIED
// 那么此时将当前子控件的高度置为wrap_content
// 为何需要这么做,主要是因为当父类为wrap_content时,其大小实际上由子控件控制
// 我们都知道,自定义控件的时候,通常我们会指定测量模式为wrap_content时的默认大小
// 这里强制给定为wrap_content为的就是防止子控件高度为0.
oldHeight = 0;
lp.height = LayoutParams.WRAP_CONTENT;
}

/**【1】*/
// 下面这句虽然最终调用的是ViewGroup通用的同名方法,但传入的height值是跟平时不一样的
// 这里可以看到,传入的height是跟weight有关,关于这里,稍后的文字描述会着重阐述
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);

// 重置子控件高度,然后进行精确赋值
if (oldHeight != Integer.MIN_VALUE) {
lp.height = oldHeight;
}

final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
// getNextLocationOffset返回的永远是0,因此这里实际上是比较child测量前后的总高度,取大值。
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));

if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}

if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
mBaselineChildTop = mTotalLength;
}

if (i < baselineChildIndex && lp.weight > 0) {
throw new RuntimeException("A child of LinearLayout with index "
+ "less than mBaselineAlignedChildIndex has weight > 0, which "
+ "won't work. Either remove the weight, or don't set "
+ "mBaselineAlignedChildIndex.");
}

boolean matchWidthLocally = false;

// 还记得我们变量里又说到过matchWidthLocally这个东东吗
// 当父类(LinearLayout)不是match_parent或者精确值的时候,但子控件却是一个match_parent
// 那么matchWidthLocally和matchWidth置为true
// 意味着这个控件将会占据父类(水平方向)的所有空间
if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
matchWidth = true;
matchWidthLocally = true;
}

final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());

allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

if (lp.weight > 0) {
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}

i += getChildrenSkipCount(child, i);
}
//... 底下第二个for循环
}
```
在第一个for循环中,主要是if(){}else{}分支,判断是heightMode == MeasureSpec.EXACTLY &&
lp.height == 0 && lp.weight > 0,这个主要是linearLayout测量模式为EXACTLY且子视图明确
了是使用linearLayout的剩余空间,此时将其上下间距计入总高度并以之前的做对比去大值,并设置
skippedMeasure标志为true。而在else中则为复杂点,else中首先对lp.height == 0 &&
lp.weight > 0的子视图的height做预处理使其为LayoutParams.WRAP_CONTENT(因为父类即
LinearLayout此时是wrap_content,或者mode为UNSPECIFIED),接着对子视图进行测量(
这个受总权重影响),并将其高度和上下间距计入到总高度中。之后对baselineChildIndex做处理,
计入总的基线高度并判定基线配置是否合理,不合理抛出异常,最后根据子视图设置最大宽度、
allFillParent、weightedMaxWidth或alternativeMaxWidth变量。

#### 第2个重要代码块(if(){}else{}分支))执行前的处理 ####

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
// … 局部变量定义和第一个for循环
// 下面的这一段代码主要是为useLargestChild属性服务的,不在本文主要分析范围,略过
if (mTotalLength > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}

  if (useLargestChild &&
          (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
      mTotalLength = 0;

      // 如果设置了useLargestChild属性,且LinearLayout的垂直方向测量模式是AT_MOST或UNSPECIFIED,
//重新测量总高度,useLargestChild属性会使所有带weight属性的子视图具有最大子视图的最小尺寸
      for (int i = 0; i < count; ++i) {
          final View child = getVirtualChildAt(i);

          if (child == null) {
              mTotalLength += measureNullChild(i);
              continue;
          }

          if (child.getVisibility() == GONE) {
              i += getChildrenSkipCount(child, i);
              continue;
          }

          final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                  child.getLayoutParams();
          // Account for negative margins
          final int totalLength = mTotalLength;
          mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                  lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
      }
  }
  //... 第2个重要代码块
}
1
2
3
4
5
这里主要对useLargestChild属性处理,执行前提是设置了useLargestChild属性,且LinearLayout的垂直
方向测量模式是AT_MOST或UNSPECIFIED,重新测量总高度,useLargestChild属性会使所有带weight属性的子视
图具有最大子视图的最小尺寸

第2个重要代码块(if(){}else{}分支))执行逻辑

//当测量完子View的大小后,总高度会再加上padding的高度
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;

int heightSize = mTotalLength;
//如果设置了minimumheight属性,会根据当前使用高度和最小高度进行比较
//然后取两者中大的值,getSuggestedMinimumHeight为背景的最小高和视图设置的最小高的大值
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpe
// 把测量出来的高度与测量模式进行匹配,得到最终的高度,MeasureSpec实际上是一个32位的int,高两位是测量模式,剩下的就是大小,因此heightSize = heightSizeAndState & MEASURED_SIZE_MASK;作用就是用来得到大小的精确值(不含测量模式)
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;

//到了这里,会再对带weight属性的子View进行一次测绘
//首先计算剩余高度

//算出剩余空间,假如之前是skipp的话,那么几乎可以肯定是有剩余空间(同时有weight)的
// Either expand children with weight to take up available space or
// shrink them if they extend beyond our current bounds. If we skipped
// measurement on any children, we need to measure them now.
int delta = heightSize - mTotalLength;
if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
    //如果设置了weightSum就会使用你设置的weightSum,否则采用当前所有子View的权重和。所以如果要手动设置weightSum的时候,千万别计算错误哦
    float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

    mTotalLength = 0;
    //这里的代码就和第一次测量很像了
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);

        if (child.getVisibility() == View.GONE) {
            continue;
        }

        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

        float childExtra = lp.weight;
        if (childExtra > 0) {
            // 全篇最精华的一个地方。。。。拥有weight的时候计算方式,ps:执行到这里时,child依然还没进行自身的measure
            //子控件的weight占比*剩余高度
            // Child said it could absorb extra space -- give him his share
            int share = (int) (childExtra * delta / weightSum);
            // weightSum计余
            weightSum -= childExtra;
            //剩余高度减去分配出去的高度
            delta -= share;

            final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                    mPaddingLeft + mPaddingRight +
                            lp.leftMargin + lp.rightMargin, lp.width);
            //如果是当前LinearLayout的模式是EXACTLY
            //那么这个子View是没有被测量过的,就需要测量一次
            //如果不是EXACTLY的,在第一次循环里就被测量一些了
            // TODO: Use a field like lp.isMeasured to figure out if this
            // child has been previously measured
            if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                // child was measured once already above...
                // base new measurement on stored values
                //如果是非EXACTLY模式下的子View就再加上
                //weight分配占比*剩余高度
                // 上面已经测量过这个子视图,把上面测量的结果加上根据weight分配的大小
                int childHeight = child.getMeasuredHeight() + share;
                if (childHeight < 0) {
                    childHeight = 0;
                }

                //重新测量一次,因为高度发生了变化
                child.measure(childWidthMeasureSpec,
                        MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
            } else {
                // child was skipped in the loop above.
                // Measure for this first time here  

                //如果是EXACTLY模式下的
                //这里只会把weight占比所拥有的高度分配给你的子View
                // 上面测量的时候被跳过,那么在这里进行测量    
                child.measure(childWidthMeasureSpec,
                        MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                MeasureSpec.EXACTLY));
            }

            // Child may now not fit in vertical dimension.
            childState = combineMeasuredStates(childState, child.getMeasuredState()
                    & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
        }

        final int margin =  lp.leftMargin + lp.rightMargin;
        final int measuredWidth = child.getMeasuredWidth() + margin;
        maxWidth = Math.max(maxWidth, measuredWidth);

        boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                lp.width == LayoutParams.MATCH_PARENT;

        alternativeMaxWidth = Math.max(alternativeMaxWidth,
                matchWidthLocally ? margin : measuredWidth);

        allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
    }
    // 这里得到最终高度
    // Add in our padding
    mTotalLength += mPaddingTop + mPaddingBottom;
    // TODO: Should we recompute the heightSpec based on the new total length?
} else {
     // 没有weight的情况下,只看useLargestChild参数,如果都无相关,那就走layout流程了,因此这里忽略
    alternativeMaxWidth = Math.max(alternativeMaxWidth,weightedMaxWidth);


    // We have no limit, so make all weighted views as tall as the largest child.
    // Children will have already been measured once.
    // 使所有具有weight属性 视图都和最大子视图一样高,子视图可能在上面已经被测量过一次
    if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);

            if (child == null || child.getVisibility() == View.GONE) {
                continue;
            }

            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            float childExtra = lp.weight;
            if (childExtra > 0) {
                child.measure(
                        MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                MeasureSpec.EXACTLY),
                        MeasureSpec.makeMeasureSpec(largestChildHeight,
                                MeasureSpec.EXACTLY));
            }
        }
    }
}

```
在进入ifelse分支前,先计算视图的总高度,并与测量模式进行比较(resolveSizeAndState)得到最终高度,
在减去总高度,得到最终还剩多高(也就是可以分配给带权重的视图的高);
ifelse首先判断(skippedMeasure || delta != 0 && totalWeight > 0.0f),

  • 如果该条件为true,先将总高度置为0再进入for循环,此处根据子视图的权重,再次判定(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)

    • true:子视图再次测量 则计算子视图可以得到多少高(有可能为负,也就是子视图要吐出一定高度出来),然后子视图测量高度和分配的高度相加,小于0,则重置为0,最后在测量一次。
    • false:直接测量子视图,这个是之前被跳过没有测量的子视图;
      最后再次测量视图的宽和总高度。

    • 如果该条件为false:看useLargestChild参数,如果都无相关,那就走layout流程了,
      我们可以看到这里直接判断是useLargestChild && heightMode != MeasureSpec.EXACTLY,如果条件成立的话,遍历子视图,再次判断子视图是否含有权重,如果有则直接将子视图高度都是largestChildHeight。如果条件不成立,则啥也不做。

最后就是视图maxWidth计算,并setMeasuredDimension(这个是一定要的),自此垂直方向已经测量完毕。

总结
这里大篇幅讲解measureVertical()的流程,事实上对于LinearLayout来说,其最大的特性也正是两个
方向的排布以及weight的计算方式。回过头来看测量过程,我们可以看出设计者的测量计算思路,就是将有weight
和不含有weight的测量分开处理,再利用height跟0比较来更加的细分每一种情况。
最后我们在理下其测量不同情况和原理:

  • 父控件为match_parent(或者精确值),子控件拥有weight,并且高度给定为0,也即子控件明确表示使用剩余空间:
    • 子控件的高度比例将会跟我们分配的layout_weight一致,原因在于weight二次测量时走了else分支,传入的是计算出来的share值;
  • 父控件是match_parent(或者精确值),子控件拥有weight,但高度给定为match_parent(或者精确值),子控件使用自己的高度或者父控件的高度,但在父控件空间不足时,其大小可以调整:
    • 子控件高度比例将会跟我们分配的layout_weight相反,原因在于在此之前子控件测量过一次,同时子控件的测量高度为父控件的高度,在计算剩余空间的时候得出一个负值,加上自身的测量高度的时候反而更小;
  • 父控件是wrap_content,子控件拥有weight:
    • 子控件的高度将会强行置为其wrap_content给的值并以wrap_content模式进行测量
  • 父控件是wrap_content,子控件没有weight:
    • 子控件的高度跟其他的viewgroup一致

自此,LinearLayout在垂直方向的测量分析已经结束。

参考地址:
[1]. baselineAligned解析 http://www.bubuko.com/infodetail-612730.html
[2]. measureWithLargestChild使用解析 https://blog.csdn.net/a87b01c14/article/details/49420449
[3]. LinearLayout垂直测量分析 https://www.jianshu.com/p/aea27bac7c8e
[4]. view和LinearLayout源码分析 https://www.jianshu.com/p/f9b9f05222a8

android-camera2预览拍照录制

发表于 2018-07-22 | 分类于 Android

Android 5.0(Lollipop)增加了Camera2 API,并将原有的Camera API标记为废弃。对于原有的Camera API来说,Camera2重新定义了相机的API,也重构了相机API的架构。在Camera2中其主要思想是基于会话模式和事件驱动与相机实现交互,对于预览、拍照、录制等操作都是在会话的基础下请求某种类型的会话操作。

比如一次拍照的操作:
拍照

下面一起看下camera2的操作:

  1. 相机初始化
    我们知道要使用相机,首先我们需要获得相关的权限,主要是在manifest中定义,其次在Android6.0还需要动态获取权限。
  • 在manifest中定义需要的权限
    1
    <uses-permission android:name="android.permission.CAMERA" />

如果要保存照片、录制视频,还需要两个权限:

1
2
3
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

相机功能:相机特性,如:

1
2
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

  • 动态权限申请
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private final String[] VIDEO_PERMISSIONS = {
    Manifest.permission.CAMERA,
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.WRITE_EXTERNAL_STORAGE,
    Manifest.permission.READ_EXTERNAL_STORAGE,
    };
    //......
    if (!hasPermissionsGranted(getApplicationContext(), VIDEO_PERMISSIONS)) {
    requestPermissions(VIDEO_PERMISSIONS, 1);
    return;
    }
    //......

    hasPermissionsGranted(Context context, String[] permissions) {
    for (String permission : permissions) {
    if (ActivityCompat.checkSelfPermission(context, permission)
    != PackageManager.PERMISSION_GRANTED) {
    return false;
    }
    }
    return true;
    }

以上基础工作好了基本可以开始对相机操作了。

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
//初始化相机设备
private void initCamera() {
//设备管理类
cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
//获取相机设备特征类,通过该类可以获取相机的一些特性,如相机的方向
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraManager.getCameraIdList()[0]);
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
Log.e(TAG, "sensor_orientation is :" + mSensorOrientation);
StreamConfigurationMap streamConfigurationMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//获取录制视屏时的宽高,这个通过MediaRecorder类获取系统支持的录制视频的宽高,主要是防止配置录制适配配置时失败
mVideoSize = chooseVideoSize(streamConfigurationMap.getOutputSizes(MediaRecorder.class));
//根据录制视频支持的宽高和SurfaceTexture支持的宽高,以及当前视图的宽高设置预览视图的宽高
mPreviewSize = chooseOptimalSize(streamConfigurationMap.getOutputSizes(SurfaceTexture.class), surfaceView.getWidth(), surfaceView.getHeight(), mVideoSize);
//imageReader初始化,用于获取拍照信息
imageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.JPEG, 2);
imageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
imageView.setVisibility(View.VISIBLE);
// 拿到拍照照片数据
Image image = reader.acquireNextImage();
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);//由缓冲区存入字节数组
final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
updatePreView();
}
}, mainHandler);
//打开摄像头,stateCallback为相机的状态监听回调
cameraManager.openCamera(cameraManager.getCameraIdList()[0], stateCallback, mainHandler);
mMediaRecorder = new MediaRecorder();
Log.d(TAG, "open camera");

} catch (CameraAccessException e) {
e.printStackTrace();
}
}
//开启相机后有一个回调,stateCallback,该回调是用来返回相机是否正常打开的状态的开启相机后有一个回调,stateCallback,该回调是用来返回相机是否正常打开的状态的
private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
Log.d(TAG, "camera open");
mCameraDevice = cameraDevice;
takePreview();
}

@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
Log.d(TAG, "camera onDisconnected");

if (null != mCameraDevice) {
mCameraDevice.close();
mCameraDevice = null;
}
}

@Override
public void onError(@NonNull CameraDevice cameraDevice, int i) {
Log.d(TAG, "camera onError");
cameraDevice.close();
mCameraDevice = null;
Toast.makeText(ImageShowActivity.this, "摄像头开启失败", Toast.LENGTH_SHORT).show();
}
};

  1. 开启相机预览

    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
    /**
    * 开始预览,此处创建一个捕获视频信息的请求,以此来获取一个会话session,在获取会话时监听其配置状态,一旦成功,则此时通过会话构建一个重复预览的请求;
    */
    private void takePreview() {
    try {
    closePreviewSession();
    SurfaceTexture surfaceTexture = surfaceView.getSurfaceTexture();
    surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
    Log.e(TAG, "preview SurfaceTexture buffer size is width:" + mPreviewSize.getWidth() + " height :" + mPreviewSize.getHeight());
    Surface previewSurface = new Surface(surfaceTexture);
    List<Surface> surfaces = new ArrayList<>();
    surfaces.add(previewSurface);
    surfaces.add(imageReader.getSurface());
    // 创建CameraCaptureSession,该对象负责管理处理预览请求和拍照请求
    mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() //
    {
    @Override
    public void onConfigured(CameraCaptureSession cameraCaptureSession) {
    Log.d("onConfigured", "onConfigured");
    if (null == mCameraDevice) return;
    // 当摄像头已经准备好时,开始显示预览
    mCameraCaptureSession = cameraCaptureSession;
    updatePreView();
    }

    @Override
    public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
    Log.d("onConfigureFailed", "onConfigureFailed");

    Toast.makeText(ImageShowActivity.this, "配置失败", Toast.LENGTH_SHORT).show();
    }
    }, childHandler);
    } catch (CameraAccessException e) {
    e.printStackTrace();
    }
    }
    /**
    * 更新预览视图,构建一个TEMPLATE_PREVIEW捕获请求,此时是对会话进行设置!!!setRepeatingRequest!!!
    */
    private void updatePreView() {
    try {
    // 创建预览需要的CaptureRequest.Builder
    CaptureRequest.Builder mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
    // 将SurfaceView的surface作为CaptureRequest.Builder的目标
    SurfaceTexture surfaceTexture = surfaceView.getSurfaceTexture();
    Log.e(TAG, "update preview SurfaceTexture buffer size is width:" + mPreviewSize.getWidth() + " height :" + mPreviewSize.getHeight());
    surfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
    Surface previewSurface = new Surface(surfaceTexture);

    mPreviewBuilder.addTarget(previewSurface);
    mPreviewBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
    mCameraCaptureSession.setRepeatingRequest(mPreviewBuilder.build(), null, childHandler);
    } catch (CameraAccessException e) {
    e.printStackTrace();
    }
    }
  2. 拍照

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
/**
* 拍照,构建一个TEMPLATE_STILL_CAPTURE静态的相机信息捕获请求,需要注意的是需要获取ImageReader的surface并将其作为捕获去请求的目标输出。
*/
private void takePicture() {
if (mCameraDevice == null)
return;
// 创建拍照需要的CaptureRequest.Builder
try {
CaptureRequest.Builder captureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
// 将surfaceHolder的surface作为CaptureRequest.Builder的目标
captureRequestBuilder.addTarget(imageReader.getSurface());
// 自动对焦
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// 自动曝光
captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
// 获取手机方向,手机竖屏和平板的方向是不同的,需要调整。
int rotation = getWindowManager().getDefaultDisplay().getRotation();
// 根据设备方向计算设置照片的方向
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
//拍照
CaptureRequest mCaptureRequest = captureRequestBuilder.build();
mCameraCaptureSession.capture(mCaptureRequest, null, childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}

/**
* Retrieves the JPEG orientation from the specified screen rotation.
*
* @param rotation The screen rotation.
* @return The JPEG orientation (one of 0, 90, 270, and 360)
*/
private int getOrientation(int rotation) {
// Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X)
// We have to take that into account and rotate JPEG properly.
// For devices with orientation of 90, we simply return our mapping from ORIENTATIONS.
// For devices with orientation of 270, we need to rotate the JPEG 180 degrees.
return (ORIENTATIONS.get(rotation) + mSensorOrientation + 270) % 360;
}
  1. 录制视频
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
/**
* 视屏录制,比较上层api是直接通过Recorder类来实现录制,通过这个不需要自己对视屏数据进行处理,只需要指定具体编码格式即可,同时注意这里是重启一个会话。
*/
private void startRecordingVideo() {
//关闭预览会话
closePreviewSession();
//对Recoder类进行设置
setUpMediaRecorder();
try {
//创建录制的session会话中的请求
CaptureRequest.Builder mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
//向CaptureRequest添加surface
SurfaceTexture surfaceTexture = surfaceView.getSurfaceTexture();
Log.e(TAG, "video SurfaceTexture buffer size is width:" + mVideoSize.getWidth() + " height :" + mVideoSize.getHeight());
surfaceTexture.setDefaultBufferSize(mVideoSize.getWidth(), mVideoSize.getHeight());
Surface previewSurface = new Surface(surfaceTexture);
mPreviewBuilder.addTarget(previewSurface);
//向CaptureRequest添加surface
mPreviewBuilder.addTarget(mMediaRecorder.getSurface());


mCameraDevice.createCaptureSession(Arrays.asList(previewSurface, mMediaRecorder.getSurface()), new
CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
mCameraCaptureSession = cameraCaptureSession;
updatePreView();
runOnUiThread(new Runnable() {
@Override
public void run() {
mMediaRecorder.start();

}
});
}

@Override
public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
Log.d("onConfigureFailed", "onConfigureFailed");

Toast.makeText(ImageShowActivity.this, "RecordingVideo 配置失败", Toast.LENGTH_SHORT).show();
}
}, childHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 对Recorder类进行设置,主要包括音频、视频源、视频输出格式、输出路径、编码频率、视频帧频率、视频宽高、视频编码格式、音频编码格式
*/
private void setUpMediaRecorder() {
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
//!!!这里需要设置路径,要不获取surface时会为空。context.getExternalFilesDir(null);
mMediaRecorder.setOutputFile(Environment
.getExternalStorageDirectory() + "/" + System.currentTimeMillis() + ".mp4");
mMediaRecorder.setVideoEncodingBitRate(10000000);
mMediaRecorder.setVideoFrameRate(30);
// 设置视频录制的分辨率。必须放在设置编码和格式的后面,否则报错!!!!!!需要小心设置,同时需要根据Recorder类来遴选出当前设备支持的分辨率,如果不恰当,则录制视频的时候会显示配置失败
Log.e(TAG, "video size is width:" + mVideoSize.getWidth() + " height :" + mVideoSize.getHeight());
mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());

mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
int rotation = getWindowManager().getDefaultDisplay().getRotation();
//调整视频的方向
switch (mSensorOrientation) {
case SENSOR_ORIENTATION_DEFAULT_DEGREES:
mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
break;
case SENSOR_ORIENTATION_INVERSE_DEGREES:
mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));
break;
}
try {
mMediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
}

手机支持的一些分辨率:
width:4608 height:3456
width:4608 height:2304
width:3456 height:3456
width:3840 height:2160
width:3280 height:2448
width:3264 height:2448
width:3264 height:1840
width:3264 height:1632
width:2448 height:2448
width:2592 height:1952
width:2048 height:1536
width:1920 height:1080
width:1440 height:1080
width:1536 height:864
width:1456 height:1456
width:1920 height:960
width:1440 height:720
width:1280 height:960
width:1280 height:720
width:960 height:720

参考地址:
[1].官网示例 https://github.com/googlesamples/android-Camera2Basic
[2]. https://blog.csdn.net/z_x_Qiang/article/details/77600880?locationNum=1&fps=1

http之head解析

发表于 2018-07-17 | 分类于 HTTP
Header Header 解释 示例
Accept-Ranges 表明服务器是否支持指定范围请求及那种类型的分段请求 Accept-Ranges: bytes
Age 从原始服务器到代理缓存形成的估算时间(以秒计,非负) Age:12
Allow 对某网络资源的有效请求行为,不允许则返回405 Allow:GET,HEAD
Cache-Control 告诉所有的缓存机制是否可以缓存及那种类型 Cache-Control: no-cache
Content-Encoding web服务器支持的返回内容压缩编码类型 Content-Encoding:gzip
Content-Language 响应体的语言 Content-Language: en,zh
Content-Length 响应体的长度 Content-Length:348
Content-Location 请求资源可替代的备用的另一个地址 Content-Location: /index.htm
Content-MD5 返回资源的MD5校验值 Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
Content-Range 在整个返回体中本部分的字节位置 Content-Range: bytes 21010-47021/47022
Content-type 返回内容的MIME类型 Content-Type: text/html; charset=utf-8
Date 原始服务器消息发出的时间 Date: Tue, 15 Nov 2010 08:12:31 GMT
ETag 请求变量的实体标签的当前值 ETag: “737060cd8c284d8af7ad3082f209582d”
Expires 响应过期的日期和时间 Expires: Thu, 01 Dec 2010 16:00:00 GMT
Last-Modified 请求资源的最后修改时间 Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT
Location 用来重定向接收方到非请求URL的位置来完成请求和标识新的资源 Location: http://www.zcmhi.com/archives/94.html
Pragma 包括实现特定的指令,它可应用到响应链上的任何接收方 Pragma: no-cache
Proxy-Authenticate 它指出认证方案和可应用到代理的该URL上的参数 Proxy-Authenticate: Basic
refresh 应用于重定向或一个新的资源被创造,在5秒之后重定向(由网景提出,被大部分浏览器支持) Refresh: 5; url=

http://www.zcmhi.com/archives/94.html |
| Retry-After | 如果实体暂时不可取,通知客户端在指定时间之后再次尝试 | Retry-After: 120 |
| Server | web服务器软件名称 | Server: Apache/1.3.27 (Unix) (Red-Hat/Linux) |
| Set-Cookie | 设置Http Cookie | Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1 |
| Trailer | 指出头域在分块传输编码的尾部存在 | Trailer: Max-Forwards |
| Transfet-Encoding | 文件传输编码 | Transfer-Encoding:chunked |
| Vary | 告诉下游代理是使用缓存响应还是从原始服务器请求 | Vary: * |
| Via | 告诉代理客户端响应是通过哪里发送的 | Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) |
| Warning | 警告实体可能存在的问题 | Warning: 199 Miscellaneous warning |
| WWW-Authenticate | 表明客户端请求实体应该使用的授权方案 | WWW-Authenticate: Basic |

HTTP Request的Header信息

http请求由三部分组成,分别是:请求行、消息报头、请求正文。请求行以一个方法符号开头,以空格分开,后面跟着请求的URI和协议的版本,格式如下:
POST /rest/sur?dk=28192985&ak=23565637&av=4.5.0&c=default-Channel&v=3.0&s=72eec83f97e2d4b15457f2b348b450d2&d=Wp4zp4JKnXIDAPzAGdNq5Fdp&sv=6.2.0&p=MacOSX&t=1531794939&u=&is=0 HTTP/1.1
一个简单的请求头:

1
2
3
4
5
6
7
8
9
POST /rest/sur?dk=28192985&ak=23565637&av=4.5.0&c=default-Channel&v=3.0&s=72eec83f97e2d4b15457f2b348b450d2&d=Wp4zp4JKnXIDAPzAGdNq5Fdp&sv=6.2.0&p=MacOSX&t=1531794939&u=&is=0 HTTP/1.1
Host adash.m.taobao.com
Content-Type multipart/form-data; boundary=--iphone_BOUNDARY--
Connection keep-alive
Accept */*
User-Agent %E9%92%89%E9%92%89/400 CFNetwork/901.1 Darwin/17.6.0 (x86_64)
Accept-Language zh-cn
Accept-Encoding gzip
Content-Length 702

  1. HTTP请求方式
方法 描述
GET 向Web服务器请求一个文件
POST 向Web服务器发送数据让Web服务器进行处理
PUT 向Web服务器发送数据并存储在Web服务器内部
HEAD 检查一个对象是否存在
DELETE 从Web服务器上删除一个文件
CONNECT 对通道提供支持
TRACE 跟踪到服务器的路径
OPTIONS 查询Web服务器的性能

说明:
主要使用到“GET”和“POST”。

实例:
POST /test/tupian/cm HTTP/1.1
分成三部分:

  • POST:HTTP请求方式

  • /test/tupian/cm:请求Web服务器的目录地址(或者指令)

  • HTTP/1.1: URI(Uniform Resource Identifier,统一资源标识符)及其版本

备注: 在Ajax中,对应method属性设置。

  1. Host

说明:
请求的web服务器域名地址

实例:
例如web请求URL:http://zjm-forum-test10.zjm.baidu.com:8088/test/tupian/cm ,Host就为zjm-forum-test10.zjm.baidu.com:8088

  1. User-Agent

说明:
HTTP客户端运行的浏览器类型的详细信息。通过该头部信息,web服务器可以判断到当前HTTP请求的客户端浏览器类别。

实例:
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.8.1.11) Gecko/20071127 Firefox/2.0.0.11

  1. Accept

说明:
指定客户端能够接收的内容类型,内容类型中的先后次序表示客户端接收的先后次序。

实例:
Accept:text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,/;q=0.5

备注:
在Prototyp(1.5)的Ajax代码封装中,将Accept默认设置为“text/javascript, text/html, application/xml, text/xml, /”。这是因为Ajax默认获取服务器返回的Json数据模式。在Ajax代码中,可以使用XMLHttpRequest 对象中setRequestHeader函数方法来动态设置这些Header信息。

  1. Accept-Language

说明:
指定HTTP客户端浏览器用来展示返回信息所优先选择的语言。

实例:
Accept-Language: zh-cn,zh;q=0.5 ,这里默认为中文。

  1. Accept-Encoding

说明:
指定客户端浏览器可以支持的web服务器返回内容压缩编码类型。表示允许服务器在将输出内容发送到客户端以前进行压缩,以节约带宽。而这里设置的就是客户端浏览器所能够支持的返回压缩格式。

实例:
Accept-Encoding: gzip,deflate

备注: 其实在百度很多产品线中,apache在给客户端返回页面数据之前,将数据以gzip格式进行压缩。

另外有关deflate压缩介绍:http://man.chinaunix.net/newsoft/ApacheMenual_CN_2.2new/mod/mod_deflate.html

  1. Accept-Charset

说明:
浏览器可以接受的字符编码集。

实例:Accept-Charset: gb2312,utf-8;q=0.7,*;q=0.7

  1. Content-Type

说明: 显示此HTTP请求提交的内容类型。一般只有post提交时才需要设置该属性。

实例: Content-type: application/x-www-form-urlencoded;charset:UTF-8

有关Content-Type属性值可以如下两种编码类型:

  • “application/x-www-form-urlencoded”: 表单数据向服务器提交时所采用的编码类型,默认的缺省值就是“application/x-www-form-urlencoded”。 然而,在向服务器发送大量的文本、包含非ASCII字符的文本或二进制数据时这种编码方式效率很低。
  • “multipart/form-data”: 在文件上载时,所使用的编码类型应当是“multipart/form-data”,它既可以发送文本数据,也支持二进制数据上载。

当提交为表单数据时,可以使用“application/x-www-form-urlencoded”;当提交的是文件时,就需要使用“multipart/form-data”编码类型。

在Content-Type属性当中还是指定提交内容的charset字符编码。一般不进行设置,它只是告诉web服务器post提交的数据采用的何种字符编码。 一般在开发过程,是由前端工程与后端UI工程师商量好使用什么字符编码格式来post提交的,然后后端ui工程师按照固定的字符编码来解析提交的数据。所以这里设置的charset没有多大作用。

  1. Connection

说明: 表示是否需要持久连接。如果web服务器端看到这里的值为“Keep-Alive”,或者看到请求使用的是HTTP 1.1(HTTP 1.1默认进行持久连接),它就可以利用持久连接的优点,当页面包含多个元素时(例如Applet,图片),显著地减少下载所需要的时间。要实现这一点, web服务器需要在返回给客户端HTTP头信息中发送一个Content-Length(返回信息正文的长度)头,最简单的实现方法是:先把内容写入ByteArrayOutputStream,然 后在正式写出内容之前计算它的大小。

实例: Connection: keep-alive

  1. Keep-Alive

说明:显示此HTTP连接的Keep-Alive时间。使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。

以前HTTP请求是一站式连接,从HTTP/1.1协议之后,就有了长连接,即在规定的Keep-Alive时间内,连接是不会断开的。

实例: Keep-Alive: 300

  1. cookie

说明:HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。

  1. Referer

说明:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面

服务器端返回HTTP头部信息

简单示例:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 OK
Content-Type text/plain
Content-Length 8
Last-Modified Mon, 15 May 2017 18:04:40 GMT
ETag "ae780585f49b94ce1444eb7d28906123"
Accept-Ranges bytes
Server AmazonS3
X-Amz-Cf-Id jodC9tJqDu4my9HVAend7hMtgKNcrlV4SUHJOKveUP-hHSIJ6ewkig==
Cache-Control no-cache, no-store, must-revalidate
Date Tue, 17 Jul 2018 02:28:52 GMT
Proxy-Connection Keep-alive

  1. Content-Length

说明:表示web服务器返回消息正文的长度

  1. Content-Type:

说明:返回数据的类型(例如text/html文本类型)和字符编码格式。

实例: Content-Type: text/html;charset=utf-8

  1. Date
    说明:显示当前的时间

本文根据RFC2616(HTTP/1.1规范),参考

http://www.w3.org/Protocols/rfc2068/rfc2068

http://www.w3.org/Protocols/rfc2616/rfc2616

http://www.ietf.org/rfc/rfc3229.txt

https://blog.csdn.net/u012359618/article/details/50240617

https://www.cnblogs.com/wenqiang/p/5698772.html

通常HTTP消息包括客户机向服务器的请求消息和服务器向客户机的响应消息。这两种类型的消息由一个起始行,一个或者多个头域,一个只是头域结束的空行和可 选的消息体组成。HTTP的头域包括通用头,请求头,响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成。域名是大小写无关的,域 值前可以添加任何数量的空格符,头域可以被扩展为多行,在每行开始处,使用至少一个空格或制表符。

okhttp使用记录

发表于 2018-07-17 | 分类于 Android
  1. Applicaion Interceptor和NetworkInterceptor区别
    Application interceptors:
  • Don’t need to worry about intermediate responses like redirects and retries.(不必担心重定向和重试等中间响应,因为它处于第一个拦截器,会获取到最终的响应 response 。)
  • Are always invoked once, even if the HTTP response is served from the cache.(即使从缓存提供HTTP响应,也始终调用一次)
  • Observe the application’s original intent. Unconcerned with OkHttp-injected headers like If-None-Match.(观察应用程序的原始意图。 不去关注OkHttp注入的头信息,如If-None-Match。)
  • Permitted to short-circuit and not call Chain.proceed().(允许短路而不调用Chain.proceed(),因为是第一个被执行的拦截器,因此它有权决定了是否要调用其他拦截,也就是 Chain.proceed() 方法是否要被执行。)
  • Permitted to retry and make multiple calls to Chain.proceed().(允许重试并多次调用Chain.proceed(),因为是第一个被执行的拦截器,因此它有可以多次调用 Chain.proceed() 方法,其实也就是相当与重新请求的作用了。)

Network Interceptors

  • Able to operate on intermediate responses like redirects and retries.(能够对重定向和重试等中间响应进行操作,因为 NetworkInterceptor 是排在第 6 个拦截器中,因此可以操作经过 RetryAndFollowup 进行失败重试或者重定向之后得到的resposne)
  • Not invoked for cached responses that short-circuit the network.(未调用使网络短路的缓存响应,对于从缓存获取的 response 则不会去触发 NetworkInterceptor 。因为响应直接从 CacheInterceptor 返回了)
  • Observe the data just as it will be transmitted over the network.(观察数据,就像它将通过网络传输一样)
  • Access to the Connection that carries the request.(访问带有请求的Connection)

参考地址:
[1]. 官网 https://github.com/square/okhttp/wiki/Interceptors
[2]. 简书 https://www.jianshu.com/p/d04b463806c8

Android开发记录

发表于 2018-07-13 | 分类于 Android

技巧

Android studio中如何修改运行环境中最低版本和目标版本

最近发现使用Android Studio创建的项目中,最低版本和目标版本已经不在AndroidManifest.xml中显示了。那我们应该去那里需改呢?

原来放到了File -> Project Structure中了。直接上图吧,一目了然。
修改最低版本和目标版本

参考地址:

  • https://blog.csdn.net/kingroc/article/details/50947143

编写第一个Flutter应用

发表于 2018-07-12 | 分类于 Flutter

Animated GIF of the app that you will be building.
这篇教程将指导你创建第一个 Flutter 应用程序。如果你熟悉面向对象程序设计和基本的编程概念(如变量,循环和条件),即可完成本教程。无需具有使用 Dart 语言或移动编程的相关经验。

  • 第1步:创建初始Flutter应用
  • 第2步:使用外部 package
  • 第3步:添加有状态的widget
  • 第4步:创建一个无限滚动的 ListView
  • 第5步:添加可交互性
  • 第6步:导航到新页面
  • 第7步:使用主题更改UI
  • 干得漂亮!

    你需要做什么

完成一个简单的移动应用程序,为一家创业公司进行命名推荐。用户可以选择和取消选择名称,并保存最好的一个。代码一次生成十个名称。当用户滚动时,会新生成一批名称。用户可以点击应用栏右上方的列表图标,跳转到仅显示已被收藏的名称的列表页面。

这个GIF动画可以显示出该应用是如何工作的。


你将会学到:
- Flutter 应用程序的基本结构 。
- 查找和使用 package 来扩展功能。
- 使用热加载加快开发效率。
- 如何实现一个有状态的 widget 。
- 如何创建一个无限长度的延迟加载列表。
- 如何创建并导航到第二个页面。
- 如何使用主题更改应用程序的外观。

你将会用到:

需要安装以下工具:

- Flutter SDK
Flutter SDK 包括 Flutter 的引擎,框架,控件,工具和 Dart SDK 。这个 codelab 需要 v0.1.4 或更高版本。

- Android Studio IDE
这个 codelab 基于 Android Studio IDE 构建,但也可以使用其他 IDE ,或者从命令行运行。

- 安装 IDE 插件
插件必须为您的编译器单独安装 Flutter 和 Dart 插件。除了Android Studio,Flutter和Dart插件也可用于 VS Code 和 IntelliJ IDE。

有关如何设置环境的信息,请参阅 Flutter安装和配置。

第1步:创建初始Flutter应用

使用第一个入门 Flutter 应用中的说明创建一个简单的模板化 Flutter 应用。将项目命名为 startup_namer(而不是myapp)。你将修改这个应用并最终完成它。

在这个 codelab 中,将主要编辑 Dart 代码所在的 lib / main.dart。


小贴士: 将代码粘贴到应用程序中时,缩进可能会错位。你可以使用 Flutter 工具自动修复此问题:

1. Android Studio / IntelliJ IDEA: 右键单击 dart 代码,然后选择 Reformat Code with dartfmt。
2. VS Code: 单击右键,选择 Format Document.
3. 命令行: 运行 flutter format .
  1. 替换 lib / main.dart 。
    删除 lib / main.dart 中的所有代码。替换为下面的代码,它在屏幕中心显示 “Hello World” 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import 'package:flutter/material.dart';

    void main() => runApp(new MyApp());

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return new MaterialApp(
    title: 'Welcome to Flutter',
    home: new Scaffold(
    appBar: new AppBar(
    title: new Text('Welcome to Flutter'),
    ),
    body: new Center(
    child: new Text('Hello World'),
    ),
    ),
    );
    }
    }

运行应用程序。现在应该可以看到下面的页面。
screenshot of hello world app

小结

  • 本示例创建了一个 Material app 。 Material 设计语言是一套移动设备和网页上的视觉设计标准。Flutter 提供了一套丰富的 Material Widgets 。

  • main 方法采用了 fat arrow (=>) 表示法,这是一种用于单行函数或方法的简写。

  • 该 app 继承了使它本身成为一个 widget 的 StatelessWidget 类。在 Flutter 中,大多数时候一切都可以看作 widget , 包括 alignment,padding 和 layout 。

  • Material 库中的 Scaffold widget 提供了默认的应用栏 (app bar),标题和构成主页面 widget 树结构的 body 属性。 widget 的子树可以非常复杂。

  • widget 的主要工作是提供一个build()方法,描述如何根据其他更低层级的 widget,来对这个 widget 进行展示。

  • 本示例的 widget 树由包含了 Text child widget 的 Center widget 组成。Center widget 可将它的所有子树对齐到屏幕中心。

第2步:使用外部 package

在这一步,将开始使用名为 english_words 的开源软件包 ,其中包含数千个最常用的英文单词以及一些实用功能。

可以在 pub.dartlang.org 上找到 english_words 软件包以及其他许多开源软件包。

  1. pubspec 文件管理着 Flutter 应用程序的静态资源文件(assets)。 在 pubspec.yaml 文件中, 将 english_words(3.1.0或更高版本)添加到依赖列表。新的一行高亮如下:

    1
    2
    3
    4
    5
    6
    dependencies:
    flutter:
    sdk: flutter

    cupertino_icons: ^0.1.0
    english_words: ^3.1.0
  2. 在 Android Studio 的 editor 视图中查看 pubspec 时, 点击右上角的 Packages get ,将把 package 拉取到项目中。现在应该可以在控制台中看到以下内容:

    1
    2
    3
    flutter packages get
    Running "flutter packages get" in startup_namer...
    Process finished with exit code 0
  3. 在 lib/main.dart 中,为 english_words 添加导入,如高亮的行所示:

    1
    2
    import 'package:flutter/material.dart';
    import 'package:english_words/english_words.dart';

    在键入该行时, Android Studio 会提供有关库导入的建议。然后将导入字符串显示为灰色,让你知道导入的库尚未使用(到目前为止)。

  4. 改用英文单词的 package 来生成文本,而不是字符串 “Hello World” 。


    小贴士: “Pascal case”(也被称为“大骆驼拼写法”),意味着字符串中的每个单词(包括第一个单词)都以大写字母开头。所以,“uppercamelcase” 变成 “UpperCamelCase” 。

对代码进行以下更改,如!!!所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random(); //!!!!
return new MaterialApp(
title: 'Welcome to Flutter',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Welcome to Flutter'),
),
body: new Center(
//child: new Text('Hello World'), // Replace the highlighted text...
child: new Text(wordPair.asPascalCase), // With this highlighted text. !!!
),
),
);
}
}

  1. 如果应用正在运行,请使用 Flutter Hot Reload (热重载)按钮 (lightning bolt icon)更新应用程序。每次单击按钮或保存项目时,都会看到随机的词组文本,这是因为配对的词组是在 build 方法内部生成的,每次应用需要渲染时,或在 Flutter Inspector 中切换 Platform 时都会运行

screenshot at completion of second step

有问题吗?

如果应用程序运行不正常,请检查拼写错误。如有需要,可使用以下链接中的代码使项目恢复正常

  • pubspec.yaml (pubspec.yaml文件不会再更改。)
  • lib/main.dart

第3步:添加有状态的widget

Stateless widgets 是不可改变的,这意味着它们的属性不能改变——所有的值都是 final 的。

Statefulwidget 在其 生命周期 保持的状态可能会变化,实现一个有状态的 widget 至少需要两个类:StatefulWidgets类和State类,其中StatefulWidgets类创建了一个State类的实例。StatefulWidget类本身是不可变的,但State类可存在于Widget的整个生命周期中。

在这一步,将添加一个有状态的 RandomWords widget ,它可以创建其 State 类 RandomWordsState 。 State 类会为 widget 保存被推荐和被收藏的词组。

  1. 将有状态的 RandomWords widget 添加到 main.dart 。它可以在 MyApp 类之外的任何位置使用,但当前将把它放在文件底部。 RandomWords widget 除了创建 State 类之外几乎没有任何其他代码:

    1
    2
    3
    4
    class RandomWords extends StatefulWidget {
    @override
    createState() => new RandomWordsState();
    }
  2. 添加 RandomWordsState 类。这个类保存了 RandomWords widget 的状态,该应用程序的大部分代码都放在该类中。这个类将保存随着用户的滑动操作而生成的无限增长的词组,以及保存用户收藏的词组,用户通过触发心形图标来添加或删除收藏的词组列表。

你可以一点点建立这个类。首先,通过以下!!!代码,创建一个最简的类:

1
2
3
4
5
6
  class RandomWordsState extends State<RandomWords> { //!!!
}
```

3. 添加这个 state 类之后,IDE 会提示该类缺少 build 方法。接下来,需要添加一个基本的 build 方法,并将生成单词的代码行从 MyApp 类移动到 RandomWordsState 类的 build 方法中,生成词组。
将 build 方法添加到 RandomWordState 中,如!!!代码所示:

class RandomWordsState extends State {
@override //-!!!
Widget build(BuildContext context) {
final wordPair = new WordPair.random();
return new Text(wordPair.asPascalCase);
} //-!!!
}

1
4. 根据!!!部分的更改,从 MyApp 中删除生成单词的代码:

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random(); // !!! Delete this line

  return new MaterialApp(
    title: 'Welcome to Flutter',
    home: new Scaffold(
      appBar: new AppBar(
        title: new Text('Welcome to Flutter'),
     ),
      body: new Center(
        //child: new Text(wordPair.asPascalCase), // !!!Change the highlighted text to...
        child: new RandomWords(), // !!!... this highlighted text
      ),
    ),
  );
}

}

1
重启应用。如果尝试热重载,则可能会看到警告:

Reloading…
Not all changed program elements ran during view reassembly; consider
restarting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这可能是一个误报,但可以考虑重启应用,以确保更改能正常反映在应用的 UI 界面中。

应用程序应该像之前一样运行,每次热重载或保存应用程序时都会显示一个词组。

[screenshot at completion of third step](step3-screenshot.png)
#### 有问题吗? ####

如果应用运行不正常,则可以使用以下链接中的代码使其恢复正常

- [lib/main.dart](lib/main.dart)

### 第4步:创建一个无限滚动的 ListView ###
在这一步,可以扩展 RandomWordsState 类,生成并展示词组列表。当用户滑动列表,ListView widget 中显示的列表将无限增长。 ListView 的 builder 工厂构造函数允许按需建立一个延迟加载的列表 view 。

1. \_suggestions 变量向 RandomWordsState 类中添加一个数组列表,用来保存推荐词组。 该变量以下划线(\_)开头,在 Dart 语言中使用下划线前缀表示强制私有。
此外,添加一个 biggerFont 变量来增大字体。

class RandomWordsState extends State {
final _suggestions = [];

final _biggerFont = const TextStyle(fontSize: 18.0);
…
}

1
2
3
2. 向 RandomWordsState 类添加一个 \_buildSuggestions() 函数,用于构建一个显示词组的 ListView 。   
ListView 类提供了一个 itemBuilder 属性,这是一个工厂 builder 并作为匿名函数进行回调。它有两个传入参数— BuildContext 上下文和行迭代器 i 。对于每个推荐词组都会执行一次函数调用,迭代器从 0 开始,每调用一次函数就累加 1 。这个模块允许推荐列表在用户滑动时无限增长。
添加如下代码行:

class RandomWordsState extends State {
…
Widget _buildSuggestions() {
return new ListView.builder(
padding: const EdgeInsets.all(16.0),
// The itemBuilder callback is called once per suggested word pairing,
// and places each suggestion into a ListTile row.
// For even rows, the function adds a ListTile row for the word pairing.
// For odd rows, the function adds a Divider widget to visually
// separate the entries. Note that the divider may be difficult
// to see on smaller devices.
itemBuilder: (context, i) {
// Add a one-pixel-high divider widget before each row in theListView.
if (i.isOdd) return new Divider();

      // The syntax "i ~/ 2" divides i by 2 and returns an integer result.
      // For example: 1, 2, 3, 4, 5 becomes 0, 1, 1, 2, 2.
      // This calculates the actual number of word pairings in the ListView,
      // minus the divider widgets.
      final index = i ~/ 2;
      // If you've reached the end of the available word pairings...
      if (index >= _suggestions.length) {
        // ...then generate 10 more and add them to the suggestions list.
        _suggestions.addAll(generateWordPairs().take(10));
      }
      return _buildRow(_suggestions[index]);
    }
  );
}

}

1
2
3.  函数都调用一次 \_buildRow 函数。这个函数每次会在一个 ListTile widget 中展示一条新词组,这将在下一步操作中,使一行数据更有表现力。    
添加 \_buildRow 函数到 RandomWordsState 类中:

class RandomWordsState extends State {
…

Widget _buildRow(WordPair pair) {
  return new ListTile(
    title: new Text(
      pair.asPascalCase,
      style: _biggerFont,
    ),
  );
}

}

1
2

4. 更新 RandomWordsState 类的 build 方法来使用 \_buildSuggestions() 函数,而不是直接调用单词生成库。对部分进行修改:

class RandomWordsState extends State {
…
@override
Widget build(BuildContext context) {
final wordPair = new WordPair.random(); // Delete these two lines.
Return new Text(wordPair.asPascalCase);
return new Scaffold (
appBar: new AppBar(
title: new Text(‘Startup Name Generator’),
),
body: _buildSuggestions(),
);
}
…
}

1
2
3
5. 更新 MyApp 类的 build 方法。从 MyApp 中删除 Scaffold 和 AppBar 实例。这些将由 RandomWordsState 类进行统一管理,这样在下一步操作中,可以使用户从一个页面导航到另一页面时,更方便的更改应用栏中的页面名称。

用下面高亮的 build 方法替换原始代码:

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: ‘Startup Name Generator’,
home: new RandomWords(),
);
}
}

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

重启应用程序,将可以看到一个词组清单。尽量向下滑动,将继续看到新的词组。

[screenshot at completion of fourth step](step4-screenshot.png)
有问题吗?

如果应用运行不正常,则可以使用以下链接中的代码使其恢复正常

- [lib/main.dart](https://gist.githubusercontent.com/Sfshaza/d6f9460a04d3a429eb6ac0b0f07da564/raw/34fe240f4122435c871bb737708ee0357741801c/main.dart)


### 第5步:添加可交互性 ###
在这一步,将为每一行添加可点击的心形图标。当用户点击列表中的条目,切换其“收藏”状态,词组就会添加到收藏栏,或从已保存词组的收藏栏中删除。

1. 添加一个 Set 集合 \_saved 到 RandomWordsState 类。保存用户收藏的词组。Set 集合比 List 更适用于此,因为它不允许重复元素。

class RandomWordsState extends State {
final _suggestions = [];

final _saved = new Set();

final _biggerFont = const TextStyle(fontSize: 18.0);
…
}

1
2. 在 \_buildRow 函数中,添加 alreadySaved 标志检查来确保一个词组还没有被添加到收藏。

Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
…
}

1
2
3
3. 在 \_buildRow() 的 ListTiles widget 中,添加一个心形图标来使用收藏功能,随后将添加与心形图标进行交互的功能。  

添加以下高亮代码行:

Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
);
}

1
2
3
4
5
4. 重启应用。现在应该可以在每一行看到心形图标,但还没有交互功能。

5. 在 \_buildRow 函数中使心形可点击。如果词条已经被加入收藏,再次点击它将从收藏中删除。当心形图标被点击,函数将调用 setState() 通知应用框架state已经改变。

添加高亮代码行:

Widget _buildRow(WordPair pair) {
final alreadySaved = _saved.contains(pair);
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
trailing: new Icon(
alreadySaved ? Icons.favorite : Icons.favorite_border,
color: alreadySaved ? Colors.red : null,
),
onTap: () {
setState(() {
if (alreadySaved) {
_saved.remove(pair);
} else {
_saved.add(pair);
}
});
},
);
}

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


<table><tr><td bgcolor=#f0f0f0>
小贴士: 在 Flutter 的响应式风格框架中,调用 setState() ,将为 State 对象触发 build() 方法的调用,从而实现对UI的更新。
</td></tr></table>

热重载应用。可以点击任意一行来收藏或取消收藏条目。 请注意,点击一行可以产生从心形图标展开的泼墨动画效果。

[screenshot at completion of 5th step](step5-screenshot.png)
有问题吗?

如果应用运行不正常,则可以使用以下链接中的代码使其恢复正常。

- [lib/main.dart](https://gist.githubusercontent.com/Sfshaza/936ce0059029a8c6e88aaa826a3789cd/raw/a3065d5c681a81eff32f75a9cd5f4d9a5b24f9ff/main.dart)


### 第6步:导航到新页面 ###
在这一步,将添加一个显示收藏夹的新页面(在 Flutter 中称为 route(路由))。你将学习如何在主路由和新路由之间导航。

在 Flutter 中, Navigator 管理着包含了应用程序所有路由的一个堆栈。将一个路由push到 Navigator 的堆栈,将显示更新为新页面路由。将一个路由 pull 出 Navigator 的堆栈,显示将返回到前一个页面路由。

1. 在 RandomWordsState 类的 build 方法中,向 AppBar 添加一个列表图标。当用户点击列表图标时,包含了已收藏条目的新路由将被 push 到 Navigator 堆栈并显示新页面。
<table><tr><td bgcolor=#f1f1f1>
小贴士: 某些 widget 属性使用独立 widget(child) 和其他属性例如 action 组成一个子 widget 数组(children),用方括号([])表示。
</td></tr></table>
将该图标及其相应的 action 操作添加到 build 方法中:

class RandomWordsState extends State {
…
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(‘Startup Name Generator’),
actions: [
new IconButton(icon: new Icon(Icons.list), onPressed: _pushSaved),
],
),
body: _buildSuggestions(),
);
}
…
}

1
2. 向 RandomWordsState 类添加一个 \_pushSaved() 函数。

class RandomWordsState extends State {
…
void _pushSaved() {
}
}

1
2
3
4
5
6
7
重新加载应用程序。列表图标将出现在应用栏中。点击它不会有任何响应,因为 \_pushSaved 这个函数还未实现功能。

3. 当用户点击应用栏中的列表图标时,将建立一个新路由并 push 到 Navigator 的路由堆栈中,这个操作将改变界面显示,展示新的路由页面。

新页面的内容使用匿名函数在 MaterialPageRoute widget的builder属性中创建。

将函数调用添加到 Navigator.push 中作为参数,如高亮代码所示,将路由 push 到 Navigator 的堆栈中。

void _pushSaved() {
Navigator.of(context).push(
);
}

1
4. 添加 MaterialPageRoute widget 及其 builder 属性。先添加生成 ListTile widget 的代码。其中 ListTile 的 divideTiles() 方法为每个 ListTile widget 之间添加水平间距。divided变量保存最终生成的所有行,并用 toList() 函数转换为列表。

void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();
},
),
);
}

1
2
3
5. builder 属性返回一个 Scaffold widget ,其中包含了应用栏标题名为 “Saved Suggestions” 的新路由页面。新页面的body属性由包含多个 ListTile widget 的 ListView 组成。

添加如下代码:

void _pushSaved() {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) {
final tiles = _saved.map(
(pair) {
return new ListTile(
title: new Text(
pair.asPascalCase,
style: _biggerFont,
),
);
},
);
final divided = ListTile
.divideTiles(
context: context,
tiles: tiles,
)
.toList();

    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Saved Suggestions'),
      ),
      body: new ListView(children: divided),
    );
  },
),

);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
6. 热重载应用程序。对一些条目点击收藏,然后点击应用栏右侧的列表图标。显示出包含收藏夹列表的新页面。注意,Navigator 会在应用栏左侧添加一个“返回”按钮。不必再显式实现 Navigator.pop 。点击返回按钮会返回到主页面。

[screenshot at completion of 6th stepsecond route](step6a-screenshot.png)
有问题吗?

如果应用运行不正常,则可以使用以下链接中的代码使其恢复正常。

- [lib/main.dart](https://gist.github.com/Sfshaza/bc5547e112e4dc3a1aa87afdf917caeb)

### 第7步:使用主题更改UI ##
在最后一步中,将使用该应用的主题。 theme 控制的是应用程序的观感。可以使用默认主题,该主题取决于使用的模拟器或真机,也可以自定义主题以反映你的品牌。

可以通过配置 ThemeData 类轻松更改应用程序的主题。应用程序目前使用默认主题,现在将更改主要颜色为白色。

1. 将高亮代码添加到 MyApp 类中,可以把应用程序的主题更改为白色:

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: ‘Startup Name Generator’,
theme: new ThemeData(
primaryColor: Colors.white,
),
home: new RandomWords(),
);
}
}
```

  1. 热重载应用程序。请注意,整个背景都是白色的,甚至包括应用栏。

  2. 作为读者的练习,可使用 ThemeData 来改变用户界面的其他方面。 Material 库中的 Colors 类提供了多种可以使用的颜色常量,而热重载使用户界面的修改变得简单快捷。
    screenshot at completion of 7th step
    有问题吗?

如果又不能正常运行了,请使用以下链接中的代码查看最终应用的代码。

  • lib/main.dart

flutter初体验

发表于 2018-07-12 | 分类于 Flutter

在这篇文档中你将了解如何在 Flutter 开发中进行调试和修改:从我们提供的项目模板创建一个 Flutter 应用程序,运行然后学习如何使用热重载来修改程序。

Flutter 是一个扩展性极强的工具集,因此你可以选择你钟爱的开发工具或者平台来写代码,编译以及运行 Flutter 应用程序。

Android Studio

Android Studio: 一个完整的,高度集成的 Flutter 开发编辑器。

创建应用程序

  1. 依次选择 File>New Flutter Project
  2. 选择 Flutter application 作为项目类型,然后点击下一步
  3. 输入项目名称(例如:myapp),然后点击下一步
  4. 点击 Finish
  5. 等待 Android Studio 安装好 SDK 和创建好这个项目。
    以上的命令创建了一个名叫 myapp 的 Flutter 项目,并且放在 ‘myapp’ 文件夹中。这是一个很简单的,基于 Material 组件 的项目。

在这个项目的文件夹中,和项目业务相关的代码都在 lib/main.dart 中。

运行这个程序

  1. 找到 Android Studio 的主工具栏:
    IntelliJ 的主工具栏
  2. 在 target selector 中,选择一个已连接的 Android 设备来运行这个程序。如果列表中没有可用的设备, 那么依次选择 Tools>Android>AVD Manager 创建一个可用的模拟器。了解更多,请查看 管理 AVDs。
  3. 点击工具栏中的 Run icon,或者在菜单栏中一次选择 Run > Run。
  4. 如果一切正常,那么你现在就可以在你的手机或者模拟器上看到程序的起始界面了:
    Android 设备/模拟器上的起始界面

尝试一下热重载

Flutter 提供了一种非常高效的开发方式,叫做 热重载 ,这个功能可以在应用程序运行的状态下替换部分代码,并且运行中的程序不会丢失任何状态。简单的对你的源代码做一些修改,告诉你的 IDE 或者命令行工具,你需要进行热重载,然后你就可以在模拟器或者手机中看到你的修改了。

  1. 把字符串
    ‘You have pushed the button this many times:’修改为
    ‘You have clicked the button this many times:’
  2. 不需要点击 ‘Stop’ 按钮;让应用程序继续运行。
  3. 只需要将代码 全部保存 (cmd-s / ctrl-s),或者点击 热重载 按钮(那个像闪电⚡️一样的图标按钮)你就可以看到你的修改已经被执行了。

你几乎可以立刻就在应用程序里看到你对字符串的修改。

VS Code

VS Code: 包含了运行和调试 Flutter 应用程序的轻量级编辑器。

创建应用程序

  1. 启动 VS Code
  2. 依次执行 View>Command Palette…
  3. 输入 ‘flutter’,选择 ‘Flutter: New Project’ 命令
  4. 输入项目名称(例如:myapp),回车
  5. 找到一个用于保存项目的目录,然后点击蓝色的确认按钮
  6. 项目会自动进行创建,创建完毕之后,main.dart 文件会被自动打开
    以上的命令创建了一个名叫 myapp 的 Flutter 项目,并且放在 ‘myapp’ 文件夹中。这是一个很简单的,基于 Material 组件 的项目。

在这个项目的文件夹中,和项目业务相关的代码都在 lib/main.dart 中。

运行这个程序

  1. 确保在 VS Code 的右下角能看到目标设备的名称
  2. 使用键盘上的 F5 按钮,或者依次执行 Debug>Start Debugging
  3. 等待应用程序启动
  4. 如果一切正常,构建完应用程序之后,你就可以在你的手机或者模拟器上看到应用程序的起始界面了:
    Android 设备/模拟器上的起始界面

尝试一下热重载

Flutter 提供了一种非常高效的开发方式,叫做 热重载 ,这个功能可以在应用程序运行的状态下替换部分代码,并且运行中的程序不会丢失任何状态。简单的对你的源代码做一些修改,告诉你的 IDE 或者命令行工具,你需要进行热重载,然后你就可以在模拟器或者手机中看到你的修改了。

在你最钟爱的 Dart 开发编辑器中打开 lib/main.dart 文件

  1. 把字符串
    ‘You have pushed the button this many times:’修改为
    ‘You have clicked the button this many times:’
  2. 不需要点击 ‘Stop’ 按钮;让应用程序继续运行。
  3. 只需要将代码 全部保存 (cmd-s / ctrl-s),或者点击 热重载 按钮(那个像闪电⚡️一样的图标按钮)你就可以看到你的修改已经被执行了。

你几乎可以立刻就在应用程序里看到你对字符串的修改。

控制台 + 文本编辑器

控制台 + 文本编辑器 你自己选择的代码编辑器加上 Flutter 提供的命令行工具来运行和构建 Flutter 应用程序。

创建应用程序

  1. 使用 flutter create 命令来创建一个新的应用程序:
    1
    2
    $ flutter create myapp
    $ cd myapp

以上的命令创建了一个名叫 myapp 的 Flutter 项目,并且放在 ‘myapp’ 文件夹中。这是一个很简单的,基于 Material 组件 的项目。

在这个项目的文件夹中,和项目业务相关的代码都在 lib/main.dart 中。

运行这个程序

  • 确保 Android 设备当前处于运行状态。如果没有发现在运行的设备,查看 安装 页面。

    1
    $ flutter devices
  • 使用 flutter run 命令来运行程序:

    1
    flutter run
  • 如果一切正常,构建完应用程序之后,你就可以在你的手机或者模拟器上看到应用程序的起始界面了:
    Android 设备/模拟器上的起始界面

尝试一下热重载

Flutter 提供了一种非常高效的开发方式,叫做 热重载 ,这个功能可以在应用程序运行的状态下替换部分代码,并且运行中的程序不会丢失任何状态。简单的对你的源代码做一些修改,告诉你的 IDE 或者命令行工具,你需要进行热重载,然后你就可以在模拟器或者手机中看到你的修改了。

在你最钟爱的 Dart 开发编辑器中打开 lib/main.dart 文件

  1. 把字符串
    ‘You have pushed the button this many times:’修改为
    ‘You have clicked the button this many times:’
  2. 不需要点击 ‘Stop’ 按钮;让应用程序继续运行。
  3. 只需要将代码 全部保存 (cmd-s / ctrl-s),或者点击 热重载 按钮(那个像闪电⚡️一样的图标按钮)你就可以看到你的修改已经被执行了。

你几乎可以立刻就在应用程序里看到你对字符串的修改。

flutter-开发记录(错误或技巧)

发表于 2018-07-12 | 分类于 Flutter

错误

  1. flutter doctor 显示:
    1
    Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses

方案一:
根据提示运行flutter doctor –android-licenses 一路y就好了。

  1. Row中包含TextField时在弹出软键盘时会overflow(TextField inside of Row causes layout exception: Unable to calculate size)

解决:在TextField外层在包一个Flexible组件即可;

参考:https://stackoverflow.com/questions/45986093/textfield-inside-of-row-causes-layout-exception-unable-to-calculate-size (译文:(假设您正在使用Row,因为您希望将来在TextField旁边放置其他小部件。)Row小部件想要确定其非灵活子节点的内在大小,以便它知道它为灵活子节点留下了多少空间。 但是,TextField没有固有宽度; 它只知道如何将自身大小调整到其父容器的整个宽度。 尝试将其包装在Flexible或Expanded中,告诉Row您希望TextField占用剩余空间:)

  1. Keyboard overflows TextField creating yellow/black stripes
    文本域获取焦点时,弹出软键盘,导致overflowed错误,
    修正:通过设置Scaffold的属性resizeToAvoidBottomPadding: false即可,

原因:猜测(可能键盘的弹出导致,内容视图的高度+键盘的高度超出了一个屏幕的高度从而产生了overflowed)。

参考网址:https://github.com/flutter/flutter/issues/13339 (译文:当你点击文本字段键盘显示白色内容溢出整个脚手架区域时,是的还有同样的问题。 使用resizeToAvoidBottomPadding它确实有效但我认为禁用此选项键盘不会尊重焦点文本字段,也不会将内容滚动到适当的焦点框
)

  1. 使用cipher时报如下错误:
    1
    The parameter 'm' of the method 'BigIntegerV8::modPowInt' has type bignum::BigIntegerV8, which does not match the corresponding type in the overridden method (dynamic).

这个时我在使用依赖cipher: ^0.7.1时遇到的,后来查看Dart版本,和cipher的发布日志发现,cipher: ^0.7.1只使用与Dart 1,而开发当前用的时Dart 2,因此出现该错误,加密/解密依赖目前开发用的是 crypto.

技巧

!!! 谨记:Flutter 中是基于数据驱动,Java中是基于事件驱动。

  1. Dart中字符串转换为字节数组或列表(How do I convert a UTF-8 String into an array of bytes in Dart?)

代码如下:

1
2
3
import 'dart:convert';
List<int> bytes = utf8.encode("Some data"); //UTF8 is deprecated sind Dart 2. But it was just renamed to utf8: List<int> bytes = utf8.encode("Some data");
print(bytes) //[115, 111, 109, 101, 32, 100, 97, 116, 97]

参考:https://stackoverflow.com/questions/10404147/how-do-i-convert-a-utf-8-string-into-an-array-of-bytes-in-dart

  1. Dart json操作:
    在Dart中json的构建和将对象转换为json字符很简单,麻烦在将json转换为一个对象,关于json的操作依赖于dart:convert;
  • 构建一个json字符串:

    1
    2
    3
    4
    {
    "name": "John Smith",
    "email": "john@example.com"
    }
  • 将对象转换为一个字符串

1
String json = json.encode(user);
  • 序列化json对象:

    • 序列化JSON内联

      1
      2
      3
      Map<String, dynamic> user = json.decode(json);
      print('Howdy, ${user['name']}!');
      print('We sent the verification link to ${user['email']}.');

      通过上面我们知道,json.decode()只返回一个Map ,这意味着我们直到运行时才知道值的类型。 使用这种方法,我们失去了大多数静态类型语言功能:类型安全,自动完成以及最重要的编译时异常。 我们的代码可能会立即变得更容易出错。
      例如,每当我们访问名称或电子邮件字段时,我们都会很快引入拼写错误。 由于我们的整个JSON仅存在于地图结构中,因此我们的编译器不知道这是一个错字。

    • 在模型类中序列化JSON

      我们可以通过引入一个普通的模型类来解决前面提到的问题,我们称之为User。 在User类中,我们有:
      User.fromJson构造函数,用于从地图结构构造新的User实例
      一个toJson方法,它将User实例转换为map。
      这样,调用代码现在可以具有类型安全性,名称和电子邮件字段的自动完成以及编译时异常。 如果我们使用拼写错误或将字段视为int而不是字符串,我们的应用程序甚至不会编译,而不是在运行时崩溃。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class User {
      final String name;
      final String email;

      User(this.name, this.email);

      User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
      email = json['email'];

      Map<String, dynamic> toJson() =>
      {
      'name': name,
      'email': email,
      };
      }

      现在,序列化逻辑的责任在模型本身内部移动。 通过这种新方法,我们可以非常轻松地反序列化用户。

      1
      2
      3
      4
      5
      Map userMap = json.decode(json);
      var user = new User.fromJson(userMap);

      print('Howdy, ${user.name}!');
      print('We sent the verification link to ${user.email}.');

参考地址:

  • https://flutter.io/json/
  1. Flutter中获取设备信息:

依赖于 import ‘dart:io’;和 device_info : ^0.2.0;

1
2
3
4
5
6
7
8
9
10
DeviceInfoPlugin deviceInfo = new DeviceInfoPlugin();
if(Platform.isIOS){
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
print('Running on ${iosInfo.utsname.machine}'); // e.g. "iPod7,1"
//ios相关代码
}else if(Platform.isAndroid){
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
print('Running on ${androidInfo.model}'); // e.g. "Moto G (4)"
//android相关代码
}

参考地址:

  • 官网参考:https://github.com/flutter/plugins/tree/master/packages/device_info
  • https://segmentfault.com/a/1190000014913010?utm_source=index-hottest
  • 网络请求简单示例 https://segmentfault.com/a/1190000013712168
  • 列表操作 list、map、set https://blog.csdn.net/hekaiyou/article/details/51374093
  • 下拉刷新、加载更多 https://www.jianshu.com/p/0a64d84b0937
  • flutter导航 https://blog.csdn.net/hekaiyou/article/details/72853738
  1. Flutter加载本地资源

通过异步加载本地Json资源,需要先在pubspec.yaml文件添加资源文件,然后再通过异步加载资源文件,以下为实例:

1
2
3
4
5
6
7
dependencies:
flutter:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/config.json //资源所在位置

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'dart:convert';
import 'dart:async' show Future;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
return await rootBundle.loadString('assets/config.json');
}
void _loadJson() {
loadAsset().then((value){
JsonDecoder decoder = new JsonDecoder();
List<List<String>> json = decoder.convert(value);
print('姓名:'+json[0][0]+',年龄:'+json[0][1]);
});
}

参考地址:

  • https://blog.csdn.net/hekaiyou/article/details/54602103

flutter-配置编辑器

发表于 2018-07-12 | 分类于 Flutter

你可以使用任意一款文本编辑器结合我们提供的命令行工具来构建 Flutter 应用程序。当然,我们更推荐的是使用我们开发的文本编辑器插件之一,来优化开发的使用体验。安装好编辑器插件之后,你会获得代码自动补全,关键词高亮,组件编辑助手,运行&调试的支持等一系列实用的功能。

请跟随下面的步骤来为你的编辑器添加这些插件,我们支持 Android Studio, IntelliJ 以及 VS Code。如果你使用的是另外的编辑器,也没有问题,跳过这一步,直接进入 下一步:创建并运行你的第一个应用程序。

Android Studio 配置

Android Studio: 一个完整的,高度集成的 Flutter 开发编辑器。

安装 Android Studio

  • Android Studio, 3.0 或者更高的版本。

当然,你也可以选择使用 IntelliJ:

  • IntelliJ IDEA 社区版, 2017.1 或者更高的版本。
  • IntelliJ IDEA 高级版, 2017.1 或者更高的版本。

安装 Flutter 和 Dart 插件

Flutter 的开发支持需要安装 2 个插件:

  • Flutter 插件可以提高 Flutter 在开发过程中的开发效率(运行,调试,热重载等等)。
  • Dart 插件提提升了代码层面的开发效率(在你敲代码的同时进行代码校验,代码自动补全等等)。

安装步骤:

  1. 启动 Android Studio。
  2. 打开插件设置(在 macOS 上路径为
    Preferences>Plugins,在 Windows 和 Linux 上路径为 File>Settings>Plugins)。
  3. 选择 Browse repositories…,找到或者在搜索栏输入 Flutter,然后点击 install。
  4. 当弹出对话框提示要安装 Dart 插件的时候,点击 Yes 接受。
  5. 如果弹出 Restart 需要重启编辑器的时候,点击 Yes 接受。

Visual Studio Code (VS Code) 配置

VS Code: 包含了运行和调试 Flutter 应用程序的轻量级编辑器。

安装 VS Code

  • VS Code,
    1. 20.1 或更高版本。

安装 Dart Code 插件

  1. 启动 VS Code
  2. 依次执行 View>Command Palette…
  3. 在扩展插件安装面板的输入框输入 dart code 关键词,在显示的列表中选择 ‘Dart Code’ 插件,然后点击 Install
  4. 点击 ‘OK’ 重新加载 VS Code

使用 Flutter Doctor 来验证你的配置

  1. 依次执行 View>Command Palette…
  2. 输入 ‘doctor’, 然后选择 ‘Flutter: Run Flutter Doctor’ 命令
  3. 在日志打印窗口的 ‘OUTPUT’ 标签中查看打印出的日志信息,看看有没有报错
123…6

CallteFoot

The blog from a Android coder

56 日志
23 分类
71 标签
© 2020 CallteFoot
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4