引言
前一篇Android 進階——高級UI必知必會之常用的屏幕適配完全攻略詳解(七)總結了下通過Android 自動適配資源的特性通過限定符和資源別名進行屏幕適配的相關知識,不過並非唯一的思路,還可以在運行時通過代碼動態進行適配,這篇文章將提供幾種思路,相關係列文件鏈接如下:
- Android進階——高級UI必知必會之2D繪製與Paint的基礎應用(一)
- Android進階——高級UI必知必會之2D繪製與使用Paint對圖形進行渲染和濾鏡混合處理(二)
- Android進階——高級UI必知必會之Android座標系與Canvas小結(三)
- Android 進階——高級UI必知必會之統一可繪製概念Drawable詳解(四)
- Android 進階——高級UI必知必會之Path和貝塞爾曲線(五)
- Android 進階——高級UI必知必會之藉助PathMeasure打造酷炫Path特效(六)
- Android 進階——高級UI必知必會之常用的屏幕適配完全攻略詳解(七)
- Android 進階——高級UI必知必會之常用的自定義ViewGroup進行屏幕適配核心思想分享(八)
一、自定義ViewGroup 進行屏幕適配概述
自定義ViewGroup 進行屏幕適配的核心思想很簡單,本質上來說屏幕適配就是對View的測量Measure流程進行干預,在對ViewTree進行測量前,選取一個分辨率作爲基準(1080*1920比較主流),計算縮放比例,然後繼承ViewGroup重寫onMeasure方法,在佈局裏替換Android系統的原生ViewGroup,用自己寫的ViewGroup包裹控件,並且在onMeasure方法里根據基準分辨率與目標分辨率計算縮放比例關係,再重新設置View的尺寸。簡而言之就是在ViewGroup佈局時中計算並通過測量,按照比例進行縮放設值。
二、以像素px爲單位計算縮放比例進行適配
自定義ViewGroup時要想幹預View的測量Measure過程,最直接的方式就是直接重寫onMeasure方法,通過遍歷ViewGroup中的子View,並重新設置其對應的LayoutParams的值並更新。
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
/**
* 以像素爲單位計算縮放係數進行適配
* @author cmo
*/
public class ScreenAdapterLayout extends RelativeLayout {
private boolean isMeasured =false;
public ScreenAdapterLayout(Context context) {
this(context,null);
}
public ScreenAdapterLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public ScreenAdapterLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 僅僅對其直接子View有效
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//防止兩次測量
if (!isMeasured) {
isMeasured = true;
//獲取橫豎方向等比
float scaleX = ScreenHelper.getInstance(getContext()).getHorizontalScale();
float scaleY = ScreenHelper.getInstance(getContext()).getVerticalScale();
//獲取直接子View總個數
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
//使用縮放比例參數重新計算其LayoutParams的值並更新
LayoutParams params = (LayoutParams) child.getLayoutParams();
//TODO 完善各種細節,比如說子View的Width爲-1時等
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);
child.setPadding((int) (child.getPaddingLeft() * scaleX), (int) (child.getPaddingTop() * scaleY),
(int) (child.getPaddingRight() * scaleX), (int) (child.getPaddingBottom() * scaleY));
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
一個獲取屏幕參數的工具類:
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
/**
* @author cmo
*/
public class ScreenHelper {
private static final String STATUS_BAR_HEIGHT = "status_bar_height";
private static final String DIMEN = "dimen";
private static final String ANDROID = "android";
private static ScreenHelper screenHelper;
private Context mContext;
/**
* UI設計稿的基準寬高
*/
private static final float STANDARD_WIDTH = 1080;
private static final float STANDARD_HEIGHT = 1920;
/**
* 屏幕的真實寬高
*/
private int mDisplayWidth=0;
private int mDisplayHeight=0;
private int mStatusBarHeight=0;
private ScreenHelper(Context context) {
mContext = context;
if(mStatusBarHeight==0){
mStatusBarHeight=getStatusBarHeight();
}
if (mDisplayWidth == 0 || mDisplayHeight == 0) {
initScreenSize(context);
}
}
/**
* 初始化屏幕寬高 默認是沒有虛擬按鍵的,如果有虛擬按鍵還需要減掉其高度
* @param context
*/
private void initScreenSize(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (windowManager != null) {
//寬高獲取
DisplayMetrics displayMetrics = new DisplayMetrics();
//如果不是NavigationBar沉浸式(不包含NavigationBar)
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
// windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);//真實屏幕寬高
//判斷當前的橫豎屏
if (displayMetrics.widthPixels > displayMetrics.heightPixels) {
//橫屏
mDisplayWidth = displayMetrics.heightPixels;
mDisplayHeight = displayMetrics.widthPixels - mStatusBarHeight;
} else {
//豎屏
mDisplayWidth = displayMetrics.widthPixels;
mDisplayHeight = displayMetrics.heightPixels - mStatusBarHeight;
}
}
}
public static ScreenHelper getInstance(Context context) {
if (screenHelper == null) {
screenHelper = new ScreenHelper(context);
}
return screenHelper;
}
/**
* 獲取狀態欄高度
* @return
*/
public int getStatusBarHeight() {
int resId = mContext.getResources().getIdentifier(STATUS_BAR_HEIGHT, DIMEN, ANDROID);
if (resId > 0) {
//獲取具體的像素值
return mContext.getResources().getDimensionPixelSize(resId);
}
return 0;
}
/**
* 獲取水平方向的縮放比例,通過屏幕與基準分辨率進行對比,下同
*/
public float getHorizontalScale() {
return mDisplayWidth / STANDARD_WIDTH;
}
/**
* 獲取垂直方向的縮放比例
*/
public float getVerticalScale() {
return mDisplayHeight / (STANDARD_HEIGHT - mStatusBarHeight);
}
}
dp、sp轉換爲px的工具類
/**
* dp、sp轉換爲px的工具類
*/
public class DisplayUtil {
/**
* 將px值轉換爲dip或dp值,保證尺寸大小不變
* @param context
* @param pxValue
* @return
*/
public static int px2dip(Context context, float pxValue){
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5f);
}
/**
* 將dip或dp值轉換爲px值,保證尺寸不變
* @param context
* @param dipValue
* @return
*/
public static int dip2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
/**
* 將px值轉換爲sp值,保證文字大小不變
* @param context
* @param pxValue
* @return
*/
public static int px2sp(Context context, float pxValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValue / fontScale + 0.5f);
}
/**
* 將sp值轉換爲px值,保證文字大小不變
* @param context
* @param spValue
* @return
*/
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
使用時直接在原有的控件佈局外套上一層這個自定義的ViewGroup即可:
三、百分比動態佈局
百分比動態佈局本質上來說核心思想與以像素爲單位進行動態佈局相同,區別在於View並不直接支持設置百分比的屬性,而這些自定義屬性,需要通過自定義ViewGroup.LayoutParams來傳入。
- 創建自定義屬性
res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentLayout">
<attr name="widthPercent" format="fraction" />
<attr name="heightPercent" format="fraction" />
<attr name="marginLeftPercent" format="fraction" />
<attr name="marginRightPercent" format="fraction" />
<attr name="marginTopPercent" format="fraction" />
<attr name="marginBottomPercent" format="fraction" />
</declare-styleable>
</resources>
- 在容器中去創建一個靜態內部類LayoutParams
- 在LayoutParams構造方法中獲取自定義屬性
- onMeasure中給子View設置修改後的屬性值
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
/**
* 根據百分比進行適配
* @author cmo
*/
public class PercentLayout extends RelativeLayout {
private boolean isMeasured=false;
public PercentLayout(Context context) {
this(context,null);
}
public PercentLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public PercentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//獲取父容器寬高
if (!isMeasured) {
isMeasured = true;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//給子控件設置修改後的屬性值
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
//獲取子控件
View child = getChildAt(i);
//獲取子控件LayoutParams
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
//判斷子控件是否是百分比佈局屬性
if (checkLayoutParams(layoutParams)) {
PercentLayoutParams percentLayoutParams = (PercentLayoutParams) layoutParams;
float widthPercent = percentLayoutParams.widthPercent;
float heightPercent = percentLayoutParams.heightPercent;
float marginLeftPercent = percentLayoutParams.marginLeftPercent;
float marginRightPercent = percentLayoutParams.marginRightPercent;
float marginTopPercent = percentLayoutParams.marginTopPercent;
float marginBottomPercent = percentLayoutParams.marginBottomPercent;
if (widthPercent > 0) {
percentLayoutParams.width = (int) (widthSize * widthPercent);
}
if (heightPercent > 0) {
percentLayoutParams.height = (int) (heightSize * heightPercent);
}
if (marginLeftPercent > 0) {
percentLayoutParams.leftMargin = (int) (widthSize * marginLeftPercent);
}
if (marginRightPercent > 0) {
percentLayoutParams.rightMargin = (int) (widthSize * marginRightPercent);
}
if (marginTopPercent > 0) {
percentLayoutParams.topMargin = (int) (heightSize * marginTopPercent);
}
if (marginBottomPercent > 0) {
percentLayoutParams.bottomMargin = (int) (heightSize * marginBottomPercent);
}
}
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof PercentLayoutParams;
}
@Override
public PercentLayoutParams generateLayoutParams(AttributeSet attrs) {
return new PercentLayoutParams(getContext(), attrs);
}
/**
* 1、創建自定義屬性
* 2、在容器中去創建一個靜態內部類LayoutParams
* 3、在LayoutParams構造方法中獲取自定義屬性
* 4、onMeasure中給子控件設置修改後的屬性值
*/
private static class PercentLayoutParams extends RelativeLayout.LayoutParams {
private float widthPercent;
private float heightPercent;
private float marginLeftPercent;
private float marginRightPercent;
private float marginTopPercent;
private float marginBottomPercent;
public PercentLayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
//3、在LayoutParams構造方法中獲取自定義屬性 解析自定義屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PercentLayout);
widthPercent = typedArray.getFraction(R.styleable.PercentLayout_widthPercent, 1, 2, 0);
heightPercent = typedArray.getFraction(R.styleable.PercentLayout_heightPercent, 1, 2, 0);
marginLeftPercent = typedArray.getFraction(R.styleable.PercentLayout_marginLeftPercent, 1, 2, 0);
marginRightPercent = typedArray.getFraction(R.styleable.PercentLayout_marginRightPercent, 1, 2, 0);
marginTopPercent = typedArray.getFraction(R.styleable.PercentLayout_marginTopPercent, 1, 2, 0);
marginBottomPercent = typedArray.getFraction(R.styleable.PercentLayout_marginBottomPercent, 1, 2, 0);
typedArray.recycle();
}
}
}
使用百分比佈局ViewGroup進行適配:
<?xml version="1.0" encoding="utf-8"?>
<com.crazymo.widget.PercentLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:crazymo="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:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#f00"
crazymo:heightPercent="5%"
crazymo:widthPercent="90%"
crazymo:marginLeftPercent="30%"
crazymo:marginRightPercent="30%"
crazymo:marginTopPercent="1%"
tools:ignore="MissingPrefix" />
</com.crazymo.widget.PercentLayout>
四、像素密度爲單位進行適配
像素密度爲單位進行適配,核心思想與上面的有所不同,是通過與標準屏幕像素密度計算比例,然後在Activity生命週期方法中動態改變屏幕的density。
import android.app.Activity;
import android.app.Application;
import android.content.ComponentCallbacks;
import android.content.res.Configuration;
import android.util.DisplayMetrics;
/**
* 修改density,densityDpi值-直接更改系統內部對於目標尺寸而言的像素密度
* @author cmo
*/
public class DensityHelper {
/**
* 參考像素密度(dp)
*/
private static final float WIDTH = 360;
/**
* 表示屏幕密度
*/
private static float appDensity;
/**
* 字體縮放比例,默認爲appDensity
*/
private static float appScaleDensity;
public static void setDensity(final Application application, Activity activity) {
//獲取當前屏幕信息
DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
if (appDensity == 0) {
//初始化賦值
appDensity = displayMetrics.density;
appScaleDensity = displayMetrics.scaledDensity;
//監聽字體變化
application.registerComponentCallbacks(new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration newConfig) {
//字體發生更改,重新計算scaleDensity
if (newConfig != null && newConfig.fontScale > 0) {
appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
//計算目標density scaledDensity,比如//1080/360=3;
float targetDensity = displayMetrics.widthPixels / WIDTH;
float targetScaleDensity = targetDensity * (appScaleDensity / appDensity);
int targetDensityDpi = (int) (targetDensity * 160);
//替換Activity的值
//px = dp * (dpi / 160)
DisplayMetrics dm = activity.getResources().getDisplayMetrics();
//(dpi/160) 後得到的值
dm.density = targetDensity;
dm.scaledDensity = targetScaleDensity;
dm.densityDpi = targetDensityDpi;
}
}
在Activity的onCreate方法中動態設置屏幕像素密度
或者在Application中通過監聽ActivityLifecycleCallbacks:
public class MoApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
DensityHelper.setDensity(MoApplication.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) {
}
});
}
}
注意:不建議直接拿到項目中去使用,此文僅僅是分享核心思想,不完善和適配細節。
五、劉海屏適配小技巧
public class NotchActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notch);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if(getIsHasCutout()){
ScreenAdapterLayout layout = findViewById(R.id.layout);
layout.setPadding(0, StatusBarUtil.getStatusBarHeight(this), 0, 0);
}
}
}
關於劉海屏的適配來自網絡的代碼段,我只是進行了整理小結。