diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/AndroidView.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/AndroidView.kt new file mode 100644 index 00000000..5ac30fc8 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/AndroidView.kt @@ -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(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 +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/FrameLayout.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/FrameLayout.kt new file mode 100644 index 00000000..4772eba5 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/FrameLayout.kt @@ -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) + } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ImageView.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ImageView.kt new file mode 100644 index 00000000..41f60465 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ImageView.kt @@ -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 + }, + ) +} diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/LinearLayout.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/LinearLayout.kt new file mode 100644 index 00000000..f0100771 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/LinearLayout.kt @@ -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 + } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ListView.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ListView.kt new file mode 100644 index 00000000..656f84da --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ListView.kt @@ -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) + ) + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ProgressBar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ProgressBar.kt new file mode 100644 index 00000000..e5c7efd9 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/ProgressBar.kt @@ -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, + ) +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/TextView.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/TextView.kt new file mode 100644 index 00000000..d5780627 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/view/TextView.kt @@ -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 + } + ) +} \ No newline at end of file