-
Notifications
You must be signed in to change notification settings - Fork 3
Image support #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Image support #78
Changes from all commits
dfccf76
ac550c1
d2dce0c
5b4aed0
fd5ca80
6b3af85
d2d306a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package com.github.codandotv.craftd.compose.ui.image | ||
|
|
||
| import androidx.compose.foundation.clickable | ||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.foundation.layout.aspectRatio | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.layout.ContentScale | ||
| import coil3.compose.AsyncImage | ||
| import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties | ||
| import com.github.codandotv.craftd.compose.extensions.toArrangementCompose | ||
|
|
||
| /** | ||
| * CraftDImage composable for rendering images using Coil 3. | ||
| * | ||
| * Supports both local and network images via Coil's AsyncImage. | ||
| */ | ||
| @Composable | ||
| fun CraftDImage( | ||
| imageProperties: ImageProperties, | ||
| modifier: Modifier = Modifier, | ||
| clickable: (() -> Unit)? = null, | ||
| ) { | ||
| val modifierCustom = clickable?.let { Modifier.clickable { clickable.invoke() } } ?: modifier | ||
|
|
||
| Row( | ||
| horizontalArrangement = imageProperties.align.toArrangementCompose(), | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) { | ||
| AsyncImage( | ||
| model = imageProperties.url, | ||
| contentDescription = imageProperties.contentDescription, | ||
| modifier = | ||
| modifierCustom | ||
| .then( | ||
| if (imageProperties.fillMaxSize == true) { | ||
| Modifier.fillMaxSize() | ||
| } else { | ||
| Modifier | ||
| } | ||
| ) | ||
| .then( | ||
| imageProperties.aspectRatio?.let { ratio -> | ||
| Modifier.aspectRatio(ratio) | ||
| } | ||
| ?: Modifier | ||
| ), | ||
| contentScale = imageProperties.contentScale?.toContentScale() ?: ContentScale.Fit | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private fun String.toContentScale(): ContentScale = | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Niceeee!! 👏🏼
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #suggestion |
||
| when (this.lowercase()) { | ||
| "crop" -> ContentScale.Crop | ||
| "fit" -> ContentScale.Fit | ||
| "fillbounds" -> ContentScale.FillBounds | ||
| "fillwidth" -> ContentScale.FillWidth | ||
| "fillheight" -> ContentScale.FillHeight | ||
| "inside" -> ContentScale.Inside | ||
| "none" -> ContentScale.None | ||
| else -> ContentScale.Fit | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.github.codandotv.craftd.compose.ui.image | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import com.github.codandotv.craftd.androidcore.data.convertToElement | ||
| import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties | ||
| import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties | ||
| import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey | ||
| import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener | ||
| import com.github.codandotv.craftd.compose.builder.CraftDBuilder | ||
|
|
||
| class CraftDImageBuilder(override val key: String = CraftDComponentKey.IMAGE_COMPONENT.key) : | ||
| CraftDBuilder { | ||
| @Composable | ||
| override fun craft(model: SimpleProperties, listener: CraftDViewListener) { | ||
| val imageProperties = model.value.convertToElement<ImageProperties>() | ||
| imageProperties?.let { | ||
| CraftDImage(it) { imageProperties.actionProperties?.let { listener.invoke(it) } } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| package com.github.codandotv.craftd.compose.ui.image | ||
|
|
||
| import androidx.compose.foundation.clickable | ||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.foundation.layout.aspectRatio | ||
| import androidx.compose.foundation.layout.fillMaxSize | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.layout.ContentScale | ||
| import coil3.compose.AsyncImage | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #discussion Hey folks! I’ve been thinking about something: imagine someone adds Craftd to a project that’s already using a very recent version of Coil—or even another image-loading library. Wouldn’t it make sense for us to introduce an abstraction layer so developers can plug in whichever image loader they prefer? This way, Craftd wouldn’t need updates every time a new Coil 3 release comes out, and teams would have more flexibility overall. What do you think? Does this direction make sense to you? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kind of agree, but actually, just Coil is 100% compatible with Multiplatform
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the problem that this lib has support to android 100% too. I agree with moro, i was thinking I was thinking, I've had to do similar things using those server-driven UIs I made. If you create a constructor in the Builder with a function that propagates to the component, like: class CraftDImageBuilder(override val key: String = CraftDComponentKey.IMAGE_COMPONENT.key, loadImage : (String) -> Loader( algo que seja comum entre as libs teria que ver cada para pensar o objetvo certo aqui)) :
CraftDBuilder {
@Composable
override fun craft(model: SimpleProperties, listener: CraftDViewListener) {
val imageProperties = model.value.convertToElement<ImageProperties>()
imageProperties?.let {
CraftDImage(it, loadImage) { imageProperties.actionProperties?.let { listener.invoke(it) } }
}
}
}that accepts any kind of "result" from a coil or Picasso or something like that that the person implements, then when registering the components in the CraftedManager, the person could pass an implementation block using and in the component there would be something inside the ImageView block, a loadImage.invoke(url) in Imageview and when you register the component you can do this val craftdBuilderManager = remember {
CraftDBuilderManager().add(
MySampleButtonComposeBuilder(),
CraftdIMageBuilder(){ url ->
//call your frameworker and return
}
)
}
LaunchedEffect(Unit) {
vm.loadProperties()
}
CraftDynamic(
properties = properties,
craftDBuilderManager = craftdBuilderManager
) {
println(
">>>> category ${it.analytics?.category} -" + " action ${it.analytics?.action} -" + " label ${it.analytics?.label} -" + " deeplink ${it.deeplink}"
)
}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. inside from craftdManager will be override de current CraftdImageBuilder default to new instancia fun add(vararg arrayCraftDBuilder: CraftDBuilder) : CraftDBuilderManager{
arrayCraftDBuilder.forEach {
mapBuilder[it.key] = it
}
return this
}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. another thing This PR you miss to register in CraftdManager your builder CraftDBuilderManager @Stable
@Immutable
class CraftDBuilderManager {
private val mapBuilder = hashMapOf(
CraftDComponentKey.BUTTON_COMPONENT.key to CraftDButtonBuilder(),
CraftDComponentKey.TEXT_VIEW_COMPONENT.key to CraftDTextBuilder(),
CraftDComponentKey.CHECK_BOX_COMPONENT.key to CraftDCheckBoxBuilder(),
CraftDComponentKey.IMAGE_COMPONENT.key to CraftDImageBuilder(),
)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking... what I showed you is a good approach that works for various situations, like loading an image into an ImageView. This will be quite common when thinking about components, such as having more complex components using component composition, and you can also have an ImageView. In this case, I think it's better to create an additional parameter in the CraftManager constructor, and then implement it within the builder (still keeping the builder as I mentioned). like: Stable
@Immutable
class CraftDBuilderManager (private val imageLoader : (String) -> Loader = {}){
private val mapBuilder = hashMapOf(
CraftDComponentKey.BUTTON_COMPONENT.key to CraftDButtonBuilder(),
CraftDComponentKey.TEXT_VIEW_COMPONENT.key to CraftDTextBuilder(),
CraftDComponentKey.CHECK_BOX_COMPONENT.key to CraftDCheckBoxBuilder(),
CraftDComponentKey.IMAGE_COMPONENT.key to CraftDImageBuilder(imageLoader),
) |
||
| import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties | ||
| import com.github.codandotv.craftd.compose.extensions.toArrangementCompose | ||
|
|
||
| /** | ||
| * CraftDImage composable for rendering images using Coil 3. | ||
| * | ||
| * Supports both local and network images via Coil's AsyncImage. | ||
| */ | ||
| @Composable | ||
| fun CraftDImage( | ||
| imageProperties: ImageProperties, | ||
| modifier: Modifier = Modifier, | ||
| clickable: (() -> Unit)? = null, | ||
| ) { | ||
| val modifierCustom = clickable?.let { Modifier.clickable { clickable.invoke() } } ?: modifier | ||
|
|
||
| Row( | ||
| horizontalArrangement = imageProperties.align.toArrangementCompose(), | ||
| modifier = Modifier.fillMaxWidth() | ||
| ) { | ||
| AsyncImage( | ||
| model = imageProperties.url, | ||
| contentDescription = imageProperties.contentDescription, | ||
| modifier = | ||
| modifierCustom | ||
| .then( | ||
| if (imageProperties.fillMaxSize == true) { | ||
| Modifier.fillMaxSize() | ||
| } else { | ||
| Modifier | ||
| } | ||
| ) | ||
| .then( | ||
| imageProperties.aspectRatio?.let { ratio -> | ||
| Modifier.aspectRatio(ratio) | ||
| } | ||
| ?: Modifier | ||
| ), | ||
| contentScale = imageProperties.contentScale?.toContentScale() ?: ContentScale.Fit | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| private fun String.toContentScale(): ContentScale = | ||
| when (this.lowercase()) { | ||
| "crop" -> ContentScale.Crop | ||
| "fit" -> ContentScale.Fit | ||
| "fillbounds" -> ContentScale.FillBounds | ||
| "fillwidth" -> ContentScale.FillWidth | ||
| "fillheight" -> ContentScale.FillHeight | ||
| "inside" -> ContentScale.Inside | ||
| "none" -> ContentScale.None | ||
| else -> ContentScale.Fit | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.github.codandotv.craftd.compose.ui.image | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import com.github.codandotv.craftd.androidcore.data.convertToElement | ||
| import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties | ||
| import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties | ||
| import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey | ||
| import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener | ||
| import com.github.codandotv.craftd.compose.builder.CraftDBuilder | ||
|
|
||
| class CraftDImageBuilder(override val key: String = CraftDComponentKey.IMAGE_COMPONENT.key) : | ||
| CraftDBuilder { | ||
| @Composable | ||
| override fun craft(model: SimpleProperties, listener: CraftDViewListener) { | ||
| val imageProperties = model.value.convertToElement<ImageProperties>() | ||
| imageProperties?.let { | ||
| CraftDImage(it) { imageProperties.actionProperties?.let { listener.invoke(it) } } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package com.github.codandotv.craftd.androidcore.data.model.image | ||
|
|
||
| import androidx.compose.runtime.Immutable | ||
| import androidx.compose.runtime.Stable | ||
| import com.github.codandotv.craftd.androidcore.data.model.action.ActionProperties | ||
| import com.github.codandotv.craftd.androidcore.domain.CraftDAlign | ||
| import kotlinx.serialization.SerialName | ||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| @Immutable | ||
| @Stable | ||
| data class ImageProperties( | ||
| @SerialName("url") val url: String? = null, | ||
| @SerialName("contentDescription") val contentDescription: String? = null, | ||
| @SerialName("align") val align: CraftDAlign? = null, | ||
| @SerialName("fillMaxSize") val fillMaxSize: Boolean? = false, | ||
| @SerialName("aspectRatio") val aspectRatio: Float? = null, | ||
| @SerialName("contentScale") val contentScale: String? = null, | ||
| @SerialName("actionProperties") var actionProperties: ActionProperties? = null, | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| package com.github.codandotv.craftd.androidcore.presentation | ||
|
|
||
|
|
||
| enum class CraftDComponentKey(val key: String) { | ||
| TEXT_VIEW_COMPONENT("${CRAFT_D}TextView"), | ||
| BUTTON_COMPONENT("${CRAFT_D}Button"), | ||
| CHECK_BOX_COMPONENT("${CRAFT_D}CheckBox"), | ||
| IMAGE_COMPONENT("${CRAFT_D}Image"), | ||
| } | ||
|
|
||
| internal const val CRAFT_D = "CraftD" | ||
| internal const val CRAFT_D = "CraftD" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package com.github.codandotv.craftd.xml.ui.image | ||
|
|
||
| import android.content.Context | ||
| import android.util.AttributeSet | ||
| import android.view.LayoutInflater | ||
| import android.view.ViewGroup | ||
| import android.widget.ImageView | ||
| import android.widget.RelativeLayout | ||
| import coil3.load | ||
| import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties | ||
| import com.github.codandotv.craftd.androidcore.domain.CraftDAlign | ||
| import com.github.codandotv.craftd.xml.databinding.ImageBinding | ||
|
|
||
| /** | ||
| * CraftDImageComponent for XML-based image rendering using Coil 3. | ||
| * | ||
| * Supports both local and network images via Coil's load extension. | ||
| */ | ||
| class CraftDImageComponent | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is nice, we have it in xml too 🎉 |
||
| @JvmOverloads | ||
| constructor( | ||
| context: Context, | ||
| attrs: AttributeSet? = null, | ||
| defStyleAttr: Int = 0, | ||
| ) : RelativeLayout(context, attrs, defStyleAttr) { | ||
| private var binding: ImageBinding | ||
|
|
||
| init { | ||
| binding = ImageBinding.inflate(LayoutInflater.from(context), this) | ||
| } | ||
|
|
||
| fun setProperties(imageProperties: ImageProperties) { | ||
| // Load image using Coil | ||
| imageProperties.url?.let { url -> binding.imageView.load(url) } | ||
|
|
||
| imageProperties.contentDescription?.let { description -> | ||
| binding.imageView.contentDescription = description | ||
| } | ||
|
|
||
| setupFillMaxSize(imageProperties) | ||
|
|
||
| imageProperties.aspectRatio?.let { ratio -> | ||
| binding.imageView.adjustViewBounds = true | ||
| // AspectRatio can be handled via custom logic or ConstraintLayout | ||
| } | ||
|
|
||
| imageProperties.contentScale?.let { scale -> | ||
| binding.imageView.scaleType = scale.toScaleType() | ||
| } | ||
|
|
||
| imageProperties.align?.let { | ||
| binding.imageView.layoutParams = | ||
| LayoutParams( | ||
| ViewGroup.LayoutParams.WRAP_CONTENT, | ||
| ViewGroup.LayoutParams.WRAP_CONTENT | ||
| ) | ||
| .apply { addRule(it.toRelativeLayoutParams()) } | ||
| } | ||
| } | ||
|
|
||
| private fun setupFillMaxSize(imageProperties: ImageProperties) { | ||
| binding.imageView.layoutParams = | ||
| imageProperties.fillMaxSize?.let { isFillMaxSize -> | ||
| LayoutParams( | ||
| if (isFillMaxSize) { | ||
| ViewGroup.LayoutParams.MATCH_PARENT | ||
| } else { | ||
| ViewGroup.LayoutParams.WRAP_CONTENT | ||
| }, | ||
| ViewGroup.LayoutParams.MATCH_PARENT | ||
| ) | ||
| } | ||
| ?: LayoutParams( | ||
| ViewGroup.LayoutParams.WRAP_CONTENT, | ||
| ViewGroup.LayoutParams.WRAP_CONTENT | ||
| ) | ||
| } | ||
|
|
||
| private fun CraftDAlign?.toRelativeLayoutParams(): Int = | ||
| when (this) { | ||
| CraftDAlign.CENTER -> CENTER_IN_PARENT | ||
| CraftDAlign.RIGHT -> ALIGN_PARENT_END | ||
| else -> ALIGN_PARENT_START | ||
| } | ||
|
|
||
| private fun String.toScaleType(): ImageView.ScaleType = | ||
| when (this.lowercase()) { | ||
| "crop" -> ImageView.ScaleType.CENTER_CROP | ||
| "fit" -> ImageView.ScaleType.FIT_CENTER | ||
| "fillbounds" -> ImageView.ScaleType.FIT_XY | ||
| "fillwidth" -> ImageView.ScaleType.FIT_START | ||
| "fillheight" -> ImageView.ScaleType.FIT_END | ||
| "inside" -> ImageView.ScaleType.CENTER_INSIDE | ||
| "none" -> ImageView.ScaleType.CENTER | ||
| else -> ImageView.ScaleType.FIT_CENTER | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package com.github.codandotv.craftd.xml.ui.image | ||
|
|
||
| import android.view.ViewGroup | ||
| import androidx.recyclerview.widget.RecyclerView | ||
| import com.github.codandotv.craftd.androidcore.data.convertToElement | ||
| import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties | ||
| import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties | ||
| import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey | ||
| import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener | ||
| import com.github.codandotv.craftd.xml.ui.CraftDViewRenderer | ||
|
|
||
| class ImageComponentRender(override var onClickListener: CraftDViewListener?) : | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you need to register here too object CraftDBuilderManager {
fun getBuilderRenders(
simpleProperties: List<SimpleProperties>,
customDynamicBuilderList: List<CraftDViewRenderer<*>> = emptyList(),
onAction: CraftDViewListener
): List<CraftDViewRenderer<*>> {
val allViewRenders = (customDynamicBuilderList + listOf(
CraftDTextViewComponentRender(onAction),
ButtonComponentRender(onAction),
ImageComponentRender(onAction)
))
|
||
| CraftDViewRenderer<ImageComponentRender.ImageHolder>( | ||
| CraftDComponentKey.IMAGE_COMPONENT.key, | ||
| CraftDComponentKey.IMAGE_COMPONENT.ordinal | ||
| ) { | ||
|
|
||
| inner class ImageHolder(val image: CraftDImageComponent) : RecyclerView.ViewHolder(image) | ||
|
|
||
| override fun bindView(model: SimpleProperties, holder: ImageHolder, position: Int) { | ||
| val imageProperties = model.value.convertToElement<ImageProperties>() | ||
|
|
||
| imageProperties?.let { holder.image.setProperties(it) } | ||
| imageProperties?.actionProperties?.let { actionProperties -> | ||
| holder.image.setOnClickListener { onClickListener?.invoke(actionProperties) } | ||
| } | ||
| } | ||
|
|
||
| override fun createViewHolder(parent: ViewGroup): ImageHolder { | ||
| return ImageHolder(CraftDImageComponent(parent.context)) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
|
|
||
| <merge xmlns:android="http://schemas.android.com/apk/res/android" | ||
| xmlns:tools="http://schemas.android.com/tools" | ||
| android:layout_width="match_parent" | ||
| android:layout_height="wrap_content" | ||
| android:focusable="true" | ||
| tools:parentTag="android.widget.RelativeLayout"> | ||
|
|
||
| <ImageView | ||
| android:id="@+id/imageView" | ||
| android:layout_width="wrap_content" | ||
| android:layout_height="wrap_content" | ||
| android:contentDescription="@null" | ||
| tools:src="@tools:sample/backgrounds/scenic" /> | ||
| </merge> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking if we need this component here, considering a new one was created in our commonMain source set. Any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending on the library that you will use, you can have different features os specs per platform or custom extensions.