Add experimental app widget compose reimplementation

This commit is contained in:
MM20 2024-06-24 23:18:21 +02:00
parent 2e4100f676
commit 15cb4b4f29
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 512 additions and 0 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
},
)
}

View File

@ -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
}

View File

@ -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)
)
}
}
}

View File

@ -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,
)
}

View File

@ -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
}
)
}