Graphics->Density (下)

如何支持多屏幕配置

Android系统本身自动可以支持屏幕自适应,但有时也需要应用提供屏幕配置相关的资源以达到更好的用户体验:

  • 为不同屏幕大小提供不同的Layout.
  • 为不同屏幕密度提高不同的bitmap图像资源。

Android系统运行时根据当前屏幕配置选择合适的Layout和资源,如果对于的屏幕配置没有提供备选资源,则选用缺省资源进行缩放。

下表列出了Android屏幕配置可能的组合:






来看看apidemos中定义的资源目录:





比如对于drawable 来说, drawable 不带任何后缀,为缺省drawable 资源, drawable-hdpi 为高密度屏幕使用的drawable资源, drawable-ldpi 为低密度屏幕使用的drawable 资源等。

同样values 定义了多种不同配置下使用的资源。

对应不同屏幕密度下的Bitmap资源定义(.png. .jpg, .gif ,.9.png)一个原则上使用 3:4:6:8的比例来为四种不同屏幕密度提供图像资源,比如对于一个“中等密度”下像素大小为48X48的资源,

  • Low-density: 36X36
  • Medium-density: 48X48
  • High-density: 72X72
  • Extra high-density: 96X96






关于Density的一些额外的考虑

本节内容对于大部分应用不重要,除非你的应用在不同屏幕配置时显示上出了问题。

为了更好的理解在应用绘制图形时如何支持不同的屏幕密度配置,你应该知道下面由Android系统提供的一些支持:

Pre-scaling of resources (如bitmap drawable 资源)

Android系统根据当前设备屏幕配置不进行缩放选取设备配置相关的资源(比如对于高密度屏幕选取drawable-hdpi目录下的Drawable资源)。如果没有找到当前屏幕配置对应的资源,Android使用缺省资源(在drawable 目录下的资源)放大或是缩小以适应屏幕。Android应用认为缺省资源(比如drawable 等不带后缀的目录下)是对应于基准屏幕配置(mdpi)的资源。pre-scaling 意思就是Android系统为当前屏幕密度自动缩放Bitmap来适应屏幕的过程。

此时,如果你需要知道这些pre-scaled资源的大小,Android系统返回的是缩放之后的大小,比如对应于mdpi 下50X50像素大小的图像在hdpi屏幕下会放大到75X75像素大小,此时查询图像大小返回值为75X75. 如果你不想让Android对一些资源进行pre-scaling,可以将这些资源放在带nodpi后缀的目录下:比如 /res/drawable-nodpi。

Density 例子中的 no-dpi resource drawable 显示如下:




像素大小和坐标的自动缩放

一个应用也可以通过在Manifest文件中将android:anyDensity设为”false”或在代码中将bitmap的isScaled设为false关闭pre-scaling 。在这个情况下,Android系统会自动缩放以pixel为单位的的像素值和坐标。结果使得在不同屏幕大小的设备上仍可以显示和基准屏幕下类似的物理尺寸大小。这时如果需要返回Bitmap的大小,系统返回的是缩放后的像素值,而非实际物理像素值。

大部分情况“不应该关闭pre-scaling”,而是通过提供屏幕配置相关资源来支持多种屏幕配置。

运行时缩放Bitmap

如果应用在内存中动态创建一个Bitmap对应,Android系统会认为这个Bitmap对应于基准屏幕配置(中等密度)。缺省情况下,如果在屏幕上显示这个Bitmap,Android系统会自动缩放以适应当前屏幕。 可以使用setDensity为Bitmap 设置显示密度。

如果使用BitmapFactory来创建Bitmap对象,可以使用BitmapFactory.Options来定义Bitmap的属性,如果将inScaled设为false,则会关闭pre-sacling.

下图为Density例子 显示 pre-scale 和auto-scale 显示 low (120), medium (160), high (240) 的图像中高密度屏上显示的效果:





注意这个例子的Title : Density:High ,可能你的设备运行时结果不一样:

1
this .setTitle(R.string.density_title);

标题的资源为R.string.density_title ,它在values ,values-large ,values-small中用不同的定义, 在应用运行时,Android系统会根据当前屏幕配置选取合适的资源,你可以多创建几种不同大小的模拟器,看看结果会有所不同,由此也可以理解本例和Android选取资源的机制。

下表给出模拟器支持的不同屏幕大小和密度的组合:













好的,我来为您封装一个拖动LazyColumn的item可改变item位置组件。首先,我们需要添加以下依赖项: ```groovy // Jetpack Compose implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling:$compose_version" implementation "androidx.compose.foundation:foundation:$compose_version" implementation "androidx.compose.foundation:foundation-layout:$compose_version" // Drag and drop support implementation "androidx.compose.foundation:foundation-drag:$compose_version" ``` 然后,我们可以创建一个名为 `DraggableLazyColumn` 的组件,它将包含一个 `LazyColumn` 和一个 `Box`,用于渲染拖动操作。下面是完整的代码: ```kotlin import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.ParentDataModifier import androidx.compose.ui.layout.Placeable import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp @Composable fun <T> DraggableLazyColumn( items: List<T>, itemContent: @Composable (T) -> Unit, modifier: Modifier = Modifier, itemHeight: Int = 64.dp.toInt(), onItemMoved: (fromIndex: Int, toIndex: Int) -> Unit ) { val state = rememberLazyListState() val selectedIndexes = remember { mutableStateListOf<Int>() } val dragState = rememberDragState() Box(modifier = modifier) { LazyColumn(state = state) { itemsIndexed(items) { index, item -> val isSelected = selectedIndexes.contains(index) val draggableModifier = Modifier .dragged(dragState) { if (!isSelected) { selectedIndexes.clear() selectedIndexes.add(index) } } .selectable( selected = isSelected, onClick = { select(index, isSelected) } ) DraggableItem( modifier = draggableModifier, height = itemHeight, index = index, isSelected = isSelected, onItemMoved = onItemMoved ) { itemContent(item) } } } if (selectedIndexes.isNotEmpty()) { val height = selectedIndexes.size * itemHeight val y = dragState.position.y - height / 2 Box( modifier = Modifier .height(height.dp) .background(Color.White.copy(alpha = 0.8f)) .offset(y.dp) .dragged(dragState), contentAlignment = androidx.compose.ui.Alignment.Center ) { selectedIndexes.forEach { index -> DraggableItem( modifier = Modifier.selectable(selected = true) { select(index, true) }, height = itemHeight, index = index, isSelected = true, onItemMoved = onItemMoved ) { itemContent(items[index]) } } } } } } @Composable fun DraggableItem( modifier: Modifier, height: Int, index: Int, isSelected: Boolean, onItemMoved: (fromIndex: Int, toIndex: Int) -> Unit, content: @Composable () -> Unit ) { Surface( modifier = modifier, elevation = 8.dp, shape = RoundedCornerShape(8.dp), color = if (isSelected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface ) { Layout( content = content, modifier = Modifier.height(height.dp), measurePolicy = { measurables, constraints -> val placeables = measurables.map { measurable -> measurable.measure(constraints) } layout(constraints.maxWidth, height) { placeables.forEachIndexed { index, placeable -> val parentData = placeable.parentData as DraggableItemParentData parentData.measurable = measurables[index] parentData.placeable = placeable } } } ) { measurables, constraints -> val placeables = measurables.map { measurable -> val parentData = measurable.parentData as DraggableItemParentData parentData.measurable = measurable parentData.placeable!! } layout(constraints.maxWidth, height) { placeables.forEach { placeable -> placeable.placeRelative(0, 0) } } } } } private class DraggableItemParentData(var measurable: Measurable? = null, var placeable: Placeable? = null) : ParentDataModifier { override fun Density.modifyParentData(parentData: Any?): Any { return DraggableItemParentData() } } @Composable private fun Modifier.dragged(dragState: DragState, onDrag: () -> Unit = {}): Modifier { return pointerInput(Unit) { detectTapGestures(onLongPress = { dragState.onLongPress() }) detectDragGestures( onDragStart = { dragState.onStart(it) }, onDrag = { dragState.onDrag(it); onDrag() }, onDragEnd = { dragState.onEnd() } ) } } private fun Modifier.selectable(selected: Boolean, onClick: () -> Unit): Modifier { return if (selected) { this.background(Color.LightGray) } else { this }.pointerInput(Unit) { detectTapGestures(onDoubleTap = onClick) } } private class DragState { var isDragging by mutableStateOf(false) var position by mutableStateOf(Offset.Zero) var offset by mutableStateOf(Offset.Zero) var index by mutableStateOf(-1) fun onLongPress() { isDragging = true } fun onStart(offset: Offset) { this.offset = offset } fun onDrag(offset: Offset) { this.position = offset - this.offset } fun onEnd() { isDragging = false } } @Composable private fun rememberDragState(): DragState { return remember { DragState() } } @Composable private fun LayoutScope.itemsIndexed( items: List<Any>, itemContent: @Composable (Int, Any) -> Unit ) { items.forEachIndexed { index, item -> itemContent(index, item) } } private fun Modifier.offset(y: Int): Modifier { return this.then(Modifier.offset { IntOffset(0, y) }) } private fun select(index: Int, isSelected: Boolean) { if (isSelected) { selectedIndexes.remove(index) } else { selectedIndexes.add(index) } } private var selectedIndexes = mutableStateListOf<Int>() ``` 在上述代码中,我们使用 `LazyColumn` 显示所有的项目。每个项目都被包装在 `DraggableItem` 组件中,该组件可以响应拖动手势。我们还创建了一个 `DragState` 类来跟踪拖动操作的状态。 当用户长按某个项目时,我们将启动拖动状态,并将该项目添加到选定的项目列表中。拖动状态时,我们将在拖动操作的位置显示一个 `Box`,其中包含所有选定的项目。 当用户松开鼠标按钮时,我们会在列表中更新项目的顺序,并将拖动操作状态重置为初始状态。 现在,我们可以在调用 `DraggableLazyColumn` 时将项目列表作为参数传递。我们还需要提供一个 `itemContent` 函数,该函数接受单个项目并呈现其内容。我们还可以指定项目的默认高度,以及在移动项目时调用的回调函数。 下面是一个使用示例: ```kotlin val items = List(20) { "Item $it" } DraggableLazyColumn( items = items, itemContent = { item -> Text(text = item) }, itemHeight = 64, onItemMoved = { fromIndex, toIndex -> val movedItem = items.removeAt(fromIndex) items.add(toIndex, movedItem) } ) ``` 这个例子将呈现一个包含 20 个项目的列表,每个项目的高度为 64dp。当用户长按某个项目并开始拖动时,其他选定的项目将显示在拖动操作的下方。当用户松开鼠标按钮时,项目列表的顺序将更新。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值