适配

今日头条屏幕适配方案中对横竖屏切换的优化

2019-11-04  本文已影响0人  吼啦迷迭吼啦哟

今日头条屏幕适配方案中对横竖屏切换的优化

================

项目地址

今日头条屏幕适配方案 & 横竖屏切换优化

开始使用


dependencies {

    implementation 'com.oikawaii.library:core:1.0.0'

    implementation 'com.oikawaii.library:density:1.0.0'

}

使用方法


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()

    }

}


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(伪),只需要这样就可以了:


<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_gravity="center"

    android:layout_height="420dp" >

    <TextView

        android:background="#3CB6F8"

        android:layout_width="420dp"

        android:layout_height="420dp" />

    <TextView

        android:layout_alignParentStart="true"

        android:layout_alignParentLeft="true"

        android:layout_alignParentTop="true"

        android:background="#FF554C"

        android:layout_width="210dp"

        android:layout_height="210dp" />

    <TextView

        android:layout_alignParentBottom="true"

        android:layout_alignParentRight="true"

        android:layout_alignParentEnd="true"

        android:background="#72D44F"

        android:layout_width="210dp"

        android:layout_height="210dp" />

</RelativeLayout>

效果图(竖屏)

但此时横屏是这样的:

效果图(横屏)

此时横屏出现了两个问题,正方形显示不完整与正方形错位。

解决横屏错位

横屏错位的原因很简单,解决前先看看这个程序在Android 10下的运行效果。

效果图(横屏/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 BarStatus 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 BarStatus 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))


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

    }

}


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;

}

上一篇下一篇

猜你喜欢

热点阅读