Skip to content

Commit

Permalink
Add common ProfileImage composable (#585)
Browse files Browse the repository at this point in the history
* Add Coil Compose + Zoomable dependencies
* Add content description string
  • Loading branch information
EdricChan03 committed Dec 19, 2024
1 parent 3aa212d commit 148ffbb
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 0 deletions.
6 changes: 6 additions & 0 deletions core/auth/ui/common/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Account info strings -->
<eat-comment />

<!-- Content description for the user's profile picture. -->
<string name="account_profile_content_desc">Profile photo for %1$s</string>

<!-- Auth required dialog strings -->
<eat-comment />

Expand Down
4 changes: 4 additions & 0 deletions core/auth/ui/compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ android {
dependencies {
api(projects.core.auth.model)
api(projects.core.auth.ui.common)
implementation(projects.exts.coil)
implementation(projects.ui.theming.compose)

// Firebase dependencies
Expand All @@ -36,6 +37,9 @@ dependencies {

debugImplementation(libs.bundles.androidx.compose.tooling)

api(libs.coil.compose)
implementation(libs.coil.zoomable)

testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.espresso.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.edricchan.studybuddy.core.auth.ui

import android.content.Context
import android.net.Uri
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AccountCircle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.net.toUri
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.edricchan.studybuddy.core.auth.common.R
import com.edricchan.studybuddy.core.auth.ui.painter.tintedPainter
import com.edricchan.studybuddy.exts.coil.imageRequest
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage

/**
* Displays a user's profile picture given the [photoUrl].
* The image is [cropped][Modifier.clip] with the specified [shape].
* @param modifier Modifier to be passed to [AsyncImage].
* @param shape The [Shape] to [crop][Modifier.clip] the contents to.
* @param displayName The user's display name.
* @param photoUrl URL to the profile picture as a [String].
* @param context Context to be used to create an [ImageRequest].
* @param isZoomable Whether the image should be [zoomable][ZoomableAsyncImage].
* @param imageRequestInit Additional configuration to be passed to the [ImageRequest.Builder].
*/
@Composable
fun ProfileImage(
modifier: Modifier = Modifier,
shape: Shape = CircleShape,
displayName: String,
photoUrl: String?,
context: Context = LocalContext.current,
isZoomable: Boolean = false,
imageRequestInit: ImageRequest.Builder.() -> Unit = {}
) = ProfileImage(
modifier = modifier,
shape = shape,
displayName = displayName,
photoUri = photoUrl?.toUri(),
context = context,
isZoomable = isZoomable,
imageRequestInit = imageRequestInit
)

/**
* Displays a user's profile picture given the [photoUri].
* The image is [cropped][Modifier.clip] with the specified [shape].
* @param modifier Modifier to be passed to [AsyncImage].
* @param shape The [Shape] to [crop][Modifier.clip] the contents to.
* @param displayName The user's display name.
* @param photoUri URL to the profile picture as a [Uri].
* @param context Context to be used to create an [ImageRequest].
* @param isZoomable Whether the image should be [zoomable][ZoomableAsyncImage].
* @param imageRequestInit Additional configuration to be passed to the [ImageRequest.Builder].
*/
@Composable
fun ProfileImage(
modifier: Modifier = Modifier,
shape: Shape = CircleShape,
displayName: String,
photoUri: Uri?,
context: Context = LocalContext.current,
isZoomable: Boolean = false,
imageRequestInit: ImageRequest.Builder.() -> Unit = {}
) {
val model = context.imageRequest {
data(photoUri)
crossfade(true)
imageRequestInit()
}
if (isZoomable) {
ZoomableAsyncImage(
model = model,
contentDescription = stringResource(R.string.account_profile_content_desc, displayName),
contentScale = ContentScale.Crop,
modifier = modifier
)
} else {
AsyncImage(
modifier = modifier.clip(shape),
model = model,
placeholder = tintedPainter(rememberVectorPainter(Icons.Outlined.AccountCircle)),
contentDescription = stringResource(R.string.account_profile_content_desc, displayName),
contentScale = ContentScale.Crop,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// From https://gist.github.com/colinrtwhite/c2966e0b8584b4cdf0a5b05786b20ae1
package com.edricchan.studybuddy.core.auth.ui.painter

import androidx.compose.material3.LocalContentColor
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter

/**
* Create and return a new [Painter] that wraps [painter] with its [alpha], [colorFilter], or [onDraw] overwritten.
*/
internal fun forwardingPainter(
painter: Painter,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
onDraw: DrawScope.(ForwardingDrawInfo) -> Unit = DefaultOnDraw,
): Painter = ForwardingPainter(painter, alpha, colorFilter, onDraw)

/**
* Variant of [forwardingPainter] which tints the [painter] with the given [tint] colour.
* @see forwardingPainter
*/
@Composable
internal fun tintedPainter(
painter: Painter,
alpha: Float = DefaultAlpha,
tint: Color = LocalContentColor.current
): Painter = forwardingPainter(painter, alpha, colorFilter = ColorFilter.tint(tint))

internal data class ForwardingDrawInfo(
val painter: Painter,
val alpha: Float,
val colorFilter: ColorFilter?,
)

private class ForwardingPainter(
private val painter: Painter,
private var alpha: Float,
private var colorFilter: ColorFilter?,
private val onDraw: DrawScope.(ForwardingDrawInfo) -> Unit,
) : Painter() {

private var info = newInfo()

override val intrinsicSize get() = painter.intrinsicSize

override fun applyAlpha(alpha: Float): Boolean {
if (alpha != DefaultAlpha) {
this.alpha = alpha
this.info = newInfo()
}
return true
}

override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
if (colorFilter != null) {
this.colorFilter = colorFilter
this.info = newInfo()
}
return true
}

override fun DrawScope.onDraw() = onDraw(info)

private fun newInfo() = ForwardingDrawInfo(painter, alpha, colorFilter)
}

private val DefaultOnDraw: DrawScope.(ForwardingDrawInfo) -> Unit = { info ->
with(info.painter) {
draw(size, info.alpha, info.colorFilter)
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ markwon-ext-tasklist = { module = "io.noties.markwon:ext-tasklist", version.ref

# Coil
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
coil-zoomable = "me.saket.telephoto:zoomable-image-coil:0.14.0"

# Ktor
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
Expand Down

0 comments on commit 148ffbb

Please sign in to comment.