今日头条屏幕适配方案中对横竖屏切换的优化
================
项目地址
今日头条屏幕适配方案 & 横竖屏切换优化
开始使用
- 将以下依赖项添加到
module
下的build.gradle
中
dependencies {
implementation 'com.oikawaii.library:core:1.0.0'
implementation 'com.oikawaii.library:density:1.0.0'
}
使用方法
- 在
Application
中(注意super.onCreate()
的位置)
package ${PACKAGE_NAME}
import android.app.Application
import com.oikawaii.library.core.android.app
import com.oikawaii.library.density.DensityUtil
class ${NAME} : Application() {
override fun onCreate() {
app = this
DensityUtil.init()
super.onCreate()
}
}
- 在
Activity
中(注意super.onCreate()
的位置)
class ${NAME} : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
DensityUtil.init(this, 420f, status = false, navigation = false, flag = Density.SHORT_SIDE_BASED)
super.onCreate(savedInstanceState)
}
}
}
前言
关于今日头条是配方案的优缺点,相关文章的内容的已经比较成熟这里也就不再赘述了,这里主要是解决今日头条屏幕适配方案落地研究一文评论中横竖屏切换时的一个issue.
相关文章:
一种极低成本的Android屏幕适配方式(字节跳动技术团队)
今日头条屏幕适配方案落地研究(codeGoogle)
关于横竖屏切换
横竖屏的问题一直是Android开发者相对不是太在乎的问题,因为生态上来说Android的平板本身就不是很成功,手机上也可以通过android:screenOrientation="portrait"
来禁止使用横屏模式,即使需要也可以使用layout-land
资源文件夹解决。
在今日头条适配方案中带来了一个优势——“已知宽度“,以固定420dp
的设计宽度为例,如果想要一个在屏幕正中间的Win10 Logo(伪),只需要这样就可以了:
但此时横屏是这样的:
此时横屏出现了两个问题,正方形显示不完整与正方形错位。
解决横屏错位
横屏错位的原因很简单,解决前先看看这个程序在Android 10下的运行效果。
所以解决方式很简单升级Android 10就可以了,所以原因很简单就是虚拟键的原因。
解决方法很简单:
方法1:加入对Navgation Bar
的判定即可,为了避免不时之需同时加入对Status Bar
的判定。
/**
* 在Activity中初始化Density
* @param ruler UI的设计宽度
* @param status 设置基准尺寸时是否包含标题栏
* @param navigation 设置基准尺寸时是否包含虚拟键
*/
fun init(activity: Activity, ruler: Float, status: Boolean, navigation: Boolean)
val width = DimenUtil.width(navigation)
val height = DimenUtil.height(status, navigation)
val density = width / ruler
val res = activity.resources
val metrics = res.displayMetrics
metrics.density = density
metrics.densityDpi = (160 * density).toInt()
metrics.scaledDensity = density * (scaledDensity / this.density)
scale = this.density / density
setBitmapDensity(metrics.densityDpi)
}
方法2:根据SystemUiVisibility
进行自动判断。
/**
* 在Activity中初始化Density
* @param ruler UI的设计宽度
*/
fun init(activity: Activity, ruler: Float) {
val f = activity.window.decorView.systemUiVisibility
val sf1 = View.SYSTEM_UI_FLAG_FULLSCREEN
val sf2 = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
val nf = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
val sf = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
val navigation = ((f and sf) == sf) || ((f and nf) == nf)
val status = ((f and sf) == sf) || ((f and sf1) == sf1) || ((f and sf2) == sf2)
val width = DimenUtil.width(navigation)
val height = DimenUtil.height(status, navigation)
val density = width / ruler
val res = activity.resources
val metrics = res.displayMetrics
metrics.density = density
metrics.densityDpi = (160 * density).toInt()
metrics.scaledDensity = density * (scaledDensity / this.density)
scale = this.density / density
setBitmapDensity(metrics.densityDpi)
}
(Navigation Bar
与Status Bar
的计算与排坑参见Android常用工具类(基于Kotlin)与相关源码。)
解决正方形显示不完整
关于正方形显示不完整的问题,其实前面也留有伏笔——“已知宽度”,正是这个优势带来了横屏的显示问题。由于布局文件中仅仅采用具体dp
值设置大小,而同时这个dp
值是基于设计宽度的。
解决方法一样很简单:
方法1:设置基准边(e.g.基于宽度、基于高度、基于短边、基于长边),为基准边设置设计尺寸。
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@IntDef({ Density.WIDTH_BASED, Density.HEIGHT_BASED, Density.LONG_SIDE_BASED, Density.SHORT_SIDE_BASED })
@Retention(RetentionPolicy.SOURCE)
public @interface Density {
int WIDTH_BASED = 1;
int HEIGHT_BASED = 2;
int LONG_SIDE_BASED = 3;
int SHORT_SIDE_BASED = 4;
}
需要注意,以高度为基准边时Status Bar
的高度问题(不时之需回收)
/**
* 在Activity中初始化Density
* @param ruler UI的设计宽度
* @param status 设置基准尺寸时是否包含标题栏
* @param navigation 设置基准尺寸时是否包含虚拟键
*/
fun init(activity: Activity, ruler: Float, status: Boolean, navigation: Boolean, @Density flag: Int) {
val width = DimenUtil.width(navigation)
val height = DimenUtil.height(status, navigation)
val pixels = when(flag) {
Density.WIDTH_BASED -> width
Density.HEIGHT_BASED -> height
Density.SHORT_SIDE_BASED -> if(app.isPortrait) width else height
Density.LONG_SIDE_BASED -> if(app.isLandscape) width else height
else -> 0
}
val density = pixels / ruler
val res = activity.resources
val metrics = res.displayMetrics
metrics.density = density
metrics.densityDpi = (160 * density).toInt()
metrics.scaledDensity = density * (scaledDensity / this.density)
scale = this.density / density
setBitmapDensity(metrics.densityDpi)
}
(Navigation Bar
与Status Bar
的状态判断参见Android常用工具类(基于Kotlin)与相关源码。)
方法2:以宽度为基准边,为横屏与竖屏设置不同的设计宽度。(因为)Status Bar
与宽度无关,所以可以移除Status Bar
的尺寸判定
/**
* 在Activity中初始化Density
* @param portRuler UI竖屏时的设计宽度
* @param landRuler UI横屏时的设计宽度
* @param navigation 设置基准尺寸时是否包含虚拟键
*/
fun init(activity: Activity, portRuler: Float, landRuler: Float, navigation: Boolean) {
val width = DimenUtil.width(navigation)
val ruler = if(app.isLandscape) landRuler else portRuler
val density = width / ruler
val res = activity.resources
val metrics = res.displayMetrics
metrics.density = density
metrics.densityDpi = (160 * density).toInt()
metrics.scaledDensity = density * (scaledDensity / this.density)
scale = this.density / density
setBitmapDensity(metrics.densityDpi)
}
附录——核心代码
(其中部分方法的实现参阅Android常用工具类(基于Kotlin))
- DensityUtil.kt
import android.view.View
import android.app.Activity
import android.util.DisplayMetrics
import android.content.res.Configuration
import android.content.ComponentCallbacks
import com.oikawaii.library.core.android.app
import com.oikawaii.library.core.android.util.DimenUtil
import com.oikawaii.library.core.android.ktx.isPortrait
import com.oikawaii.library.core.android.ktx.isLandscape
/**
* @author Sukcria Miksria
* @version 2018/10/01.
**/
object DensityUtil : ComponentCallbacks {
var scale = 0f //屏幕缩放倍数
private var density: Float = 0f //Application的DisplayMetrics
private var scaledDensity: Float = 0f //
private lateinit var metrics: DisplayMetrics //Application的Density
/**
* 在Application中初始化Metrics
*/
fun init() {
//获取application的DisplayMetrics
metrics = app.resources.displayMetrics
//判断是否需要初始化
if (density != 0f) return
//初始化
density = metrics.density
scaledDensity = metrics.scaledDensity
//监听字体变化
app.registerComponentCallbacks(this)
}
/**
* 在Activity中初始化Density
* @param ruler UI的设计宽度,默认为420dp
*/
fun init(activity: Activity, ruler: Float = 420f, @Density flag: Int = Density.SHORT_SIDE_BASED) {
val f = activity.window.decorView.systemUiVisibility
val sf1 = View.SYSTEM_UI_FLAG_FULLSCREEN
val sf2 = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
val nf = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
val sf = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
val navigation = ((f and sf) == sf) || ((f and nf) == nf)
val status = ((f and sf) == sf) || ((f and sf1) == sf1) || ((f and sf2) == sf2)
init(activity, ruler, status, navigation, flag)
}
/**
* 在Activity中初始化Density
* @param ruler UI的设计宽度,默认为420dp
* @param status 设置基准尺寸时是否包含标题栏
* @param navigation 设置基准尺寸时是否包含虚拟键
*/
fun init(activity: Activity, ruler: Float = 420f, status: Boolean = false, navigation: Boolean = false, @Density flag: Int = Density.SHORT_SIDE_BASED) {
val width = DimenUtil.width(navigation)
val height = DimenUtil.height(status, navigation)
val pixels = when(flag) {
Density.WIDTH_BASED -> width
Density.HEIGHT_BASED -> height
Density.SHORT_SIDE_BASED -> if(app.isPortrait) width else height
Density.LONG_SIDE_BASED -> if(app.isLandscape) width else height
else -> 0
}
init(activity.resources.displayMetrics, pixels / ruler)
}
/**
* 在Activity中初始化Density
* @param portRuler UI竖屏时的设计宽度,默认为420dp
* @param landRuler UI横屏时的设计宽度,默认为980dp
*/
fun init(activity: Activity, portRuler: Float = 420f, landRuler: Float) {
val f = activity.window.decorView.systemUiVisibility
val nf = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
val sf = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
val navigation = ((f and sf) == sf) || ((f and nf) == nf)
init(activity, portRuler, landRuler, navigation)
}
/**
* 在Activity中初始化Density
* @param portRuler UI竖屏时的设计宽度,默认为420dp
* @param landRuler UI横屏时的设计宽度,默认为980dp
* @param navigation 设置基准尺寸时是否包含虚拟键
*/
fun init(activity: Activity, portRuler: Float = 420f, landRuler: Float, navigation: Boolean = false) {
val pixels = DimenUtil.width(navigation)
val ruler = if(app.isLandscape) landRuler else portRuler
init(activity.resources.displayMetrics, pixels / ruler)
}
/**
* 初始化Activity的Density
*/
private fun init(metrics: DisplayMetrics, density: Float) {
metrics.density = density
metrics.densityDpi = (160 * density).toInt()
metrics.scaledDensity = density * (scaledDensity / this.density)
scale = this.density / density
setBitmapDensity(metrics.densityDpi)
}
/**
* 设置 Bitmap 的默认屏幕密度
* 由于 Bitmap 的屏幕密度是读取配置的,需要使用反射强行修改
* @param density 屏幕密度
*/
private fun setBitmapDensity(density: Int) {
try {
val cls = Class.forName("android.graphics.Bitmap")
val field = cls.getDeclaredField("sDefaultDensity")
field.isAccessible = true
field.set(null, density)
field.isAccessible = false
} catch (e: ClassNotFoundException) {
} catch (e: NoSuchFieldException) {
} catch (e: IllegalAccessException) {
e.printStackTrace()
}
}
/**
* 字体变化时的回调
*/
override fun onLowMemory() {}
/**
* 字体变化时的回调
*/
override fun onConfigurationChanged(newConfig: Configuration) {
//字体改变后,将appScaledDensity重新赋值
if (newConfig.fontScale > 0) scaledDensity = metrics.scaledDensity
}
}
- Density.java
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* @author Sukcria Miksria
* @version 2019/10/04
*/
@IntDef({ Density.WIDTH_BASED, Density.HEIGHT_BASED, Density.LONG_SIDE_BASED, Density.SHORT_SIDE_BASED })
@Retention(RetentionPolicy.SOURCE)
public @interface Density {
int WIDTH_BASED = 1;
int HEIGHT_BASED = 2;
int LONG_SIDE_BASED = 3;
int SHORT_SIDE_BASED = 4;
}