Skip to content

Commit

Permalink
Migrate curated photos to Compose
Browse files Browse the repository at this point in the history
  • Loading branch information
SIKV committed Jun 2, 2024
1 parent 0b51f6b commit 197e16d
Show file tree
Hide file tree
Showing 37 changed files with 901 additions and 255 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import com.skydoves.landscapist.animation.circular.CircularRevealPlugin
import com.skydoves.landscapist.components.rememberImageComponent
import com.skydoves.landscapist.glide.GlideImage

// TODO: Move to compose-ui module.

@Composable
fun TransparentTopAppBar(
onBackPressed: () -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent

fun Context.showSoftInput(view: View): Boolean {
Expand Down Expand Up @@ -43,6 +45,17 @@ fun Context.openAppSettings() {
startActivity(intent)
}

fun Context.findActivity(): AppCompatActivity {
var context = this
while (context is ContextWrapper) {
if (context is AppCompatActivity) {
return context
}
context = context.baseContext
}
throw IllegalStateException("AppCompatActivity not found")
}

fun Context.copyText(label: String, text: String) {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText(label, text)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.github.sikv.photos.common

import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import java.util.UUID

class ActivityPermissionManager(
private val activity: AppCompatActivity
) : DefaultLifecycleObserver {

private val key = UUID.randomUUID().toString()
private lateinit var requestPermission: ActivityResultLauncher<String>

private var onPermissionRequestResult: ((Boolean) -> Unit)? = null

init {
activity.lifecycle.addObserver(this)
}

override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)

requestPermission = activity.activityResultRegistry
.register(key, ActivityResultContracts.RequestPermission()) { granted ->
onPermissionRequestResult?.invoke(granted)
}
}

fun requestPermission(permission: String, onPermissionRequestResult: (Boolean) -> Unit) {
this.onPermissionRequestResult = onPermissionRequestResult
requestPermission.launch(permission)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import java.util.*

@Deprecated("Use ActivityPermissionManager")
class PermissionManager(
private val fragment: Fragment
) : DefaultLifecycleObserver {
Expand Down
1 change: 1 addition & 0 deletions compose-ui/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
24 changes: 24 additions & 0 deletions compose-ui/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}

android {
namespace = "com.github.sikv.photos.compose.ui"

buildFeatures {
compose = true
viewBinding = true
}

composeOptions {
kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
}
}

dependencies {
implementation(project(":domain"))
implementation(project(":common-ui"))

implementation(libs.androidx.compose.material3)
}
2 changes: 2 additions & 0 deletions compose-ui/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.github.sikv.photos.compose.ui

import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import com.github.sikv.photos.common.ui.ActionIconButton

private const val favoriteAnimationDuration = 100
private const val unFavoriteAnimationDuration = 400

@Composable
fun FavoriteButton(
isFavorite: Boolean,
onToggleFavorite: () -> Unit
) {
val scale = remember { Animatable(1.0f) }
val offsetX = remember { Animatable(0f) }

LaunchedEffect(isFavorite) {
if (isFavorite) {
scale.animateTo(
targetValue = 1.3f,
animationSpec = tween(favoriteAnimationDuration, easing = LinearEasing)
)
scale.animateTo(
targetValue = 1f,
animationSpec = tween(favoriteAnimationDuration, easing = LinearEasing)
)
} else {
// Source: https://ophilia.in/creating-a-wiggle-animation-in-jetpack-compose
offsetX.animateTo(
targetValue = 0f,
animationSpec = keyframes {
for (i in 1..8) {
val x = when (i % 3) {
0 -> 2f
1 -> -2f
else -> 0f
}
x at unFavoriteAnimationDuration / 10 * i with LinearEasing
}
}
)
}
}

val icon =
if (isFavorite) R.drawable.ic_favorite_red_24dp
else R.drawable.ic_favorite_border_white_24dp

val tintColor =
if (isFavorite) colorResource(id = R.color.colorRed)
else LocalContentColor.current

val tint: Color by animateColorAsState(
targetValue = tintColor,
animationSpec = tween(favoriteAnimationDuration),
label = "Favorite button color animation",
)

ActionIconButton(
icon = icon,
contentDescription = R.string.toggle_favorite,
iconTint = tint,
onClick = onToggleFavorite,
modifier = Modifier
.scale(scale.value)
.offset(x = offsetX.value.dp, y = 0.dp)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.github.sikv.photos.compose.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.sikv.photos.common.ui.ActionIconButton
import com.github.sikv.photos.common.ui.NetworkImage
import com.github.sikv.photos.common.ui.PlaceholderImage
import com.github.sikv.photos.common.ui.getAttributionPlaceholderBackgroundColor
import com.github.sikv.photos.common.ui.getAttributionPlaceholderTextColor
import com.github.sikv.photos.domain.Photo

@Composable
fun PhotoItem(
photo: Photo,
isFavorite: Boolean,
onClick: () -> Unit,
onAttributionClick: () -> Unit,
onMoreClick: () -> Unit,
onToggleFavorite: () -> Unit,
onShareClick: () -> Unit,
onDownloadClick: () -> Unit
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(Spacing.One))
Attribution(
photo = photo,
onAttributionClick = onAttributionClick
)
IconButton(
modifier = Modifier
.background(Color.Transparent, shape = CircleShape),
onClick = onMoreClick
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(id = R.string.more)
)
}
}

Spacer(modifier = Modifier.height(Spacing.One))

NetworkImage(
imageUrl = photo.getPhotoPreviewUrl(),
loading = {
Box(
modifier = Modifier
.aspectRatio(1f)
.background(colorResource(id = R.color.colorPlaceholder))
)
},
modifier = Modifier
.aspectRatio(1f)
.clickable(onClick = onClick)
)
Row {
FavoriteButton(
isFavorite = isFavorite,
onToggleFavorite = onToggleFavorite
)
IconButton(
modifier = Modifier
.background(Color.Transparent, shape = CircleShape),
onClick = onShareClick
) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = stringResource(id = R.string.share)
)
}
Spacer(modifier = Modifier.weight(1f))
ActionIconButton(
icon = R.drawable.ic_file_download_24dp,
contentDescription = R.string.download,
onClick = onDownloadClick
)
}

Spacer(modifier = Modifier.height(Spacing.Two))
}
}

@Composable
private fun RowScope.Attribution(
photo: Photo,
onAttributionClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(onClick = onAttributionClick)
.padding(end = Spacing.One)
.weight(1f)
) {
val photographerImageUrl = photo.getPhotoPhotographerImageUrl()

val modifier = Modifier
.size(36.dp)
.clip(CircleShape)

if (photographerImageUrl != null) {
NetworkImage(
imageUrl = photographerImageUrl,
modifier = modifier
)
} else {
PlaceholderImage(
text = photo.getPhotoPhotographerName().first().uppercaseChar().toString(),
textColor = getAttributionPlaceholderTextColor(LocalContext.current),
backgroundColor = getAttributionPlaceholderBackgroundColor(LocalContext.current),
modifier = modifier
)
}
Spacer(modifier = Modifier.width(Spacing.One))
Column {
Text(photo.getPhotoPhotographerName(),
style = MaterialTheme.typography.titleSmall
)
Text(photo.getPhotoSource().title,
style = MaterialTheme.typography.bodySmall
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.github.sikv.photos.compose.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import com.github.sikv.photos.common.ui.NetworkImage
import com.github.sikv.photos.domain.Photo

@Composable
fun PhotoItemCompact(
photo: Photo,
onClick: () -> Unit
) {
NetworkImage(
imageUrl = photo.getPhotoPreviewUrl(),
loading = {
Box(
modifier = Modifier
.aspectRatio(1f)
.background(colorResource(id = R.color.colorPlaceholder))
)
},
modifier = Modifier
.aspectRatio(1f)
.clickable(onClick = onClick)
)
}
Loading

0 comments on commit 197e16d

Please sign in to comment.