app首页是一个不可以滑动的页面,因此需要高和宽同时适配;内容比较多——日期,抽奖按钮,步数表盘,步数柱状图,底部的Tab,广告等;首页中心是一个圆形表盘,高度取剩余高度,宽度取屏幕宽度,并且考虑高宽相等,两方面适应之后决定圆形表盘的直径,导致在不同手机上效果千奇百怪。
常见的适配方案,可以参考这篇文章——《Android 目前稳定高效的UI适配方案》,文中提到的几种适配方案包括:
对于Pacer这个App和首页的适配需求来说:
使用dp为单位,让屏 幕宽度不等于设计图的都能很好的适配,以宽为基准进行适配。
可以解决大部分页面效果不统一的情况。
设定宽度基准之后,假设宽度都是360的基础上,计算首页中心fragment的高度:
fragment高度=屏幕高度 - 系统导航栏高度 - 系统状态栏高度 - app广告高度 - app导航tab高度 - 头部按钮高度
根据不同的fragment高度,划分到5档,5个区间高度分别为:
由UI分别按照比例设计5套UI,尽可能保证在不同宽高比的屏幕上,视觉效果统一。
代码上,根据fragment高度,加载5套布局文件中的一套。
布局文件的区别体现在:
1.字体大小不同
2.控件大小不同
因为布局的高度在一定范围内而不是确定的值,所以需要控件之间的间隔在一定范围内可以自适应。
首页内容比较多,使用ConstaintLayout布局,减少嵌套层次。
使用Space控件来做组件之间的间隔,Space控件的高度使用权重(layout_constraintVertical_weight)设置,保证5个档位内的高度范围内,按照比例分配间隔高度。
Space 经常用于组件之间的缝隙,其
draw()
为空,减少了绘制渲染的过程。组件之间的距离使用 Space 会提高了绘制效率,特别是对于动态设置间距会很方便高效。
正是因为
draw()
为空,对该 view 没有做任务绘制渲染,所以不能对 Space 设置背景色。
因为首页+左滑+右滑,共3个fragment,都是圆盘结构,都需要宽度高度同时适配,所以一共需要3*5=15个layout文件。
首页改版为可滑动,通过布局自适应不能满足自动填充一整屏幕的需求,改为通过代码计算高度进行设置。
具体实施——工具类ScreenAutoSizeUtil
将比例写入map里面,将其他元素高度设定之后,用代码通过比例和剩余总高度计算真实高度,设置到代码中。节省了布局文件,可读性变差,灵活性高。(待补充)
原理同今日头条适配方案原理相同。
设计稿宽度是固定的,只有一种宽度,而屏幕的真是宽度其实是有很多的。
虽然dp的出现目的在于取代像素,尽量保证屏幕的宽度统一,但是屏幕碎片化严重,导致以dp为单位,宽度还是有很多种,不能统一。
在屏幕绘制的时候,需要把单位换算成px,也就是最终是使用px为单位进行绘制的。平时我们使用dp为单位写布局之后,最终要使用公式px=dp * density转换为px进行绘制。
这套方案,相当于修改了dp的定义——dp代表的是屏幕宽度平均分成【设计图宽度】的份数之后的1单位长度。
比如屏幕宽度是1080px,用dp为单位时屏幕宽度是480dp,density=2.25。
要使用这个方案的话,假设我们的设计图宽度是360,我们修改了dp的定义,dp的新定义是讲屏幕平分为360份之后的单位长度(也就是3px),按照这个dp的数值计算出density应该修改为3。
我们再使用新的dp去绘制控件的宽度,最终在绘制的时候再用新的density去计算成px:
屏幕宽度 x density=360 x 3 = 1080
在像素这个单位上,宽度没有问题,还是真实宽度。
简单说,方案通过在 Acivity#onCreate 中修改 density 的值,强行把所有不同尺寸分辨率的手机的屏幕宽度dp值改成一个统一的值。
主要回答3个问题:
先说结论:density不同于dpi,dpi是有物理含义的,表示每英寸的像素数,屏幕的物理宽度确定,像素数量确定,dpi就是确定的;density不是一个固定在系统中的值,而是一个“用于显示的逻辑密度”(The logical density of the display)在程序启动之后通过真实物理中的dpi和规则(160为标准)计算出来的,这个规则是以160为标准,是一个类似约定俗成的参考标准。density是public成员,因此可以很方便的修改。
阅读源码,查看density的值是怎么来的:
density 是 DisplayMetrics 中的成员变量,DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得:
public DisplayMetrics getDisplayMetrics() {
return mResourcesImpl.getDisplayMetrics();
}
mResourcesImpl是ResourcesImpl实例,查看ResourcesImpl#getDisplayMetrics :
DisplayMetrics getDisplayMetrics() {
if (DEBUG_CONFIG) Slog.v(TAG, "Returning DisplayMetrics: " + mMetrics.widthPixels
+ "x" + mMetrics.heightPixels + " " + mMetrics.density);
return mMetrics;
}
返回的mMetrics是ResourcesImpl的对象,来看它是怎么创建和赋值的。
private final DisplayMetrics mMetrics = new DisplayMetrics();
……
public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
@Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
mAssets = assets;
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
mConfiguration.setToDefaults();
updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
}
查看DisplayMetrics中density的描述和DisplayMetrics#setToDefaults方法,DisplayMetrics的density是一个“逻辑”密度,无关像素的比例因子。它是通过设备dpi除以160(DENSITY_DEFAULT)计算出来的,就是设备dpi是真实的,而它是考虑比例之后规定出来的。
public class DisplayMetrics {
……略……
/**
* The logical density of the display. This is a scaling factor for the
* Density Independent Pixel unit, where one DIP is one pixel on an
* approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen),
* providing the baseline of the system's display. Thus on a 160dpi screen
* this density value will be 1; on a 120 dpi screen it would be .75; etc.
*
* <p>This value does not exactly follow the real screen size (as given by
* {@link #xdpi} and {@link #ydpi}, but rather is used to scale the size of
* the overall UI in steps based on gross changes in the display dpi. For
* example, a 240x320 screen will have a density of 1 even if its width is
* 1.8", 1.3", etc. However, if the screen resolution is increased to
* 320x480 but the screen size remained 1.5"x2" then the density would be
* increased (probably to 1.5).
*
* @see #DENSITY_DEFAULT
*/
public float density;
……略……
public void setToDefaults() {
widthPixels = 0;
heightPixels = 0;
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
densityDpi = DENSITY_DEVICE;
scaledDensity = density;
xdpi = DENSITY_DEVICE;
ydpi = DENSITY_DEVICE;
noncompatWidthPixels = widthPixels;
noncompatHeightPixels = heightPixels;
noncompatDensity = density;
noncompatDensityDpi = densityDpi;
noncompatScaledDensity = scaledDensity;
noncompatXdpi = xdpi;
noncompatYdpi = ydpi;
}
……略……
上述代码中的DENSITY_DEFAULT的数值就是160。
以上就是density的计算过程,所以density是一个计算出来的值,用途是在使用dp为单位,计算转换成px时使用的比例因子,影响dp到px的转换。对其进行修改,需要保证修改后dp转换成px不会出错,也就是响应的使用的dp的值要配套。
阅读源码,可以发现:
density 是 DisplayMetrics 中的成员变量,DisplayMetrics 是 Resource 的成员变量,Resource 是 Activity 的父类 ContextWrapper 的成员变量,因此 density 对于各个 Activity 来说是自己持有的,Activity 修改 density 只影响当前 Activity 。
修改的地方选在Activity#onCreate方法里。
View绘制的时候会获取DisplayMetrics,用到的也是当前Activity的resource。
同2的原因一样,因为density是以Activity为单位的,因此只要在该Activity的onCreate方法中选择是否修改density,如何修改density就可以对Activity进行是否适配,单独适配了。
所以框架才能配置哪些页面进行适配,哪些不进行适配,哪些个性化适配。
├── external
│ ├── ExternalAdaptInfo.java
│ ├── ExternalAdaptManager.java
│── internal
│ ├── CancelAdapt.java
│ ├── CustomAdapt.java
│── unit
│ ├── Subunits.java
│ ├── UnitsManager.java
│── utils
│ ├── AutoSizeUtils.java
│ ├── LogUtils.java
│ ├── Preconditions.java
│ ├── ScreenUtils.java
├── ActivityLifecycleCallbacksImpl.java
├── AutoAdaptStrategy.java
├── AutoSize.java
├── AutoSizeConfig.java
├── DefaultAutoAdaptStrategy.java
├── DisplayMetricsInfo.java
├── FragmentLifecycleCallbacksImpl.java
├── InitProvider.java
使用者只需要在 AndroidManifest.xml 中填写一下 meta-data 标签,其他什么都不做,AndroidAutoSize 就能自动运行,并在 App 启动时自动解析。
原理:ContentProvider的onCreate的调用时机介于Application的attachBaseContext和onCreate之间,Provider的onCreate优先于Application的onCreate执行,并且此时的Application已经创建成功,这样就不需要调用方在Application里去进行初始化,框架可以自动运行了。
InitProvider#onCreate中,初始化框架:
public boolean onCreate() {
AutoSizeConfig.getInstance()
.setLog(true)
.init((Application) getContext().getApplicationContext())
.setUseDeviceSize(false);
return true;
}
参数配置类, 给 AndroidAutoSize 配置一些必要的自定义参数
DefaultAutoAdaptStrategy中做了什么?
自定义一个DefaultAutoAdaptStrategy类型,实现AutoAdaptStrategy接口的applyAdapt方法——开始执行屏幕适配逻辑,applyAdapt方法中判断是否对第三方适配,是否是不适配,是否单独适配等 。
最后调用的是适配:
//如果 target 实现 CustomAdapt 接口表示该 target 想自定义一些用于适配的参数, 从而改变最终的适配效果
if (target instanceof CustomAdapt) {
LogUtils.d(String.format(Locale.ENGLISH, "%s implemented by %s!", target.getClass().getName(), CustomAdapt.class.getName()));
AutoSize.autoConvertDensityOfCustomAdapt(activity, (CustomAdapt) target);
} else {
LogUtils.d(String.format(Locale.ENGLISH, "%s used the global configuration.", target.getClass().getName()));
AutoSize.autoConvertDensityOfGlobal(activity);
}
ActivityLifecycleCallbacksImpl 实现 Application.ActivityLifecycleCallbacks接口,可用来代替在 BaseActivity 中加入适配代码的传统方式,这种方案类似于 AOP, 面向接口, 侵入性低, 方便统一管理, 扩展性强, 并且也支持适配三方库的Activity。
自定义ActivityLifecycleCallbacksImpl实现Application.ActivityLifecycleCallbacks接口,用于registerActivityLifecycleCallbacks中,在ActivityLifecycleCallbacksImpl写onActivityCreated中调用applyAdapt,进行适配。
具体适配方法位于AutoSize类。
还提供了一些接口:onAdaptListener(onAdaptBefore,onAdaptAfter)
AutoSize#cancelAdapt中,做到如何取消适配——调用setDensity方法将density设置成原来的density
/**
* 取消适配
*
* @param activity {@link Activity}
*/
public static void cancelAdapt(Activity activity) {
float initXdpi = AutoSizeConfig.getInstance().getInitXdpi();
switch (AutoSizeConfig.getInstance().getUnitsManager().getSupportSubunits()) {
case PT:
initXdpi = initXdpi / 72f;
break;
case MM:
initXdpi = initXdpi / 25.4f;
break;
default:
}
setDensity(activity, AutoSizeConfig.getInstance().getInitDensity()
, AutoSizeConfig.getInstance().getInitDensityDpi()
, AutoSizeConfig.getInstance().getInitScaledDensity()
, initXdpi);
}
DefaultAutoAdaptStrategy#applyAdapt
//如果 target 实现 CancelAdapt 接口表示放弃适配, 所有的适配效果都将失效
if (target instanceof CancelAdapt) {
LogUtils.w(String.format(Locale.ENGLISH, "%s canceled the adaptation!", target.getClass().getName()));
AutoSize.cancelAdapt(activity);
return;
}
问题,density是以Activity为单位的吗?
原理同上