乙巳🐍年

acc8226 的博客

安卓自定义 ViewGroup 需要注意的地方

至少需要提供 width, 和 height 两个属性

同样地,如果要使用自定义的属性,那么就需要创建自己的名字空间,在 Android Studio 中,第三方的控件都使用如下代码来引入名字空间。

xmlns:custom="http://schemas.android.com/apk/res-auto"

流动布局手写精简版

  • 增加了 ‘center’ 居中等三种排列方式
  • 额外支持 padding 属性
  • layout_newline 属性支持自定义换行(类似’\n’的换行效果)

参考

下一步升级

  • 逆序排列子 view
  • 倒序排列子 view
  • 使每行 view 均分剩余空间
  • 可尝试增加目前流行的 tag 效果

FlowLayout.java

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
package cn.lik.view;

import java.util.ArrayList;
import java.util.List;

import cn.lik.R;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
* 流式布局
*
* @author l1k
*/
public class FlowLayout extends ViewGroup {
private static final String TAG = FlowLayout.class.getSimpleName();
private static final boolean DEBUG = false;

/**
* 存储所有的View,按行记录
*/
private ArrayList<List<View>> mAllViews = new ArrayList<List<View>>();
/**
* 记录每一行的最大高度
*/
private ArrayList<Integer> mLineHeight = new ArrayList<Integer>();


/**
* The current value of the {@link AlignItems}, the default value is
* {@link AlignItems#CENTER}.
*
* @see AlignItems
*/
private int mAlignItems;

public FlowLayout(Context context) {
this(context, null);
}

public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlexLayout, defStyleAttr, 0);
mAlignItems = a.getInt(R.styleable.FlexLayout_alignItems, AlignItems.CENTER);
a.recycle();
}

@AlignItems
public int getAlignItems() {
return mAlignItems;
}

public void setAlignItems(@AlignItems int alignItems) {
if (mAlignItems != alignItems) {
mAlignItems = alignItems;
requestLayout();
}
}

@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}

@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}

@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

/**
* 负责设置子控件的测量模式和大小 根据所有子控件设置自己的宽和高
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获得它的父容器为它设置的测量模式和大小
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

if (DEBUG) {
Log.i(TAG, "onMeasure: sizeWidth = " + sizeWidth + ", sizeHeight = " + sizeHeight);
}

// 如果是warp_content情况下,记录宽和高
int width = 0;
int height = 0;
/**
* 记录每一行的宽度,width不断取最大宽度
*/
int lineWidth = 0;
/**
* 每一行的高度,累加至height
*/
int lineHeight = 0;

int cCount = getChildCount();

final int paddingHorizontal = getPaddingLeft() + getPaddingRight();

// 遍历每个子元素
for (int i = 0; i < cCount; i++) {
final View child = getChildAt(i);
// 测量每一个child的宽和高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 得到child的lp
final LayoutParams lp = (LayoutParams) child.getLayoutParams();

// 当前子空间实际占据的宽度
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
// 当前子空间实际占据的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
/**
* 如果加入当前child,则超出最大宽度,则的到目前最大宽度给width,累加height 然后开启新行
*/
if ((i!=0) && (lp.newline || (lineWidth + childWidth > sizeWidth - paddingHorizontal))) {
width = Math.max(Math.max(lineWidth, childWidth), width);// 取最大
lineWidth = childWidth; // 开启新行,记录宽度
// 叠加当前高度,
height += lineHeight;
// 开启记录下一行的高度
lineHeight = childHeight;// 开启新行, 记录高度
} else { // 否则累加值lineWidth,lineHeight取最大高度
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
// 如果是最后一个,则将当前记录的最大宽度和当前lineWidth做比较
if (i == cCount - 1) {
width = Math.max(width, lineWidth);
height += lineHeight;
}
}

super.setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth
: width + getPaddingLeft() + getPaddingRight(),
(modeHeight == MeasureSpec.EXACTLY) ? sizeHeight
: height + getPaddingTop() + getPaddingBottom());
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mAllViews.clear();
mLineHeight.clear();

int width = getWidth();
int lineWidth = 0;
int lineHeight = 0;

int left = getPaddingLeft();
int top = getPaddingTop();
final int paddingHorizontal = left + getPaddingRight();

// 存储每一行所有的childView
List<View> lineViews = new ArrayList<View>();
int cCount = getChildCount();
// 遍历所有的孩子
for (int i = 0; i < cCount; i++) {
final View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
// 当前子空间实际占据的高度
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;

// 如果已经需要换行
if ((i!=0) && (lp.newline || (childWidth + lineWidth > width - paddingHorizontal))) {
// 记录这一行所有的View以及最大高度
mLineHeight.add(lineHeight);
// 将当前行的childView保存,然后开启新的ArrayList保存下一行的childView
mAllViews.add(lineViews);

lineWidth = 0;// 重置宽
lineHeight = 0;//重置高
lineViews = new ArrayList<View>();
}
/**
* 如果不需要换行,则累加
*/
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
lineViews.add(child);
}
// 记录最后一行
mLineHeight.add(lineHeight);
mAllViews.add(lineViews);

// 得到总行数
int lineNums = mAllViews.size();
for (int i = 0; i < lineNums; i++) {
// 每一行的所有的views
lineViews = mAllViews.get(i);
// 当前行的最大高度
lineHeight = mLineHeight.get(i);

if (DEBUG) {
Log.e(TAG, "第" + i + "行 :" + lineViews.size() + " , " + lineViews);
Log.e(TAG, "第" + i + "行, :" + lineHeight);
}

// 遍历当前行所有的View
for (int j = 0; j < lineViews.size(); j++) {
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

// 计算childView的left,top,right,bottom
int lc = 0;
int tc = 0;
int rc = 0;
int bc = 0;

switch (mAlignItems) {
case AlignItems.FLEX_START:
lc = left + lp.leftMargin;
tc = top + lp.topMargin;
rc = lc + child.getMeasuredWidth();
bc = tc + child.getMeasuredHeight();
break;
case AlignItems.FLEX_END:
lc = left + lp.leftMargin;
tc = top + lineHeight - child.getMeasuredHeight() - lp.bottomMargin;
rc = lc + child.getMeasuredWidth();
bc = tc + child.getMeasuredHeight();
break;
case AlignItems.CENTER:
lc = left + lp.leftMargin;
tc = top + lp.topMargin
+ (lineHeight - lp.topMargin - child.getMeasuredHeight() - lp.bottomMargin) / 2;
rc = lc + child.getMeasuredWidth();
bc = tc + child.getMeasuredHeight();
break;
default:
throw new IllegalStateException("Invalid alignItems is set: " + mAlignItems);
}

child.layout(lc, tc, rc, bc);

left += child.getMeasuredWidth() + lp.rightMargin + lp.leftMargin;
}
left = getPaddingLeft();
top += lineHeight;
}

}

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
/**
* need newline or not
*/
public boolean newline = false;

public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FlexLayout_Layout);
newline = a.getBoolean(R.styleable.FlexLayout_Layout_layout_newline, false);
a.recycle();
}

public LayoutParams(ViewGroup.LayoutParams lp) {
super(lp);
}

public LayoutParams(ViewGroup.MarginLayoutParams lp) {
super(lp);
}

/**
* Copy constructor. Clones the width, height, margin values, newline
*
* @param lp The layout params to copy from.
*/
public LayoutParams(LayoutParams lp) {
super(lp);
this.newline = lp.newline;
}

/**
* Copy constructor. Clones the width, height
*
* @param lp The layout params to copy from.
*/
public LayoutParams(int width, int height) {
super(width, height);
}

/**
* Copy constructor. Clones the width, height, newline
*
* @param lp The layout params to copy from.
*/
public LayoutParams(int width, int height, boolean newline) {
super(width, height);
this.newline = newline;
}

}

}

AlignItems.java

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
/*
* Copyright 2016 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cn.lik.view.flexbox;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** This attribute controls the alignment along the cross axis. */
@Retention(RetentionPolicy.SOURCE)
public @interface AlignItems {

/** Flex item's edge is placed on the cross start line. */
int FLEX_START = 0;

/** Flex item's edge is placed on the cross end line. */
int FLEX_END = 1;

/** Flex item's edge is centered along the cross axis. */
int CENTER = 2;
}

res/values/attrs.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlexLayout">
<attr name="alignItems">
<enum name="flex_start" value="0" />
<enum name="flex_end" value="1" />
<enum name="center" value="2" />
</attr>
</declare-styleable>
<declare-styleable name="FlexLayout_Layout">
<attr name="layout_newline" format="boolean" />
</declare-styleable>
</resources>

官网

下载地址 Eclipse Packages | The Eclipse Foundation - home to a global community, the Eclipse IDE, Jakarta EE and over 350 open source projects… https://www.eclipse.org/downloads/packages/

使用前基本配置

将工作区 Text 文件编码改为 UTF-8。

默认编辑区域的字体太小,优先推荐调节为 14 或者 16 号。

阅读全文 »

型号 分辨率 尺寸 densityDpi density 屏幕级别 逻辑分辨率 屏幕比例
iPhone6 1334*750像素 4.7" 326(≈320) 2(320/160) xhdpi 667*375 16:9
Nexus5 1920*1080像素 5" 445(480) 3 xxhdpi 640*360 同上
标准 5 寸手机 1280*720 像素 5" 294(320) 2 xhdpi 640*360 同上
魅族 note3 1920*1080 像素 5.5" 401(480) 3 xxhdpi 640*360 同上
华为 Nexus 6P 2560x1440 像素 5.7" 515(640) 4 xxxhdpi 640*360 同上
华为 Mate 8 1920x1080 像素 6" 367(480) 3 xxhdpi 640*360 同上
小米 MAX 1920*1080像素 6.44" 342(441) 2.75 xxhdpi 698*392 同上
华为 M2 PLE-703L 1920*1200 7" 323(400) 2.5 xxhdpi 768*480 16:10
华为 M3 CPN-W09 1920*1200 8" 283(360) 2.25 xxhdpi 853*533 同上
华为 T1-821W 1280*800 8" 189(213) 1.33 hdpi 960*600 同上
NCI 定制 T106 1920*1200 8" 283(320) 2 xhdpi 同上 同上
华为 M3 BTV-W09 2560*1600 8.4" 359(400) 2.5 xxhdpi 1024*640 同上
小米平板1~3代 2048*1536 8" 324(320) 2 xhdpi 1024*768 4:3
三星 Galaxy Tab S2 2048x1536 9.7" 264(320) 2 同上 同上 同上
NCI定制T101 1280*800 10.1" 150(160) 1 mdpi 1280*800 16:10
华为 FDR-A01w 1920*1200 10.1" 224(240) 1.5 hdpi 同上 同上

sw600 系列:
同华为 T1-821W: 华为 T1-823L
NCI定制T106: 华为 T1-801W / 华为M2-803L / JDN-W09 /

sw768系列:
同三星Galaxy Tab S2 : 华硕 ZenPad 3S

总结:

  1. 一般而言屏幕尺寸越大, 逻辑分辨率越大
  2. 手机屏目前标准都是 1920 * 1080 像素, xxhdpi 3倍关系, 逻辑分辨率为 640dp * 360dp, 比例为 16:9
  3. Pad 屏幕大多数最小宽度 600dp 以上, 目前看到的最小数据为为 480dp

附录

iPhone 界面设计规范

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
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.TextView;

// 版权声明:https://blog.csdn.net/lfdfhl/article/details/52735103
public class SupportMultipleScreensUtil {
public static final int BASE_SCREEN_WIDTH = 1080;
public static float scale = 1.0F;

private SupportMultipleScreensUtil() {

}

public static void init(Context context) {
Resources resources = context.getResources();
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
int widthPixels = displayMetrics.widthPixels;
scale = (float) widthPixels / BASE_SCREEN_WIDTH;
}

public static void scale(View view) {
if (null != view) {
if (view instanceof ViewGroup) {
scaleViewGroup((ViewGroup) view);
} else {
scaleView(view);
}
}
}

public static void scaleViewGroup(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); ++i) {
View view = viewGroup.getChildAt(i);
if (view instanceof ViewGroup) {
scaleViewGroup((ViewGroup) view);
}
scaleView(view);
}
}

public static void scaleView(View view) {
Object isScale = view.getTag(R.id.is_scale_size_tag);
if (!(isScale instanceof Boolean) || !((Boolean) isScale).booleanValue()) {
if (view instanceof TextView) {
scaleTextView((TextView) view);
} else {
scaleViewSize(view);
}
view.setTag(R.id.is_scale_size_tag, Boolean.valueOf(true));
}
}

// 对于TextView,不但要缩放其尺寸,还需要对其字体进行缩放:
private static void scaleTextView(TextView textView) {
if (null != textView) {
scaleViewSize(textView);

Object isScale = textView.getTag(R.id.is_scale_font_tag);
if (!(isScale instanceof Boolean) || !((Boolean) isScale).booleanValue()) {
float size = textView.getTextSize();
size *= scale;
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
}

Drawable[] drawables = textView.getCompoundDrawables();
Drawable leftDrawable = drawables[0];
Drawable topDrawable = drawables[1];
Drawable rightDrawable = drawables[2];
Drawable bottomDrawable = drawables[3];
setTextViewCompoundDrawables(textView, leftDrawable, topDrawable, rightDrawable, bottomDrawable);
int compoundDrawablePadding = getScaleValue(textView.getCompoundDrawablePadding());

textView.setCompoundDrawablePadding(compoundDrawablePadding);
}
}

/**
* 等比例缩放: 对每个View的宽高,padding,margin值都按比例缩 放,并且在缩放后重新设置其布局参数。 博客地址:
*/
private static void scaleViewSize(View view) {
if (null != view) {
int paddingLeft = getScaleValue(view.getPaddingLeft());
int paddingTop = getScaleValue(view.getPaddingTop());
int paddingRight = getScaleValue(view.getPaddingRight());
int paddingBottom = getScaleValue(view.getPaddingBottom());
view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);

LayoutParams layoutParams = view.getLayoutParams();
if (null != layoutParams) {
if (layoutParams.width > 0) {
layoutParams.width = getScaleValue(layoutParams.width);
}

if (layoutParams.height > 0) {
layoutParams.height = getScaleValue(layoutParams.height);
}

if (layoutParams instanceof MarginLayoutParams) {
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
int topMargin = getScaleValue(marginLayoutParams.topMargin);
int leftMargin = getScaleValue(marginLayoutParams.leftMargin);
int bottomMargin = getScaleValue(marginLayoutParams.bottomMargin);
int rightMargin = getScaleValue(marginLayoutParams.rightMargin);
marginLayoutParams.topMargin = topMargin;
marginLayoutParams.leftMargin = leftMargin;
marginLayoutParams.bottomMargin = bottomMargin;
marginLayoutParams.rightMargin = rightMargin;
}
}
view.setLayoutParams(layoutParams);
}
}

private static void setTextViewCompoundDrawables(TextView textView, Drawable leftDrawable, Drawable topDrawable,
Drawable rightDrawable, Drawable bottomDrawable) {
if (null != leftDrawable) {
scaleDrawableBounds(leftDrawable);
}

if (null != rightDrawable) {
scaleDrawableBounds(rightDrawable);
}

if (null != topDrawable) {
scaleDrawableBounds(topDrawable);
}

if (null != bottomDrawable) {
scaleDrawableBounds(bottomDrawable);
}
textView.setCompoundDrawables(leftDrawable, topDrawable, rightDrawable, bottomDrawable);
}

// 考虑到对TextView的CompoundDrawable进行缩放
private static Drawable scaleDrawableBounds(Drawable drawable) {
int right = getScaleValue(drawable.getIntrinsicWidth());
int bottom = getScaleValue(drawable.getIntrinsicHeight());
drawable.setBounds(0, 0, right, bottom);
return drawable;
}

private static int getScaleValue(int value) {
return value <= 4 ? value : (int) Math.ceil((double) (scale * (float) value));
}

}

图简便, 直接贴了代码, R.id.is_scale_size_tagR.id.is_scale_size_tag报错只需要在\res\values下创建ids.xml文件下定义即可

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<item type="id" name="test"/>
</resources>

用法

1
2
3
4
5
6
7
8
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View rootView=findViewById(android.R.id.content);
SupportMultipleScreensUtil.init(getApplication());
SupportMultipleScreensUtil.scale(rootView);
}

总结

  • 切图存放于drawable-nodpi
  • 抛开系统的dpi并且摒弃dp和sp,统一使用px作为尺寸单位
  • 按照给定高分辨率(如1920*1080)切图和布局, 其实只有1080px有参考价值
  • 根据需要, 等比例缩放每个View

目前,xxhdpi分辨率的手机占了主流,所以在该框架中采用了drawable-xxhdpi的切图。倘若以后xxxhdpi分辨率的手机占了主导地位,那么就请UI设计师按照该分辨率切图,我们将其放在drawable-nohdpi中,再修改BASE_SCREEN_WIDTH即可。

文章来源(References)

Android多分辨率适配框架(1)— 核心基础 - CSDN博客

几组概念

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

屏幕尺寸: 按屏幕对角测量的实际物理尺寸。目前市面上说的几英寸是对角线的英寸数
为简便起见,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:

参考

0%