[SES-337] Add rounded corners to thumbnail in QuoteView (#1285)

* Add rounded corners to thumbnail in QuoteView

* Simplify ThumbnailView

* Cleanup ThumbnailView

* Removed include custom attributes

The custom attributes are not passed to the view.
I added the radius programatically instead.

* Clipping whole thumbnail view instead of just the image requests

---------

Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com>
Co-authored-by: ThomasSession <thomas.r@getsession.org>
This commit is contained in:
Andrew 2024-07-01 09:11:46 +09:30 committed by GitHub
parent 5cd2cf5cb5
commit 1d80bb0ba9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 127 additions and 129 deletions

View File

@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment()); Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
if (slide != null) { if (slide != null) {
thumbnailView.setImageResource(glideRequests, slide, false, null); thumbnailView.setImageResource(glideRequests, slide, false);
} }
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord)); thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));

View File

@ -104,7 +104,7 @@ class AlbumThumbnailView : RelativeLayout {
// iterate binding // iterate binding
slides.take(MAX_ALBUM_DISPLAY_SIZE).forEachIndexed { position, slide -> slides.take(MAX_ALBUM_DISPLAY_SIZE).forEachIndexed { position, slide ->
val thumbnailView = getThumbnailView(position) val thumbnailView = getThumbnailView(position)
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message) thumbnailView.setImageResource(glideRequests, slide, isPreview = false)
} }
} }

View File

@ -31,10 +31,10 @@ class LinkPreviewDraftView : LinearLayout {
// Hide the loader and show the content view // Hide the loader and show the content view
binding.linkPreviewDraftContainer.isVisible = true binding.linkPreviewDraftContainer.isVisible = true
binding.linkPreviewDraftLoader.isVisible = false binding.linkPreviewDraftLoader.isVisible = false
binding.thumbnailImageView.root.radius = toPx(4, resources) binding.thumbnailImageView.root.setRoundedCorners(toPx(4, resources))
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, null) binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false)
} }
binding.linkPreviewDraftTitleTextView.text = linkPreview.title binding.linkPreviewDraftTitleTextView.text = linkPreview.title
} }

View File

@ -41,7 +41,7 @@ class LinkPreviewView : LinearLayout {
// Thumbnail // Thumbnail
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false)
binding.thumbnailImageView.root.loadIndicator.isVisible = false binding.thumbnailImageView.root.loadIndicator.isVisible = false
} }
// Title // Title

View File

@ -108,8 +108,9 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
attachments.thumbnailSlide != null -> { attachments.thumbnailSlide != null -> {
val slide = attachments.thumbnailSlide!! val slide = attachments.thumbnailSlide!!
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.quoteViewAttachmentThumbnailImageView.root.radius = toPx(4, resources) binding.quoteViewAttachmentThumbnailImageView
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false, null) .root.setRoundedCorners(toPx(4, resources))
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false)
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
} }

View File

@ -2,10 +2,13 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Outline
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewOutlineProvider
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -21,18 +24,17 @@ import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.SettableFuture import org.session.libsignal.utilities.SettableFuture
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GlideRequest import org.thoughtcrime.securesms.mms.GlideRequest
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
import kotlin.Boolean
import kotlin.Int
import kotlin.getValue
import kotlin.lazy
import kotlin.let
open class ThumbnailView: FrameLayout { open class ThumbnailView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
companion object { companion object {
private const val WIDTH = 0 private const val WIDTH = 0
private const val HEIGHT = 1 private const val HEIGHT = 1
@ -41,29 +43,28 @@ open class ThumbnailView: FrameLayout {
private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) } private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) }
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize(null) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator } val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
private val dimensDelegate = ThumbnailDimensDelegate() private val dimensDelegate = ThumbnailDimensDelegate()
private var slide: Slide? = null private var slide: Slide? = null
var radius: Int = 0
private fun initialize(attrs: AttributeSet?) { init {
if (attrs != null) { attrs?.let { context.theme.obtainStyledAttributes(it, R.styleable.ThumbnailView, 0, 0) }
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0) ?.apply {
dimensDelegate.setBounds(
getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0),
getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0),
getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0),
getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0)
)
dimensDelegate.setBounds(typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0), setRoundedCorners(
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0), getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0)
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0), )
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0))
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0) recycle()
typedArray.recycle()
} }
} }
@ -84,114 +85,118 @@ open class ThumbnailView: FrameLayout {
private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0) private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0)
private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0) private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0)
// endregion // endregion
// region Interaction // region Interaction
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord?): ListenableFuture<Boolean> { fun setRoundedCorners(radius: Int){
return setImageResource(glide, slide, isPreview, 0, 0, mms) // create an outline provider and clip the whole view to that shape
// that way we can round the image and the background ( and any other artifacts that the view may contain )
val mOutlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
// all corners
outline.setRoundRect(0, 0, view.width, view.height, radius.toFloat())
}
} }
fun setImageResource(glide: GlideRequests, slide: Slide, outlineProvider = mOutlineProvider
clipToOutline = true
}
fun setImageResource(
glide: GlideRequests,
slide: Slide,
isPreview: Boolean
): ListenableFuture<Boolean> = setImageResource(glide, slide, isPreview, 0, 0)
fun setImageResource(
glide: GlideRequests, slide: Slide,
isPreview: Boolean, naturalWidth: Int, isPreview: Boolean, naturalWidth: Int,
naturalHeight: Int, mms: MmsMessageRecord?): ListenableFuture<Boolean> { naturalHeight: Int
): ListenableFuture<Boolean> {
val currentSlide = this.slide
binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() && binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
if (equals(currentSlide, slide)) { if (equals(this.slide, slide)) {
// don't re-load slide // don't re-load slide
return SettableFuture(false) return SettableFuture(false)
} }
if (currentSlide != null && currentSlide.fastPreflightId != null && currentSlide.fastPreflightId == slide.fastPreflightId) {
// not reloading slide for fast preflight
this.slide = slide
}
this.slide = slide this.slide = slide
binding.thumbnailLoadIndicator.isVisible = slide.isInProgress binding.thumbnailLoadIndicator.isVisible = slide.isInProgress
binding.thumbnailDownloadIcon.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED binding.thumbnailDownloadIcon.isVisible =
slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
dimensDelegate.setDimens(naturalWidth, naturalHeight) dimensDelegate.setDimens(naturalWidth, naturalHeight)
invalidate() invalidate()
val result = SettableFuture<Boolean>() return SettableFuture<Boolean>().also {
when { when {
slide.thumbnailUri != null -> { slide.thumbnailUri != null -> {
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, result)) buildThumbnailGlideRequest(glide, slide).into(
GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, it)
)
} }
slide.hasPlaceholder() -> { slide.hasPlaceholder() -> {
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, null, result)) buildPlaceholderGlideRequest(glide, slide).into(
GlideBitmapListeningTarget(binding.thumbnailImage, null, it)
)
} }
else -> { else -> {
glide.clear(binding.thumbnailImage) glide.clear(binding.thumbnailImage)
result.set(false) it.set(false)
}
} }
} }
return result
} }
fun buildThumbnailGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Drawable> { private fun buildThumbnailGlideRequest(
glide: GlideRequests,
val dimens = dimensDelegate.resourceSize() slide: Slide
): GlideRequest<Drawable> = glide.load(DecryptableUri(slide.thumbnailUri!!))
val request = glide.load(DecryptableUri(slide.thumbnailUri!!))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.let { request -> .overrideDimensions()
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
request.override(getDefaultWidth(), getDefaultHeight())
} else {
request.override(dimens[WIDTH], dimens[HEIGHT])
}
}
.transition(DrawableTransitionOptions.withCrossFade()) .transition(DrawableTransitionOptions.withCrossFade())
.centerCrop() .transform(CenterCrop())
.missingThumbnailPicture(slide.isInProgress)
return if (slide.isInProgress) request else request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)) private fun buildPlaceholderGlideRequest(
} glide: GlideRequests,
slide: Slide
fun buildPlaceholderGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Bitmap> { ): GlideRequest<Bitmap> = glide.asBitmap()
val dimens = dimensDelegate.resourceSize()
return glide.asBitmap()
.load(slide.getPlaceholderRes(context.theme)) .load(slide.getPlaceholderRes(context.theme))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.let { request -> .overrideDimensions()
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
request.override(getDefaultWidth(), getDefaultHeight())
} else {
request.override(dimens[WIDTH], dimens[HEIGHT])
}
}
.fitCenter() .fitCenter()
}
open fun clear(glideRequests: GlideRequests) { open fun clear(glideRequests: GlideRequests) {
glideRequests.clear(binding.thumbnailImage) glideRequests.clear(binding.thumbnailImage)
slide = null slide = null
} }
fun setImageResource(glideRequests: GlideRequests, uri: Uri): ListenableFuture<Boolean> { fun setImageResource(
val future = SettableFuture<Boolean>() glideRequests: GlideRequests,
uri: Uri
var request: GlideRequest<Drawable> = glideRequests.load(DecryptableUri(uri)) ): ListenableFuture<Boolean> = glideRequests.load(DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(DrawableTransitionOptions.withCrossFade()) .transition(DrawableTransitionOptions.withCrossFade())
.transform(CenterCrop())
.intoDrawableTargetAsFuture()
request = if (radius > 0) { private fun GlideRequest<Drawable>.intoDrawableTargetAsFuture() =
request.transforms(CenterCrop(), RoundedCorners(radius)) SettableFuture<Boolean>().also {
} else { binding.run {
request.transforms(CenterCrop()) GlideDrawableListeningTarget(thumbnailImage, thumbnailLoadIndicator, it)
}.let { into(it) }
} }
request.into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, future)) private fun <T> GlideRequest<T>.overrideDimensions() =
dimensDelegate.resourceSize().takeIf { 0 !in it }
return future ?.let { override(it[WIDTH], it[HEIGHT]) }
} ?: override(getDefaultWidth(), getDefaultHeight())
} }
private fun <T> GlideRequest<T>.missingThumbnailPicture(
inProgress: Boolean
) = takeIf { inProgress } ?: apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.mediapreview; package org.thoughtcrime.securesms.mediapreview;
import static org.thoughtcrime.securesms.util.GeneralUtilitiesKt.toPx;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -151,6 +153,8 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
{ {
image.setImageResource(glideRequests, media.getUri()); image.setImageResource(glideRequests, media.getUri());
image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive)); image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive));
// set the rounded corners
image.setRoundedCorners(toPx(5, image.getResources()));
outline.setVisibility(isActive ? View.VISIBLE : View.GONE); outline.setVisibility(isActive ? View.VISIBLE : View.GONE);

View File

@ -9,11 +9,6 @@
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"/>
app:minWidth="@dimen/media_bubble_min_width"
app:maxWidth="@dimen/media_bubble_max_width"
app:minHeight="@dimen/media_bubble_min_height"
app:maxHeight="@dimen/media_bubble_max_height"
app:thumbnail_radius="1dp"/>
</FrameLayout> </FrameLayout>

View File

@ -10,14 +10,12 @@
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_2_cell_width" android:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height" android:layout_height="@dimen/album_2_total_height"/>
app:thumbnail_radius="0dp"/>
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_2_cell_width" android:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height" android:layout_height="@dimen/album_2_total_height"
android:layout_gravity="end" android:layout_gravity="end"/>
app:thumbnail_radius="0dp"/>
</FrameLayout> </FrameLayout>

View File

@ -9,15 +9,13 @@
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_3_cell_width_big" android:layout_width="@dimen/album_3_cell_width_big"
android:layout_height="@dimen/album_3_total_height" android:layout_height="@dimen/album_3_total_height"/>
app:thumbnail_radius="0dp"/>
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_3_cell_size_small" android:layout_width="@dimen/album_3_cell_size_small"
android:layout_height="@dimen/album_3_cell_size_small" android:layout_height="@dimen/album_3_cell_size_small"
android:layout_gravity="end|top" android:layout_gravity="end|top"/>
app:thumbnail_radius="0dp"/>
<FrameLayout <FrameLayout
@ -29,8 +27,7 @@
android:id="@+id/album_cell_3" android:id="@+id/album_cell_3"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_gravity="center_horizontal|bottom" android:layout_gravity="center_horizontal|bottom"/>
app:thumbnail_radius="0dp"/>
<TextView <TextView
tools:visibility="visible" tools:visibility="visible"

View File

@ -12,9 +12,7 @@
android:id="@+id/rail_item_image" android:id="@+id/rail_item_image"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:layout_gravity="center" android:layout_gravity="center"/>
android:background="@drawable/mediarail_media_outline"
app:thumbnail_radius="5dp"/>
<ImageView <ImageView
android:id="@+id/rail_item_outline" android:id="@+id/rail_item_outline"