与iOS不同,Android总是被设想为一个平台,该平台将在不同制造商创建的各种设备和屏幕类型上运行。
因此,Android具有多种与不同的物理屏幕类型配合正常的UI的方法,并且您可能会感到有些惊讶(至少我是),这些技术在JetPack构成时大大删除了这些技术。
- 如果您是Android的新手,情况可能会有些混乱,尤其是当Pixel密度播放时。
- 如果您是一只老手
因此,这是现代Android开发人员在开发UIS时需要考虑的内容的摘要
我们的自适应计数器应用在分开屏幕模式下显示,字体大小设置占用另一个窗口
我们首先审查密度独立性,然后讨论响应式和适应性设计之间的差异。但是,请随时直接跳到Windowsize类讨论here
我希望东西看起来同样的
让我们首先将屏幕密度弄清楚,因为这是一个非常简单的解决方案。
问题:
物理像素的尺寸不是相同的...它们甚至都不是全方位
如果您在屏幕上显示50x50像素元素,由大像素(您可以通过人眼轻松看到的类型)组成,将看起来相当大(无论实际屏幕的物理大小如何) 。
屏幕上具有更高像素密度的相同的50x50像素元素(即小像素,例如如此小的类型,您甚至看不到它们)会呈现得小得多。
这可能不是您想要的。通常,您希望该元素看起来相同的物理大小,无论它在显示在上面的屏幕上是什么像素密度(这与屏幕本身的大小分开)
因此,我们倾向于用DP来指定内容,DP是一种虚拟像素,可以用1个物理像素呈现(如果屏幕具有很大的像素)或可能是2.5个物理像素(如果屏幕很小,则像素)
显然,我在这些示例中使用了图像,但是插条,布局尺寸等也存在相同的问题。
无论如何,我会说屏幕密度问题主要是在Android上解决的问题(尤其是当偏爱矢量图像而不是基于栅格的图像时)。我们只需要理解这个问题,避免用像素来指定内容。 JetPack组成的API也很好地鼓励您使用DP而不是像素来指定尺寸。
我希望事情看起来很大,在大屏幕上
问题:
有时您实际上并不希望物理大小相同。
有时,如果有更多空间,您希望您的屏幕元素占用更多空间。如果您的单身型设计被挤入一个小角落,将所有可爱的空间放在平板电脑显示屏上有什么意义?
Pre JetPack组成的解决方案是使用尺寸桶(除了某些屏幕尺寸断点之外,盒子的尺寸还可以从其他桶中取出)。因此,它可能是针对小屏幕的50dp x 50dp元素,但是大屏幕的100dp x 100dp元素。
注意:我们在此处考虑的屏幕大小在DP中指定,因此它们完全独立于像素密度。的确,较小的屏幕通常更便宜并且具有较大的像素。而且更大,更昂贵的屏幕通常也具有更高的像素密度。但是这种关系不能保证。屏幕尺寸和像素密度是完全独立的,需要单独考虑。
在作曲中,大小的存储桶系统被Windowsize类替换。然而,我认为在撰写上下文中最有意义的是,尽可能使用可用空间的百分比/比例来设计我们的UI。
假设元素应是屏幕宽度的25%。这是相同的值,无论我们的屏幕是小手机还是大平板电脑,还是(至关重要的)介于两者之间。特定屏幕大小属于两个窗口之间的边界,基于百分比的布局仍然可以正常工作。
该技术的另一个主要优势是减少了所需的开销。设计,开发和测试具有多组尺寸的尺寸桶装式UIS,对于多个屏幕尺寸,对于大型项目而言,这是一个重大挑战。设计,开发和测试团队也可以很好地理解基于百分比的尺寸。
对于基本元素尺寸,fillMaxWidth()
和fillMaxHeight()
在这方面非常有用,因为它们接受分数参数:
modifier = Modifier.fillMaxWidth(0.25f)
要从可用空间得出其他值,BoxWithConstraints
也非常有用:
BoxWithConstraints {
val derivedDimension = this.maxWidth * 0.10f // 10% of the width
Box(modifier = Modifier.padding(derivedDimension)){
// content
}
}
虽然在这里小心。根据您父母的视图的不同,MaxWidth或Maxheight的值有时可以是Infinity.dp
,在下面有一个解决方案
我想要不同的屏幕sizeS1111111111111111111111111111111
问题:
将相同的设计比例更大或更小,只会使您走得太远,有时实际的设计需要更改
假设我们有一个电视指南的设计,希望我们的用户喜欢在其Android平板电脑上使用。也许我们可以有一个不错的大网格,每个频道的行,该频道上的程序从左到右。
如果我们根据屏幕百分比指定了尺寸,则可能也可以在手机上使用相同的设计(它会起作用,看起来只会小一点)。
但是,将有一个扩展事物无法正常工作的点。手表尺寸显示屏上的相同设计将无法使用。这需要完全不同的电视指南UI。
Android文档称之为“自适应设计”(与响应式设计相反),更适合用户的屏幕尺寸代替布局。如果我们达到这一点,我们需要谈论Windowsize类...
Windowsize类
当前的Android文档确实有一些关于此的advice,但是建议非常接近说“自己动手做”。如果您查看Kotlin视图示例:
尤其如此。
enum class WindowSizeClass { COMPACT, MEDIUM, EXPANDED }
fun computeWindowSizeClasses() {
val widthDp = //... from WindowMetricsCalculator
val widthWindowSizeClass = when {
widthDp < 600f -> COMPACT
widthDp < 840f -> MEDIUM
else -> EXPANDED
}
val heightDp = //... from WindowMetricsCalculator
val heightWindowSizeClass = when {
heightDp < 480f -> COMPACT
heightDp < 900f -> MEDIUM
else -> EXPANDED
}
// Use widthWindowSizeClass and heightWindowSizeClass.
}
至少使用撰写版本,他们为您提供了该功能,这大约是相同的事情:
val windowSizeClass = calculateWindowSizeClass(this)
大概是这样使用的:
val boxPadding = when (windowSizeClass.widthSizeClass) {
Compact -> 2.dp
Medium -> 10.dp
else -> 25.dp
}
Box(
modifier = Modifier.padding(boxPadding)
) {
// box content
}
或
Box {
if (windowSizeClass.widthSizeClass == Compact) {
SomeSmallLayout()
} else {
SomeLargeLayout()
}
}
我对此代码的基本 /临时构度感到有些惊讶,但是该软件包目前仍然标记为实验。< / p>
首先,它看起来不可维护。随着较大复杂的UI,所有if
语句都可能开始成为问题。
这些类是硬编码的断点也有些冒险,而我们只为宽度和高度提供了尺寸类(例如方向或最小维度)
对于使用此代码的人可能不明显的东西,直到他们需要为小屏幕实施UI:尺寸本身是巨大的。紧凑型覆盖宽度最高600dp的所有内容(这意味着无法区分大型手机,小型手机或手表尺寸屏幕)。
当然,很多时候您都不需要那种粒度,但是有时您会。而且,如果我们已经使用一定的断点方案构建了该应用程序的一半,那么意识到它不做我们需要做的事情,我们要处理的下一个UI。
// width
COMPACT < 600 <= MEDIUM < 840 <= EXPANDED
// height
COMPACT < 480 <= MEDIUM < 900 <= EXPANDED
另外,这可能只是我的个人问题,但是命名...中小型,大,不够好?这不是星巴克ð
Maven软件包此代码来自Material3-Window-Size级,并且可能会改善,因为目前它仍然是实验性的。
但断点是从Material3 design guidelines本身中获取的,因此它们可能不会改变,并且它们可能不是您真正想要的(文档确实声称它是“自以为是”的,我认为这就是原因)。
麻烦是它不能满足:
的项目需求- a)除了选择一些组件和设计元素或 之外,对材料3的设计不感兴趣
- b)希望选项编写将在任何尺寸的Android屏幕上使用的UI代码,并且不要以小于600DP的肖像宽度为同一设备!
即使是在Material2中讨论并由材料3文档引用的水平网格系统,也无法使用Material3-Window-Size级级别提供的断点。例如:
- 600DP 是从4列切换到8列的断点
- 905DP 切换到12列,带有缩放边距
- 1240DP 边距不再扩展,但内容确实
- 1440dp 内容停止缩放,边距再次接管
我怀疑会有很多开发人员没有时间阅读这样的文章,或者自己调查了材料3断点自己,并以这些硬编码的断点来遗憾地遗憾地构建了UIS。我猜只有时间告诉我。
尝试更好的Windownize类
首先,在理想的Windowsize方案中,我们希望能够基于仅小的VS大量选择一个优先的值。许多应用程序项目都希望支持设计的两个版本,仅此而已。这是我们将如何指定上述响应式盒装示例:
val boxPadding = WidthBasedDp(s = 5.dp, l = 53.dp)
也许我们有一些相当特定的设计,这些设计需要不同的屏幕尺寸值,大致相当于:手表,小型手机,大手机,平板电脑,大台式机
val boxPadding = WidthBasedDp(
xs = 2.dp,
s = 3.dp,
m = 5.dp,
l = 20.dp,
xl = 50.dp
)
有很多选择值的方法,它并不总是是宽度,因此我们也可以做到这一点:
val boxPadding = HeightBasedDp(s = 3.dp, m = 5.dp, l = 20.dp)
val boxPadding = AspectBasedDp(port = 3.dp, land = 15.dp, squarish = 5.dp)
val boxPadding = MinDimBasedDp(s = 3.dp, m = 5.dp, l = 20.dp)
当然,我们不仅需要自适应DPS,有时也要基于Windowsize类也有浮标或textunit是有用的:
val myInt = WidthBasedInt(s = 3, m = 5, l = 20)
val myFontSize = AspectBasedTextUnit(port = 3.em, land = 6.sp)
val myFloat = HeightBasedFloat(s = 3.fl, l = 20.fl)
实际上,为什么不允许我们根据Windowsize类选择任何内容:
val myLabel = AspectBasedValue<String>(
port = "the view is portrait",
land = "the view is landscape",
squarish = "the view is approximately square"
)
最后,我们需要能够在我们想要完全不同的布局的情况下根据Windowsize类选择一个可复合的内容:
private val MyAdaptiveLayout = MinDimBasedComposable(
s = { SomeSmallLayout() },
l = { SomeLargeLayout() },
)
使我们能够在使用时保持我们的撰写布局代码更加清晰:
Box {
MyAdaptiveLayout(size)
}
实际上,以上所有内容都应减少您在后续布局代码(少if
语句)中需要进行的分支量
,当然,我们希望每当屏幕尺寸更改时(例如,当用户以拆分屏幕模式显示您的应用程序或打开其可折叠)时对其进行重新组装)
val size: WindowSize = rememberWindowSize()
为方便起见,当我们想使用屏幕尺寸来计算其他维度(请参见上文re boxwithconstraints),我剩下的原始dpsize用于计算自身内部的Windowsize类,以便访问:
data class WindowSize(
val width: Width,
val height: Height,
val minDim: MinDim,
val aspect: Aspect,
val dpSize: DpSize,
)
实现可比较的(如材料3 Windowsizeclass所做的那样)也可能很有用,这样我们就可以在我们绝对必须这样做这样的事情:
时执行这样的事情。
if (size.height >= Height.Medium) {
...
}
(不过,imo,这是一个反模式,因为它将命令式代码引入我们的布局。我几乎不支持它,但是毫无疑问,您有时可以为此做一个理由,出于务实的原因或其他原因)
最后,这是一个边缘情况,但是下面的示例应用程序中的Windowsize实现可以像这样组合:
val shape = MinDimBasedValue(
xs = AspectBasedValue(
port = CircleShape,
land = CircleShape,
squarish = RectangleShape
),
m = FixedValue(CircleShape),
l = FixedValue(CircleShape),
)
将形状设置为圆,除非:1)最小尺寸为xs and 2)窗口是平方的。
这有点笨拙,老实说,我唯一不满意的是,您需要这样使用它:shape(size)(size)
,而不仅仅是shape(size)
。 (如果您能想到一种使它变得更好的简单方法,请给我发送PR!)
断点
提醒您,材料3的DP值为:
// width
COMPACT < 600 <= MEDIUM < 840 <= EXPANDED
// height
COMPACT < 480 <= MEDIUM < 900 <= EXPANDED
这些是我认为对于Android UI的断点:
// width
XS < 250 <= S < 400 <= M < 500 <= L < 900 <= XL
// height
XS < 250 <= S < 700 <= M < 900 <= L < 1280 <= XL
这就是为什么我认为:
并绘制在图上
这是一个相当小的设备样本,但足以突出显示材料3断点的一些简短启动。
OK OK 99.96%的肖像模式中的手机 <600dp的宽度是宽度,但这有用吗?注意:这与说99.96%的设备<600dp的宽度是partorait模式的手机。
(无论如何,电话屏幕的定义是什么?如果您将电话屏幕定义为宽度<600dp宽度,那么它是100%的,不是ðü)
这是材料3启发的紧凑断点,以维恩图表示:
因此,有许多带有屏幕小于600dp宽度的Android设备,我们可能希望单独设计。但是我们也不能忘记诸如“拆分屏幕”之类的模式。在拆分屏幕模式下,许多较大的手机都可以为您的应用程序提供相当于非常小的手机的窗口尺寸。使用“图片中的图片”或“自由形式”模式,所有赌注都关闭了。
例如,我现在有一个三星平板电脑,它提供了一种“弹出”模式,使用户能够将示例计数器应用程序的窗口调整到任意大小。
材料3设计断点不考虑这些情况中的任何一个,因为它是一个非常专注于屏幕尺寸尺度较大端的设计系统。
数据是here,如果您遇到任何可能值得添加
的离群设备,请告诉我无论我们选择哪个断点,我们都希望它们是可配置的,以防万一。如果我们想在库中构建这些值,那肯定是正确的。无法更改它们的方法是要麻烦。
,但是只要我们有可能做这样的事情,事情应该可以了:
BreakPoints.overrideBreakPoints(
ViewPortBreakPoints(
widthSDpBelow = 400.dp,
widthMDpBelow = 500.dp,
)
)
支持所有这些的代码是here(它的代码少于500行,因此没有太多)。 Breakpoints.kt中有一个关于如何覆盖值以重新创建材料3 Windowsize类的行为的注释。
。我可能最终将其承诺给fore compose package,但是直到我(或者如果不愿意)之前,请继续将其复制到您的项目中(如果您有所改善,请告诉我在上面!)
示例应用程序
我整理了一个非常基本的计数器应用程序,演示了我们到目前为止讨论过的所有代码的使用。
我发现的东西非常有用,就是拥有一个单个预览函数,该功能使用Windowsize断点来为您在一个视图中为您生成所有边缘案例预览尺寸
@Preview(widthDp = wXS_low, heightDp = hXS_low)
@Preview(widthDp = wXS_high, heightDp = hXS_high)
@Preview(widthDp = wS_low, heightDp = hS_low)
@Preview(widthDp = wS_high, heightDp = hS_high)
@Preview(widthDp = wM_low, heightDp = hM_low)
@Preview(widthDp = wM_high, heightDp = hM_high)
@Preview(widthDp = wL_low, heightDp = hL_low)
@Preview(widthDp = wL_high, heightDp = hL_high)
@Preview(widthDp = wXL_low, heightDp = hXL_low)
@Preview(widthDp = wXL_high, heightDp = hXL_high)
@Composable
fun MyPreview() {
PreviewWithWindowSize {
MyLayout(size = it)
}
}
在上面的情况下,我们的Windowsize类是直接从预览注释上设置的宽度和高度尺寸创建的,然后将其传递到我们的组合布局中。
这给我一个看起来像这样的视图,所以我可以一目了然地告诉设计是否有任何边缘案例问题
看起来可能有些奇怪,所以在这里实际发生的事情是根据屏幕的宽度尺寸选择形状的颜色
val color = WidthBasedValue(
xs = Color.Red,
s = Color.Green,
m = Color.Blue,
l = Color.Magenta,
xl = Color.Gray
)
预览尺寸已被故意选择,以便它们存在于Windowsize类边界上,因为它在那里我们更有可能发现不起作用的边缘案例。
例如,第四预览是我们可以拥有的最大可能的屏幕,它仍然算作S(这就是形状为绿色的原因)。第五预览仅比第四预览大1.dp,是我们仍然可以算作M的最小屏幕(这就是为什么形状为蓝色)。
因此,前两个预览是XS宽度,接下来是S宽度,依此类推,直到我们到达均为XL宽度的灰色形状。
您会注意到形状有时是椭圆形的,有时是矩形。这是因为根据屏幕的方向选择形状,如下所示:
val shape = AspectBasedValue(
port = CircleShape,
land = CircleShape,
squarish = RectangleShape
)
字体大小,按钮和形状边框的厚度直接从屏幕尺寸缩放。由于我们的Windowsize类包括最初用于创建它的窗口的dpsize,因此我们使用该值来运行计算(这避免了我们在使用BoxWithConstraints上看到的陷阱)
val minimumDimension = size.dpSize.minimumDimension()
val borderThickness = minimumDimension * 0.10f
val boxHeight = minimumDimension * 0.50f
val numberFontSize = (minimumDimension / 5f).value.sp
最后,还有两个略有不同的诊断组合,这些组合是根据屏幕的宽度类选择的
@Composable
fun BoxScope.DiagnosticInfo(size: WindowSize) {
WidthBasedComposable(
xs = { sz -> MiniDiagnostics(sz) },
m = { sz -> MiniDiagnostics(sz) },
l = { sz -> RegularDiagnostics(sz) },
)(size)
}
在布局中使用这样的使用:
Box {
DiagnosticInfo(size)
}
好吧,这很长,感谢您坚持到最后!
包括预览注释和所有Windowsize代码的示例应用程序可在github git clone git@github.com:erdo/compose-windowsize.git