Add experimental app widget compose reimplementation
This commit is contained in:
parent
2e4100f676
commit
15cb4b4f29
@ -0,0 +1,190 @@
|
||||
package de.mm20.launcher2.ui.component.view
|
||||
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.core.view.children
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* A reimplementation of View using composables.
|
||||
* Only views that are supported by RemoteViews are supported here.
|
||||
*/
|
||||
@Composable
|
||||
fun ComposeAndroidView(
|
||||
view: () -> View,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var view_ by remember { mutableStateOf<View?>(null) }
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(Unit) {
|
||||
view_ = view()
|
||||
onDispose {
|
||||
view_ = null
|
||||
}
|
||||
}
|
||||
if (view_ != null) {
|
||||
ComposeAndroidView(view_!!, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ComposeAndroidView(
|
||||
view: View,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (view.visibility == View.GONE) {
|
||||
return
|
||||
}
|
||||
|
||||
if (view.visibility == View.INVISIBLE) {
|
||||
Spacer(modifier = modifier)
|
||||
}
|
||||
|
||||
val mod = modifier.view(view)
|
||||
|
||||
when (view) {
|
||||
is FrameLayout -> ComposeFrameLayout(view, mod)
|
||||
is LinearLayout -> ComposeLinearLayout(view, mod)
|
||||
is ListView -> ComposeListView(view, mod)
|
||||
is TextView -> ComposeTextView(view, mod)
|
||||
is ImageView -> ComposeImageView(view, mod)
|
||||
is ViewGroup -> {
|
||||
Column(modifier = mod) {
|
||||
for (child in view.children) {
|
||||
ComposeAndroidView(
|
||||
child,
|
||||
modifier = Modifier.layoutParams(child.layoutParams)
|
||||
)
|
||||
}
|
||||
Text(view.javaClass.toString(), modifier = mod)
|
||||
}
|
||||
}
|
||||
|
||||
else -> Text(view.javaClass.toString(), modifier = mod)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Modifier.view(view: View): Modifier = this then Modifier.composed {
|
||||
val density = LocalDensity.current
|
||||
val backgroundDrawable = view.background
|
||||
val background = when (backgroundDrawable) {
|
||||
is ColorDrawable -> Modifier.background(Color(backgroundDrawable.color))
|
||||
is Drawable -> Modifier.drawBehind {
|
||||
backgroundDrawable.setBounds(
|
||||
0, 0, size.width.roundToInt(), size.height.roundToInt()
|
||||
)
|
||||
backgroundDrawable.draw(this.drawContext.canvas.nativeCanvas)
|
||||
}
|
||||
|
||||
else -> Modifier
|
||||
}
|
||||
|
||||
val padding = with(density) {
|
||||
Modifier.padding(
|
||||
start = view.paddingStart.toDp(),
|
||||
top = view.paddingTop.toDp(),
|
||||
end = view.paddingEnd.toDp(),
|
||||
bottom = view.paddingBottom.toDp()
|
||||
)
|
||||
}
|
||||
|
||||
val clickable = when {
|
||||
view.isClickable || view.isLongClickable -> Modifier.combinedClickable(
|
||||
enabled = view.isEnabled,
|
||||
onClick = {
|
||||
if (view.isClickable) view.performClick()
|
||||
},
|
||||
onLongClick = { view.performLongClick() }
|
||||
)
|
||||
|
||||
view.isClickable -> Modifier.clickable(
|
||||
enabled = view.isEnabled,
|
||||
onClick = { view.performClick() }
|
||||
)
|
||||
|
||||
else -> Modifier
|
||||
}
|
||||
|
||||
val graphicsLayer = Modifier.graphicsLayer {
|
||||
translationX = view.translationX
|
||||
translationY = view.translationY
|
||||
rotationX = view.rotationX
|
||||
rotationY = view.rotationY
|
||||
rotationZ = view.rotation
|
||||
scaleX = view.scaleX
|
||||
scaleY = view.scaleY
|
||||
shadowElevation = view.elevation
|
||||
cameraDistance = view.cameraDistance
|
||||
}
|
||||
|
||||
background then clickable then padding then graphicsLayer
|
||||
}
|
||||
|
||||
|
||||
internal fun Modifier.layoutParams(params: ViewGroup.LayoutParams?) = this then Modifier.composed {
|
||||
params ?: return@composed Modifier
|
||||
val density = LocalDensity.current
|
||||
|
||||
val margins = if (params is ViewGroup.MarginLayoutParams) {
|
||||
with(density) {
|
||||
Modifier.padding(
|
||||
start = params.marginStart.coerceAtLeast(0).toDp(),
|
||||
top = params.topMargin.coerceAtLeast(0).toDp(),
|
||||
end = params.marginEnd.coerceAtLeast(0).toDp(),
|
||||
bottom = params.bottomMargin.coerceAtLeast(0).toDp()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
val width = when (params.width) {
|
||||
ViewGroup.LayoutParams.MATCH_PARENT -> Modifier.fillMaxWidth()
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT -> Modifier.wrapContentWidth()
|
||||
else -> Modifier.width(with(density) { params.width.toDp() })
|
||||
}
|
||||
|
||||
val height = when (params.height) {
|
||||
ViewGroup.LayoutParams.MATCH_PARENT -> Modifier.fillMaxHeight()
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT -> Modifier.wrapContentHeight()
|
||||
else -> Modifier.height(with(density) { params.height.toDp() })
|
||||
}
|
||||
|
||||
margins then width then height
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package de.mm20.launcher2.ui.component.view
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.view.children
|
||||
|
||||
|
||||
@Composable
|
||||
internal fun ComposeFrameLayout(
|
||||
view: FrameLayout,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.TopStart,
|
||||
) {
|
||||
for (child in view.children) {
|
||||
ComposeAndroidView(
|
||||
child,
|
||||
modifier = Modifier.frameLayoutChild(
|
||||
this@Box,
|
||||
child.layoutParams
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.frameLayoutChild(scope: BoxScope, params: ViewGroup.LayoutParams) = this then
|
||||
Modifier.layoutParams(params) then
|
||||
with(scope) {
|
||||
if (params !is FrameLayout.LayoutParams) return@with Modifier
|
||||
val alignment = when (params.gravity) {
|
||||
Gravity.START or Gravity.TOP -> Alignment.TopStart
|
||||
Gravity.START or Gravity.BOTTOM -> Alignment.BottomStart
|
||||
Gravity.END or Gravity.TOP -> Alignment.TopEnd
|
||||
Gravity.END or Gravity.BOTTOM -> Alignment.BottomEnd
|
||||
Gravity.START or Gravity.CENTER_VERTICAL -> Alignment.CenterStart
|
||||
Gravity.END or Gravity.CENTER_VERTICAL -> Alignment.CenterEnd
|
||||
Gravity.CENTER_HORIZONTAL or Gravity.TOP -> Alignment.TopCenter
|
||||
Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM -> Alignment.BottomCenter
|
||||
Gravity.CENTER_HORIZONTAL or Gravity.CENTER_VERTICAL -> Alignment.Center
|
||||
else -> Alignment.TopStart
|
||||
}
|
||||
Modifier.align(alignment)
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package de.mm20.launcher2.ui.component.view
|
||||
|
||||
import android.graphics.BlendModeColorFilter
|
||||
import android.graphics.ColorMatrixColorFilter
|
||||
import android.widget.ImageView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
@Composable
|
||||
internal fun ComposeImageView(
|
||||
view: ImageView,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = modifier.alpha(view.imageAlpha / 255f),
|
||||
model = view.drawable,
|
||||
contentDescription = view.contentDescription?.toString(),
|
||||
contentScale = when (view.scaleType) {
|
||||
ImageView.ScaleType.CENTER -> ContentScale.None
|
||||
ImageView.ScaleType.FIT_XY -> ContentScale.FillBounds
|
||||
ImageView.ScaleType.FIT_START,
|
||||
ImageView.ScaleType.FIT_CENTER,
|
||||
ImageView.ScaleType.FIT_END -> ContentScale.Fit
|
||||
|
||||
ImageView.ScaleType.CENTER_CROP -> ContentScale.Crop
|
||||
ImageView.ScaleType.CENTER_INSIDE -> ContentScale.Inside
|
||||
else -> ContentScale.None
|
||||
},
|
||||
alignment = when (view.scaleType) {
|
||||
ImageView.ScaleType.FIT_XY,
|
||||
ImageView.ScaleType.CENTER,
|
||||
ImageView.ScaleType.FIT_CENTER,
|
||||
ImageView.ScaleType.CENTER_CROP,
|
||||
ImageView.ScaleType.CENTER_INSIDE -> Alignment.Center
|
||||
|
||||
ImageView.ScaleType.FIT_START -> Alignment.TopStart
|
||||
ImageView.ScaleType.FIT_END -> Alignment.BottomEnd
|
||||
else -> Alignment.Center
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
package de.mm20.launcher2.ui.component.view
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.view.children
|
||||
|
||||
@Composable
|
||||
internal fun ComposeLinearLayout(
|
||||
view: LinearLayout,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
|
||||
if (view.orientation == LinearLayout.VERTICAL) {
|
||||
val horizontalAlignment = when (view.gravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
|
||||
Gravity.START -> Alignment.Start
|
||||
Gravity.CENTER_HORIZONTAL -> Alignment.CenterHorizontally
|
||||
Gravity.END -> Alignment.End
|
||||
else -> Alignment.Start
|
||||
}
|
||||
val verticalArrangement = when (view.gravity and Gravity.VERTICAL_GRAVITY_MASK) {
|
||||
Gravity.TOP -> Arrangement.Top
|
||||
Gravity.CENTER_VERTICAL -> Arrangement.Center
|
||||
Gravity.BOTTOM -> Arrangement.Bottom
|
||||
else -> Arrangement.Top
|
||||
}
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
verticalArrangement = verticalArrangement,
|
||||
) {
|
||||
for (child in view.children) {
|
||||
ComposeAndroidView(
|
||||
child,
|
||||
modifier = Modifier.linearLayoutChild(
|
||||
this@Column,
|
||||
child.layoutParams
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val horizontalArrangement =
|
||||
when (view.gravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
|
||||
Gravity.START -> Arrangement.Start
|
||||
Gravity.CENTER_HORIZONTAL -> Arrangement.Center
|
||||
Gravity.END -> Arrangement.End
|
||||
else -> Arrangement.Start
|
||||
}
|
||||
val verticalAlignment = when (view.gravity and Gravity.VERTICAL_GRAVITY_MASK) {
|
||||
Gravity.TOP -> Alignment.Top
|
||||
Gravity.CENTER_VERTICAL -> Alignment.CenterVertically
|
||||
Gravity.BOTTOM -> Alignment.Bottom
|
||||
else -> Alignment.Top
|
||||
}
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = verticalAlignment,
|
||||
horizontalArrangement = horizontalArrangement,
|
||||
) {
|
||||
for (child in view.children) {
|
||||
ComposeAndroidView(
|
||||
child,
|
||||
modifier = Modifier.linearLayoutChild(
|
||||
this@Row,
|
||||
child.layoutParams,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.linearLayoutChild(scope: RowScope, params: ViewGroup.LayoutParams) = this then
|
||||
Modifier.layoutParams(params) then
|
||||
with(scope) {
|
||||
if (params !is LinearLayout.LayoutParams) return@with Modifier
|
||||
val alignment = when (params.gravity and Gravity.VERTICAL_GRAVITY_MASK) {
|
||||
Gravity.TOP -> Modifier.align(Alignment.Top)
|
||||
Gravity.CENTER_VERTICAL -> Modifier.align(Alignment.CenterVertically)
|
||||
Gravity.BOTTOM -> Modifier.align(Alignment.Bottom)
|
||||
else -> Modifier
|
||||
}
|
||||
|
||||
val weight = if ((params.weight) > 0f) {
|
||||
Modifier.weight(params.weight)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
alignment then weight
|
||||
}
|
||||
|
||||
private fun Modifier.linearLayoutChild(scope: ColumnScope, params: ViewGroup.LayoutParams) =
|
||||
this then
|
||||
Modifier.layoutParams(params) then
|
||||
with(scope) {
|
||||
if (params !is LinearLayout.LayoutParams) return@with Modifier
|
||||
val alignment = when (params.gravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
|
||||
Gravity.START -> Modifier.align(Alignment.Start)
|
||||
Gravity.CENTER_HORIZONTAL -> Modifier.align(Alignment.CenterHorizontally)
|
||||
Gravity.END -> Modifier.align(Alignment.End)
|
||||
else -> Modifier
|
||||
}
|
||||
|
||||
val weight = if ((params.weight) > 0f) {
|
||||
Modifier.weight(params.weight)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
alignment then weight
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package de.mm20.launcher2.ui.component.view
|
||||
|
||||
import android.widget.ListView
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun ComposeListView(
|
||||
view: ListView,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val adapter = view.adapter ?: return
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
) {
|
||||
items(
|
||||
adapter.count,
|
||||
contentType = { adapter.getItemViewType(it) },
|
||||
) { index ->
|
||||
val itemView = adapter.getView(index, null, view)
|
||||
ComposeAndroidView(
|
||||
itemView,
|
||||
modifier = Modifier.layoutParams(itemView.layoutParams)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package de.mm20.launcher2.ui.component.view
|
||||
|
||||
import android.widget.ProgressBar
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@Composable
|
||||
internal fun ComposeProgressBar(
|
||||
view: ProgressBar,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = view.progressTintList?.defaultColor?.let { Color(it) } ?: MaterialTheme.colors.primary,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package de.mm20.launcher2.ui.component.view
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.DeviceFontFamilyName
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.font.SystemFontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
|
||||
@Composable
|
||||
internal fun ComposeTextView(
|
||||
view: TextView,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
Text(
|
||||
text = view.text.toString(),
|
||||
color = Color(view.textColors.defaultColor),
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontSize = with(density) {
|
||||
view.textSize.toSp()
|
||||
},
|
||||
fontWeight = if (isAtLeastApiLevel(28)) {
|
||||
FontWeight(view.typeface.weight)
|
||||
} else if (view.typeface.isBold) {
|
||||
FontWeight.Bold
|
||||
} else {
|
||||
FontWeight.Normal
|
||||
},
|
||||
fontStyle = if (view.typeface.isItalic) {
|
||||
FontStyle.Italic
|
||||
} else {
|
||||
FontStyle.Normal
|
||||
},
|
||||
),
|
||||
modifier = modifier,
|
||||
textAlign = when (view.textAlignment) {
|
||||
TextView.TEXT_ALIGNMENT_CENTER -> TextAlign.Center
|
||||
TextView.TEXT_ALIGNMENT_TEXT_START -> TextAlign.Start
|
||||
TextView.TEXT_ALIGNMENT_TEXT_END -> TextAlign.End
|
||||
else -> TextAlign.Start
|
||||
}
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user