移动开发之100%设备屏幕适配方案
前言:本片博客将全面深入分析屏幕适配方案,针对不同项目要求,提供不同的适配框架分析,要求框架能100%适配所有设备。
目录
1.前期技术问题分析
2.适用于中小型项目适配方案
3.适用于大型项目适配方案
一、前期技术问题分析:
1.如何获取全局Context并解决Application冲突:
前言:从上图Context的类继承关系可以看到,有两个直系子类,分别是包装类和实现类。包装类下可以看到我们熟悉的身影:Service、Application、Activity,所以我们得出结论,Context只在上述环境中存在。因此要想获取全局context,我们常用的方式就是写一个MyApplication继承Application,然后重写onCreate方法提供全局上下文环境,然后
public class MyApplication extends Applocation {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
public static Context getContext() {
return context;
}
}
我们要告知系统修改其默认做法,转而启动我们自定义的MyApplication类。具体来说就是在AndroidManifest.xml文件的<application>标签下进行指定。 由于一个工程只能有一个Application,所以当我们使用第三方开源项目如Litepal等对application的配置有需求的资源时,就不能使用上述方法重复配置了,我们只需要在自定义的Application加入支持就可以了。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
......
<application
android:name="com.example.test.MyApplication"
......>
......
</application>
</manifest>
public class MyApplication extends Applocation {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
LitePalApplication.initialize(context);//加入支持
}
public static Context getContext() {
return context;
}
}
2.自定义View相关重点:
首先上图:
通过上图可以清楚的看出View自定义的过程:
如果是View:首先调用measure方法,接着会调用measure中的onMeasure方法进行测量,测量规则自定义
如果是ViewGroup:首先调用measure方法,接着会调用每个子View的measure方法进行测量,测量规则见下面分析
如何在Activity启动时获取View的宽/高?
1.重写onWindowFocusChanged方法,该方法的含义就是View已经初始化完成了
2.view.post(runnable),该方法的含义就是View已经初始化完成了
3.使用ViewTreeObserver的众多回调可以实现该功能
布局过程的自定义分为三类:
1.重写onMeasure来修改已有View的尺寸
第一步:调用super.onMeasure方法按原来的计算方法测量一次
第二步:获取到原来的尺寸
第三步:修改成新的尺寸
第四步:保存修改后的尺寸
2.重写onMeasure来全新定义View的尺寸
区别:不需要调用super.onMeasure方法
第一步:重写onMeasure把自己的尺寸计算出来
第二步:把计算的尺寸通过resolveSize()过滤一遍即可满足父View对子View限制要求的View
3.重写onMeasure和onLayout来全新定义ViewGroup的内部布局
第一步:重写onMeasure来计算内部布局
- 调用每个子View的measure方法,让子View自我测量
- 根据子View给出的尺寸,得出子View的位置,并保存他们的位置和尺寸
- 根据子View的位置和尺寸计算出自己的尺寸,并用setMeasureDimension保存
第二步:重写onLayout来摆放子View
自定义View须知:
1.让view支持wrap_content
解决方法:只需要对wrap_content模式设置一个默认宽/高即可
2.如果有必要,让view支持padding
解决方法:在onDraw方法中进行运算即可
3.尽量不要在view中使用handler,没必要,因为view内部提供了很多post方法
4.view中如果有线程或者动画,需要及时停止
5.view带有滑动嵌套时,要处理好滑动冲突
二、中小型项目屏幕适配解决方案框架:
框架分析:中小型项目对性能要求相对较低,因此采用修改布局参数的方法进行屏幕适配,该框架存在的性能问题是布局绘制了两次,第一次是setContentView(R.layout.activity_main),第二次是修改布局参数时,因此性能较低。
模块一:实现MyApplication提供全局context,方便使用
模块二:编写UIUtils类计算得到缩放比例
模块三:提供队外调用接口程序,实现框架的调用
模块一:
package com.example.adapter_screenapplication;
import android.app.Application;
public class JettApplication extends Application {
private static JettApplication intance;
public static JettApplication getInstance(){
return intance;
}
@Override
public void onCreate() {
super.onCreate();
intance = this;
}
}
模块二:
package com.example.adapter_screenapplication;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import java.lang.reflect.Field;
public class UIUtils {
//美工提供的标准宽高
public static final float STANDARD_WIDTH = 1080.0f;
public static final float STANDARD_HEIGTH = 1872.0f;
private static final String DIMEN_CLASS = "com.android.internal.R$dimen";
//实际设备的分辨率是 480 800
public float displayMetricsWidth;
public float displayMetricsHeigth;
//生成单例
private static UIUtils ourInstance;
public static UIUtils getInstance(Context context){
if (ourInstance==null){
ourInstance = new UIUtils(context);
}
return ourInstance;
}
private UIUtils(Context context){
//获取屏幕的真实宽高
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics displayMetrics = new DisplayMetrics();
if (displayMetricsWidth==0.0F||displayMetricsHeigth==0.0F){
windowManager.getDefaultDisplay().getMetrics(displayMetrics);
//获取状态栏高度
int systemHeigth = getSystemHeigth(context);
//处理真实宽高
if (displayMetrics.widthPixels>displayMetrics.heightPixels){//横屏
this.displayMetricsWidth = (float) displayMetrics.heightPixels;
this.displayMetricsHeigth = (float) displayMetrics.widthPixels-systemHeigth;
}else {//竖屏
this.displayMetricsWidth = (float) displayMetrics.widthPixels;
this.displayMetricsHeigth = (float) displayMetrics.heightPixels-systemHeigth;
}
}
}
private int getSystemHeigth(Context context) {
return getValue(context,"com.android.internal.R$dimen","system_bar_heigth",48);
}
private int getValue(Context context, String attrGroupClass, String ayyrName, int defValue) {
try {
Class c = Class.forName(attrGroupClass);
Object obj = c.newInstance();
Field field = c.getField(ayyrName);
//获取到的是ID
int x = Integer.parseInt(field.get(obj).toString());
return context.getResources().getDimensionPixelOffset(x);
} catch (Exception e) {
return defValue;
}
}
//获取缩放后的结果
public float getWidth(float width){
return width*(this.displayMetricsWidth/STANDARD_WIDTH);
}
public float getHeigth(float heigth){
return heigth*(this.displayMetricsHeigth/STANDARD_HEIGTH);
}
public int getWidth(int width){
return (int) (width*(this.displayMetricsWidth/STANDARD_WIDTH));
}
public int getHeigth(int heigth){
return (int) (heigth*(this.displayMetricsHeigth/STANDARD_HEIGTH));
}
}
模块三:
package com.example.adapter_screenapplication;
import android.view.View;
import android.widget.LinearLayout;
public class ViewCal {
//获取调用层的值进行设置
public static void setViewLinearLayoutParas(View view,int width,int heigth,int topMargin,int bottonMargin,int leftMargin,int rigthMargin){
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) view.getLayoutParams();
if (width!= LinearLayout.LayoutParams.MATCH_PARENT&&width!= LinearLayout.LayoutParams.WRAP_CONTENT){
layoutParams.width = (int) UIUtils.getInstance(JettApplication.getInstance()).getWidth(width);
}else {
layoutParams.width = width;
}
if (heigth!= LinearLayout.LayoutParams.MATCH_PARENT&&heigth!= LinearLayout.LayoutParams.WRAP_CONTENT){
layoutParams.height = (int) UIUtils.getInstance(JettApplication.getInstance()).getHeigth(heigth);
}else {
layoutParams.height = heigth;
}
layoutParams.topMargin = (int) UIUtils.getInstance(JettApplication.getInstance()).getHeigth(topMargin);
layoutParams.bottomMargin = (int) UIUtils.getInstance(JettApplication.getInstance()).getHeigth(bottonMargin);
layoutParams.leftMargin = (int) UIUtils.getInstance(JettApplication.getInstance()).getWidth(leftMargin);
layoutParams.rightMargin = (int) UIUtils.getInstance(JettApplication.getInstance()).getWidth(rigthMargin);
view.setLayoutParams(layoutParams);
}
}
调用层:
package com.example.adapter_screenapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
TextView textView1;
TextView textView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView1 = findViewById(R.id.text1);
textView2 = findViewById(R.id.text2);
ViewCal.setViewLinearLayoutParas(textView1,1040,80,10,0,20,10);
ViewCal.setViewLinearLayoutParas(textView2,400,400,20,0,0,0);
}
}
执行结果:
三、大型项目屏幕适配解决方案框架:
框架分析:该框架从自定义布局入手,在绘制层面进行屏幕适配,因此布局只会绘制一次,提高了应用性能,适用于大型项目屏幕适配解决方案。
模块一:自定义属性(宽/高的百分比)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PercentRelativeLayout" >
<attr name="layout_widthPercent" format="float"/>
<attr name="layout_heigthPercent" format="float"/>
</declare-styleable>
</resources>
模块二:布局中使用自定义属性
<?xml version="1.0" encoding="utf-8"?>
<!--名称空间app-->
<com.example.adapter_screenapplication2.PercentRelativeLayout 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"
app:layout_widthPercent = "0.5"
app:layout_heigthPercent = "0.5"
android:background="@color/colorAccent"
android:text="Hello World!"/>
</com.example.adapter_screenapplication2.PercentRelativeLayout>
模块三:自定义ViewGroup
package com.example.adapter_screenapplication2;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
//自定义RelativeLayout
public class PercentRelativeLayout extends RelativeLayout {
//自动生成的构造方法
public PercentRelativeLayout(Context context) {
super(context);
}
public PercentRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PercentRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
//重写onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = View.MeasureSpec.getSize(widthMeasureSpec);
int heigth = View.MeasureSpec.getSize(heightMeasureSpec);
//测量出子控件
int childCount = this.getChildCount();
for (int i=0;i<childCount;i++){
View child = this.getChildAt(i);
ViewGroup.LayoutParams layoutParams = child.getLayoutParams();
//把得到的布局参数进行更改
float widthPercent = 0;
float heigthPercent = 0;
if (layoutParams instanceof PercentRelativeLayout.LayoutParams){
//获取到布局文件上的内容
widthPercent = ((LayoutParams) layoutParams).getWidthPercent();
heigthPercent = ((LayoutParams) layoutParams).getHeigthPercent();
}
if (widthPercent>0){
layoutParams.width = (int) (width*widthPercent);
}
if (heigthPercent>0){
layoutParams.height = (int) (heigth*heigthPercent);
}
}
//调用父类的onMeasure方法得到的值就是修改后的值
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
//重写generateLayoutParams:
@Override
public RelativeLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
//这里返回设置好的布局参数
return new LayoutParams(getContext(),attrs);
}
public static class LayoutParams extends RelativeLayout.LayoutParams{
private float widthPercent;
private float heigthPercent;
//set/get方法
public float getWidthPercent() {
return widthPercent;
}
public void setWidthPercent(float widthPercent) {
this.widthPercent = widthPercent;
}
public float getHeigthPercent() {
return heigthPercent;
}
public void setHeigthPercent(float heigthPercent) {
this.heigthPercent = heigthPercent;
}
//在这里把自定义属性加入
public LayoutParams(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs,R.styleable.PercentRelativeLayout);
widthPercent = array.getFloat(R.styleable.PercentRelativeLayout_layout_widthPercent,0);
heigthPercent = array.getFloat(R.styleable.PercentRelativeLayout_layout_heigthPercent,0);
}
}
}
运行结果:TextView占屏幕宽高的1/2
总结:
本篇博客主要从适配过程中常见的技术问题或注意事项入手,对两种不同的框架进行对比分析,读者可根据需要选择框架更改代码,实现自己的适配框架。上述框架也是自己借鉴大佬们的框架自己消化理解得到的,如有问题欢迎读者指正。