跳至主要內容

Compose 屏幕适配

JI,XIAOYONG...大约 4 分钟

Compose 屏幕适配

一种Compose中屏幕适配的解决方案,灵感参考头条屏幕适配AndroidAutoSize等,以设计稿宽度和屏幕水平方法大小为准,等比拉伸控件大小。

后文附有本方案的 Kotlin 语言实现,使用只需要两个步骤即可:

// 1 初始化
class MainApp : Application() {
    override fun onCreate() {
        super.onCreate()
        SizeEtx.init(this, 375) // 375 为设计稿宽度
    }
}

// 2 使用
size(width = 9.composeDp, height = 16.composeDp)

主要的设计思想

假设如下变量:设计稿总宽度dpx,控件在设计稿中的大小n,屏幕的实际水平 dp 大小rdp,以及我们需要求得的控件在设备中的 dp 值m

那么我们不难得到以下方程:

n / dpx = m / rdp

也就可以推导出:

m = n * (rdp / dpx)

上述值中,只有屏幕水平 dp 值rdp还是未知的,又根据**(density 在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度wpx / density = 屏幕的总 dp 宽度rdp)**可知:

rdp = wpx / density

所以,我们可以推导出:

m = n * (rdp / dpx)
  = n * ( wpx / density ) / dpx
  = n * wpx / (density * dpx)

到这里,等式后面的所有数据都为已知或者在 app 运行时可知,由此我们可以计算出设计稿中的控件在 Compose 中对应的 dp 大小。

下面是以上思路的 kotlin 实现:

import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Point
import android.os.Build
import android.view.WindowManager
import androidx.compose.ui.unit.Dp

/**
 * @author : jixiaoyong
 * @description:Compose 屏幕适配方案
 *
 * 根据设计稿宽度(设计稿宽度对应设备水平方向)和设计稿对应物体大小,计算实际应该填写的 dp
 *
 * 使用:
 * 在 Application onCreate() 方法中执行
 * SizeEtx.init(this, 375)
 * 其中 375 位设计稿屏幕宽度,然后在代码中使用 width(315.composeDp) 作为大小单位即可,
 * 其中 315 为设计稿中的控件大小
 *
 * 计算方式为:
 * wpx 屏幕实际像素宽度
 * dpx 设计稿屏幕宽度
 * n 控件设计稿中的宽度(dp、px 都可,与 dpx 单位保持一致)
 * m 控件在 app 中对应的 dp
 * rpx 控件在屏幕中应该展示的像素大小
 * 已知条件:dp = px / density
 * (density 在每个设备上都是固定的,DPI / 160 = density,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度)
 *
 * DisplayMetrics#density 就是上述的 density
 * DisplayMetrics#densityDpi 就是上述的 dpi
 *
 * 综上得出如下结论 (以竖屏情况下屏幕宽度为例):
 * 屏幕宽度总 dp:rdp = wpx / density
 * m / rdp = n / dpx
 * 那么,m = (rdp / dpx) * n
 * 其中 (rdp / dpx) 被我们当做设计稿中控件大小与设备中控件 dp 大小之间的缩放系数:dpWidthScale
 * 所以:m = dpWidthScale * n
 *
 * @email : jixiaoyong1995@gmail.com
 * @date : 2021/8/2
 */
class SizeEtx private constructor(context: Context, dpx: Int) {

    init {
        val density = Resources.getSystem().displayMetrics.density
        var wpx = Resources.getSystem().displayMetrics.widthPixels
        dpWidthScale = wpx.toFloat() / (dpx * density)
        dpHeightScale =
            getScreenRealHeightPx(context).toFloat() / (dpx * density)
        pxWidthScale = wpx.toFloat() / dpx.toFloat()
// 以下数据为 Redmi Note 5 的测试数据
//        "getScreenRealHeight ${getScreenRealHeightPx(context)}".logd() // 2160,实际设备高度为 2160
//        "Resources.getSystem().displayMetrics.heightPixels ${Resources.getSystem().displayMetrics.heightPixels}".logd() // 2033,实际设备高度为 2160
//        "Resources.getSystem().displayMetrics.density ${Resources.getSystem().displayMetrics.density}".logd() // 2.7
//        "Resources.getSystem().displayMetrics.densityDpi ${Resources.getSystem().displayMetrics.densityDpi}".logd() // 432
    }

    /**
     * 获得屏幕真实高度(包含底部导航栏)
     */
    private fun getScreenRealHeightPx(context: Context): Int {
        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val display = windowManager.defaultDisplay
        val outPoint = Point()
        if (Build.VERSION.SDK_INT >= 19) {
            // 可能有虚拟按键的情况
            display.getRealSize(outPoint)
        } else {
            // 不可能有虚拟按键
            display.getSize(outPoint)
        }
        // 手机屏幕真实高度
        return outPoint.y
    }

    companion object {
        /**
         * 初始化大小适配工具类
         * @param context
         * @param dpx 设计稿中的屏幕宽度,例如 375,在使用到本工具的所有地方,都应该以此宽度为准来获取其他控件的大小
         */
        fun init(context: Context, dpx: Int) {
            SizeEtx(context, dpx)
        }

        var dpWidthScale = 1.0f
        var dpHeightScale = 1.0f
        var pxWidthScale = 1.0f
        var pxHeightScale = 1.0f
    }
}

// Compose 屏幕适配方案
/**
 * 获取 Compose 中对应的 dp,输入值为设计稿中对应的控件大小
 */
inline val Number.composeDp: Dp
    get() {
        val isPortrait = isPortrait()
        return Dp(this.toFloat() * if (isPortrait) SizeEtx.dpWidthScale else SizeEtx.dpHeightScale)
    }

/**
 * 获取 Compose 中对应的 px,输入值为设计稿中对应的控件大小
 */
inline val Number.composePx: Int
    get() {
        val isPortrait = isPortrait()
        return (this.toFloat() * if (isPortrait) SizeEtx.pxWidthScale else SizeEtx.pxHeightScale).toInt()
    }

// 是否竖屏
fun isPortrait() =
    Resources.getSystem().configuration.orientation == Configuration.ORIENTATION_PORTRAIT

参考文章

AndroidAutoSizeopen in new window

一种极低成本的 Android 屏幕适配方式open in new window

文章标题:《Compose 屏幕适配》
本文作者: JI,XIAOYONG
发布时间: 2021/08/03 11:51:32 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/e15dda2e.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8