布局適配:
- 避免寫死控件尺寸草巡,使用wrap_content、match_parent
- LinearLayout使用layout_weight
- RelativeLayout使用centerInParent等
- 使用ContraintLayout眠寿,類似RelativeLayout躬翁,比RelativeLayout性能好
- 使用Percent-support-lib,layout_widthPercent="30%"等
圖片資源適配:
- .9圖或則SVG圖實現(xiàn)縮放
- 備用位圖匹配不同分辨率
限定符適配:
- 分辨率限定符drawable-hdpi盯拱、drawable-xdpi盒发、...
- 尺寸限定符layout-small、layout-large(不如在phone和pad上顯示不同的布局)
- 最小寬度限定符values-sw360dp狡逢、values-sw384dp宁舰、...
- 屏幕方向限定符layout-land、layout-port
如果對適配要求比較高奢浑,限定符適配就不能滿足需求明吩,舉個例子,假設我們有這樣的需求:顯示寬度為屏幕一半的一張圖片殷费。
先說下Android布局中單位的基本概念:
px:像素,平常所說的1920×1080就是像素數量低葫,也就是1920px×1080px详羡,代表手機高度上有1920個像素點,寬度上有1080個像素點
dpi:每英寸多少像素嘿悬,也就是說同分辨率的手機也會存在dpi不同的情況
dp:官方敘述為當屏幕每英寸有160個像素時(也就是160dpi)实柠,dp與px等價的。那如果每英寸240個像素呢善涨?1dp—>1240/160=1.5px窒盐,即1dp與1.5px等價了草则。
綜上:dpi = 像素/尺寸, px=dpi/160dp
然后說上面的問題蟹漓,直接用px肯定不行炕横,換成dp能處理大多數情況,但是有些情況還是顯示不正確葡粒。比如寬度都為1080px的屏幕份殿,但是因為尺寸不同dpi分別是160和240嗽交,當把圖片寬度設置為540dp時卿嘲,那么在dpi為160的屏幕上顯示是540px,也就是屏幕的一半夫壁,但是在dpi為240的屏幕上拾枣,根據上述算法,顯示為540*(240/160)px盒让,所以在屏幕寬度為1080px的屏幕上顯示并不是屏幕的一半(dpi越大梅肤,顯示圖片越寬)。這樣滿足不了我們需求糯彬。
所以適配還是需要手擼凭语,常見的有:自定義像素適配、百分比布局適配撩扒、修改像素密度適配似扔。
1. 自定義像素適配
以一個特定寬度尺寸的設備為參考,在View的加載過程中根據當前設備的實際像素換算出目標像素搓谆,再作用在控件上炒辉。
首先獲取寫一個工具類獲取設計稿和當前手機屏幕的縮放比例,這里采用單例的Utils:
public class Utils {
private static Utils utils;
//這里是設計稿參考寬高
private static final float STANDARD_WIDTH = 1080;
private static final float STANDARD_HEIGHT = 1920;
//這里是屏幕顯示寬高
private int mDisplayWidth;
private int mDisplayHeight;
private Utils(Context context){
//獲取屏幕的寬高
if(mDisplayWidth == 0 || mDisplayHeight == 0){
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (manager != null){
DisplayMetrics displayMetrics = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(displayMetrics);
if (displayMetrics.widthPixels > displayMetrics.heightPixels){
//橫屏
mDisplayWidth = displayMetrics.heightPixels;
mDisplayHeight = displayMetrics.widthPixels;
}else{
mDisplayWidth = displayMetrics.widthPixels;
mDisplayHeight = displayMetrics.heightPixels - getStatusBarHeight(context);
}
}
}
}
public int getStatusBarHeight(Context context){
int resID = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resID > 0){
return context.getResources().getDimensionPixelSize(resID);
}
return 0;
}
public static Utils getInstance(Context context){
if (utils == null){
utils = new Utils(context.getApplicationContext());
}
return utils;
}
//獲取水平方向的縮放比例
public float getHorizontalScale(){
return mDisplayWidth / STANDARD_WIDTH;
}
//獲取垂直方向的縮放比例
public float getVerticalScale(){
return mDisplayHeight / STANDARD_HEIGHT;
}
}
自定義一個RelativeLayout:
public class ScreenAdapterLayout extends RelativeLayout {
// 防止重復調用
private boolean flag;
public ScreenAdapterLayout(Context context) {
super(context);
}
public ScreenAdapterLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ScreenAdapterLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!flag){
//獲取橫泉手、縱向縮放比
float scaleX = Utils.getInstance(getContext()).getHorizontalScale();//
float scaleY = Utils.getInstance(getContext()).getVerticalScale();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
//重新設置子View的布局屬性
LayoutParams params = (LayoutParams) child.getLayoutParams();
params.width = (int) (params.width * scaleX);
params.height = (int) (params.height * scaleY);
params.leftMargin = (int)(params.leftMargin * scaleX);
params.rightMargin = (int)(params.rightMargin * scaleX);
params.topMargin = (int)(params.topMargin * scaleY);
params.bottomMargin = (int)(params.bottomMargin * scaleY);
}
flag = true;
}
// 計算完成后再進行測量
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
之后我們的布局文件都要用這個自定義的RelativeLayout包裹黔寇,當前我們還需要自定義LinearLayout等,就能實現(xiàn)適配斩萌,注意的是單位要用px缝裤,就是設計稿上的px值:
<?xml version="1.0" encoding="utf-8"?>
<com.netease.screenadapter.pixel.ScreenAdapterLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="540px"
android:layout_height="540px"
android:layout_marginLeft="10px"
android:text="Hello World!"
android:background="@color/colorAccent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.netease.screenadapter.pixel.ScreenAdapterLayout>
完事!
2. 百分比布局適配
用Google的Percent-support-lib就可以颊郎,這里不說使用憋飞,說下實現(xiàn)。
首先肯定要自定義屬性姆吭,讓控件可以設置百分比榛做,在attrs里添加:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentLayout">
<attr name="widthPercent" format="float" />
<attr name="heightPercent" format="float" />
<attr name="marginLeftPercent" format="float" />
<attr name="marginRightPercent" format="float" />
<attr name="marginTopPercent" format="float" />
<attr name="marginBottomPercent" format="float" />
</declare-styleable>
</resources>
這些屬性肯定要解析并使用,具體的解析過程可以在RelativeLayout或者LinearLayout的源碼中查看它們的特有屬性是怎么處理的。LayoutInflater的源碼中可以看出View的布局屬性检眯,都是在父容器中創(chuàng)建的(源碼分析就不貼出了厘擂,主要的方法就是調用了父容器的generateLayoutParams()方法),所以直接自定義Layout去獲取去這些屬性就可以了锰瘸。這里直接貼出處理代碼:
public class PercentLayout extends RelativeLayout {
public PercentLayout(Context context) {
super(context);
}
public PercentLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PercentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//獲取父容器的尺寸
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
ViewGroup.LayoutParams params = child.getLayoutParams();
//如果說是百分比布局屬性
if (checkLayoutParams(params)){
LayoutParams lp = (LayoutParams)params;
float widthPercent = lp.widthPercent;
float heightPercent = lp.heightPercent;
float marginLeftPercent = lp.marginLeftPercent;
float marginRightPercent= lp.marginRightPercent;
float marginTopPercent= lp.marginTopPercent;
float marginBottomPercent = lp.marginBottomPercent;
if (widthPercent > 0){
params.width = (int) (widthSize * widthPercent);
}
if (heightPercent > 0){
params.height = (int) (heightSize * heightPercent);
}
if (marginLeftPercent > 0){
((LayoutParams) params).leftMargin = (int) (widthSize * marginLeftPercent);
}
if (marginRightPercent > 0){
((LayoutParams) params).rightMargin = (int) (widthSize * marginRightPercent);
}
if (marginTopPercent > 0){
((LayoutParams) params).topMargin = (int) (heightSize * marginTopPercent);
}
if (marginBottomPercent > 0){
((LayoutParams) params).bottomMargin = (int) (heightSize * marginBottomPercent);
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs){
return new LayoutParams(getContext(), attrs);
}
public static class LayoutParams extends RelativeLayout.LayoutParams{
private float widthPercent;
private float heightPercent;
private float marginLeftPercent;
private float marginRightPercent;
private float marginTopPercent;
private float marginBottomPercent;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
//解析自定義屬性
TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.PercentLayout);
widthPercent = a.getFloat(R.styleable.PercentLayout_widthPercent, 0);
heightPercent = a.getFloat(R.styleable.PercentLayout_heightPercent, 0);
marginLeftPercent = a.getFloat(R.styleable.PercentLayout_marginLeftPercent, 0);
marginRightPercent = a.getFloat(R.styleable.PercentLayout_marginRightPercent, 0);
marginTopPercent = a.getFloat(R.styleable.PercentLayout_marginTopPercent, 0);
marginBottomPercent = a.getFloat(R.styleable.PercentLayout_marginBottomPercent, 0);
a.recycle();
}
}
}
然后我們布局的時候刽严,用自定的Layout包裹就行:
<?xml version="1.0" encoding="utf-8"?>
<com.netease.screenadapter.percentlayout.PercentLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="寬50%;高75%"
android:background="#f00"
app:widthPercent="0.5"
app:heightPercent="0.75"
app:marginLeftPercent="0.5"/>
</com.netease.screenadapter.percentlayout.PercentLayout>
完事获茬!
總結下自定義屬性解析:
- 在attrs里創(chuàng)建自定義屬性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentLayout">
<attr name="widthPercent" format="float" />
...
</declare-styleable>
</resources>
- 創(chuàng)建自定義Layout港庄,比如:
public class PercentLayout extends RelativeLayout
- 在自定義Layout中創(chuàng)建靜態(tài)內部類LayoutParams繼承自該Layout. LayoutParams并實現(xiàn)構造方法,在其構造方法中用obtainStyledAttributes去解析這些自定義屬性:
public static class LayoutParams extends RelativeLayout.LayoutParams
private float widthPercent;
...
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
//解析自定義屬性
TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.PercentLayout);
widthPercent = a.getFloat(R.styleable.PercentLayout_widthPercent, 0);
...
a.recycle();
}
}
- 重寫自定義Layout的generateLayoutParams()方法恕曲,使用我們自定義的LayoutParams:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs){
return new LayoutParams(getContext(), attrs);
}
- 重寫checkLayoutParams鹏氧,模仿ViewGroup中的代碼,可寫可不寫佩谣。用于獲取LayoutParams時的類型判斷把还,也可以直接用p instanceof LayoutParams去判斷:
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
- 使用:
if (checkLayoutParams(params)){
LayoutParams lp = (LayoutParams)params;
float widthPercent = lp.widthPercent;
...
}
3. 修改像素密度適配
修改density、scaleDensity茸俭,densityDpi的值吊履,直接更改系統(tǒng)內部對于目標尺寸的像素密度。
density:屏幕密度调鬓,系統(tǒng)針對某一尺寸的分辨率縮放比例(某一尺寸是指每寸有160px的屏幕艇炎,上面也有提到過),假設某個屏幕每英寸有320px腾窝,那么此時density為2
scaleDensity:字體縮放比例缀踪,默認情況下和density一樣
densityDpi:每英寸像素的,比如剛才說的160或320虹脯,可以通過屏幕尺寸和分辨率算出來
為什么修改這些值能達到屏幕適配驴娃?
TypeValue源碼中有這樣一段:
public static float applyDimension(int unit, float value, DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
這段代碼說明我們不管在XML里設置什么單位(sp、dp循集、px)唇敞,最終都會轉換成px設置到屏幕上,而轉換過程的計算方式就用到了density咒彤、scaledDensity疆柔。
為什么修改density,不使用系統(tǒng)的density镶柱?
因為相同分辨率的屏幕婆硬,因為尺寸不同,density也會不同奸例,例子上面提到過。
原理完事直接貼代碼:
新建一個Density類,提供setDensity()方法:
public class Density {
private static final float WIDTH = 320;//參考設備的寬查吊,單位是dp 320 / 2 = 160
private static float appDensity;//表示屏幕密度
private static float appScaleDensity; //字體縮放比例谐区,默認appDensity
public static void setDensity(final Application application, Activity activity){
//獲取當前app的屏幕顯示信息
DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
if (appDensity == 0){
//初始化賦值操作
appDensity = displayMetrics.density;
appScaleDensity = displayMetrics.scaledDensity;
//添加字體變化監(jiān)聽回調
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字體發(fā)生更改,重新對scaleDensity進行賦值
if (newConfig != null && newConfig.fontScale > 0){
appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
//計算目標值density, scaleDensity, densityDpi
float targetDensity = displayMetrics.widthPixels / WIDTH; // 1080 / 360 = 3.0
float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
int targetDensityDpi = (int) (targetDensity * 160);
//替換Activity的density, scaleDensity, densityDpi
DisplayMetrics dm = activity.getResources().getDisplayMetrics();
dm.density = targetDensity;
dm.scaledDensity = targetScaleDensity;
dm.densityDpi = targetDensityDpi;
}
}
然后在每個Activity里調用Density.setDensity(getApplication(),this)設置就可以了逻卖,當然可以在BaseActivity里調用宋列。但是最好的解決方式是在Application的registerActivityLifecycleCallbacks()里設置:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
Density.setDensity(App.this, activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
});
}
}
完事!F酪病炼杖!