Add polish to stories link previews.

This commit is contained in:
Alex Hart
2022-08-01 13:13:45 -03:00
committed by Greyson Parrelli
parent 19af68a27c
commit c7cd261641
10 changed files with 192 additions and 70 deletions

View File

@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.graphics.Canvas
import android.graphics.Path
import android.graphics.Rect
import android.graphics.RectF
import android.util.AttributeSet
import androidx.cardview.widget.CardView
import androidx.core.graphics.withClip
/**
* Adds manual clipping around the card. This ensures that software rendering
* still maintains border radius of cards.
*/
class ClippedCardView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : CardView(context, attrs) {
private val bounds = Rect()
private val boundsF = RectF()
private val path = Path()
override fun draw(canvas: Canvas) {
canvas.getClipBounds(bounds)
boundsF.set(bounds)
path.reset()
path.addRoundRect(boundsF, radius, radius, Path.Direction.CW)
canvas.withClip(path) {
super.draw(canvas)
}
}
}

View File

@@ -5,6 +5,7 @@ import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
@@ -14,6 +15,7 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.UiThread;
@@ -280,6 +282,14 @@ public class ThumbnailView extends FrameLayout {
forceLayout();
}
public void setImageDrawable(@NonNull GlideRequests glideRequests, @Nullable Drawable drawable) {
glideRequests.clear(image);
glideRequests.clear(blurhash);
image.setImageDrawable(drawable);
blurhash.setImageDrawable(null);
}
@UiThread
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide,
boolean showControls, boolean isPreview)

View File

@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.StoryTextPostView
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
import org.thoughtcrime.securesms.util.visible
class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creation_fragment), TextStoryPostTextEntryFragment.Callback, SafetyNumberBottomSheet.Callbacks {
@@ -102,8 +103,10 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati
send.isEnabled = canSend
}
linkPreviewViewModel.linkPreviewState.observe(viewLifecycleOwner) { state ->
storyTextPostView.bindLinkPreviewState(state, View.GONE)
LiveDataUtil.combineLatest(viewModel.state, linkPreviewViewModel.linkPreviewState) { viewState, linkState ->
Pair(viewState.body.isBlank(), linkState)
}.observe(viewLifecycleOwner) { (useLargeThumb, linkState) ->
storyTextPostView.bindLinkPreviewState(linkState, View.GONE, useLargeThumb)
storyTextPostView.postAdjustLinkPreviewTranslationY()
}

View File

@@ -62,7 +62,7 @@ class TextStoryPostLinkEntryFragment : KeyboardEntryDialogFragment(
}
linkPreviewViewModel.linkPreviewState.observe(viewLifecycleOwner) { state ->
linkPreview.bind(state)
linkPreview.bind(state, useLargeThumbnail = false)
shareALinkGroup.visible = !state.isLoading && !state.linkPreview.isPresent && (state.error == null && state.activeUrlForError == null)
confirmButton.isEnabled = state.linkPreview.isPresent || state.activeUrlForError != null
}

View File

@@ -1,19 +1,20 @@
package org.thoughtcrime.securesms.stories
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.OutlinedThumbnailView
import org.thoughtcrime.securesms.components.ThumbnailView
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.ImageSlide
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture
import org.thoughtcrime.securesms.util.concurrent.SettableFuture
import org.thoughtcrime.securesms.util.views.Stub
@@ -32,26 +33,54 @@ class StoryLinkPreviewView @JvmOverloads constructor(
}
private val close: View = findViewById(R.id.link_preview_close)
private val image: OutlinedThumbnailView = findViewById(R.id.link_preview_image)
private val smallImage: ThumbnailView = findViewById(R.id.link_preview_image)
private val largeImage: ThumbnailView = findViewById(R.id.link_preview_large)
private val title: TextView = findViewById(R.id.link_preview_title)
private val url: TextView = findViewById(R.id.link_preview_url)
private val description: TextView = findViewById(R.id.link_preview_description)
private val fallbackIcon: ImageView = findViewById(R.id.link_preview_fallback_icon)
private val loadingSpinner: Stub<View> = Stub(findViewById(R.id.loading_spinner))
fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE): ListenableFuture<Boolean> {
var listenableFuture: ListenableFuture<Boolean>? = null
private fun getThumbnailTarget(useLargeThumbnail: Boolean): ThumbnailView {
return if (useLargeThumbnail) largeImage else smallImage
}
fun getThumbnailViewWidth(useLargeThumbnail: Boolean): Int {
return getThumbnailTarget(useLargeThumbnail).measuredWidth
}
fun getThumbnailViewHeight(useLargeThumbnail: Boolean): Int {
return getThumbnailTarget(useLargeThumbnail).measuredHeight
}
fun setThumbnailDrawable(drawable: Drawable?, useLargeThumbnail: Boolean) {
val image = getThumbnailTarget(useLargeThumbnail)
image.setImageDrawable(GlideApp.with(this), drawable)
}
fun bind(linkPreview: LinkPreview?, hiddenVisibility: Int = View.INVISIBLE, useLargeThumbnail: Boolean, loadThumbnail: Boolean = true): ListenableFuture<Boolean> {
var future: ListenableFuture<Boolean>? = null
if (linkPreview != null) {
visibility = View.VISIBLE
isClickable = true
val corners = DimensionUnit.DP.toPixels(18f).toInt()
image.setCorners(corners, corners, corners, corners)
val image = getThumbnailTarget(useLargeThumbnail)
val notImage = getThumbnailTarget(!useLargeThumbnail)
val imageSlide: ImageSlide? = linkPreview.thumbnail.map { ImageSlide(image.context, it) }.orElse(null)
notImage.visible = false
val imageSlide: Slide? = linkPreview.thumbnail.map { ImageSlide(context, it) }.orElse(null)
if (imageSlide != null) {
listenableFuture = image.setImageResource(GlideApp.with(image), imageSlide, false, false)
if (loadThumbnail) {
future = image.setImageResource(
GlideApp.with(this),
imageSlide,
false,
false
)
}
image.visible = true
fallbackIcon.visible = false
} else {
@@ -69,17 +98,17 @@ class StoryLinkPreviewView @JvmOverloads constructor(
isClickable = false
}
return listenableFuture ?: SettableFuture(false)
return future ?: SettableFuture(false)
}
fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE) {
fun bind(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int = View.INVISIBLE, useLargeThumbnail: Boolean) {
val linkPreview: LinkPreview? = linkPreviewState.linkPreview.orElseGet {
linkPreviewState.activeUrlForError?.let {
LinkPreview(it, LinkPreviewUtil.getTopLevelDomain(it) ?: it, null, -1L, null)
}
}
bind(linkPreview, hiddenVisibility)
bind(linkPreview, hiddenVisibility, useLargeThumbnail)
loadingSpinner.get().visible = linkPreviewState.isLoading
if (linkPreviewState.isLoading) {

View File

@@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.os.Parcel
import android.os.Parcelable
import android.view.ContextThemeWrapper
import android.view.View
import androidx.core.graphics.scale
import androidx.core.view.drawToBitmap
@@ -14,6 +15,7 @@ import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.resource.SimpleResource
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
@@ -23,6 +25,8 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.fonts.TextFont
import org.thoughtcrime.securesms.fonts.TextToScript
import org.thoughtcrime.securesms.fonts.TypefaceCache
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Base64
@@ -109,7 +113,7 @@ data class StoryTextPostModel(
override fun decode(source: StoryTextPostModel, width: Int, height: Int, options: Options): Resource<Bitmap> {
val message = SignalDatabase.mmsSms.getMessageFor(source.storySentAtMillis, source.storyAuthor)
val view = StoryTextPostView(ApplicationDependencies.getApplication())
val view = StoryTextPostView(ContextThemeWrapper(ApplicationDependencies.getApplication(), R.style.TextSecure_DarkNoActionBar))
val typeface = TypefaceCache.get(
ApplicationDependencies.getApplication(),
TextFont.fromStyle(source.storyTextPost.style),
@@ -119,9 +123,30 @@ data class StoryTextPostModel(
val displayWidth: Int = ApplicationDependencies.getApplication().resources.displayMetrics.widthPixels
val arHeight: Int = (RENDER_HW_AR * displayWidth).toInt()
val linkPreview = (message as? MmsMessageRecord)?.linkPreviews?.firstOrNull()
val useLargeThumbnail = source.text.isBlank()
view.setTypeface(typeface)
view.bindFromStoryTextPost(source.storyTextPost)
view.bindLinkPreview((message as? MmsMessageRecord)?.linkPreviews?.firstOrNull())
view.bindLinkPreview(linkPreview, useLargeThumbnail, loadThumbnail = false)
view.postAdjustLinkPreviewTranslationY()
view.invalidate()
view.measure(View.MeasureSpec.makeMeasureSpec(displayWidth, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(arHeight, View.MeasureSpec.EXACTLY))
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
val drawable = if (linkPreview != null && linkPreview.thumbnail.isPresent) {
GlideApp
.with(view)
.load(DecryptableStreamUriLoader.DecryptableUri(linkPreview.thumbnail.get().uri!!))
.centerCrop()
.submit(view.getLinkPreviewThumbnailWidth(useLargeThumbnail), view.getLinkPreviewThumbnailHeight(useLargeThumbnail))
.get()
} else {
null
}
view.setLinkPreviewDrawable(drawable, useLargeThumbnail)
view.invalidate()
view.measure(View.MeasureSpec.makeMeasureSpec(displayWidth, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(arHeight, View.MeasureSpec.EXACTLY))

View File

@@ -48,6 +48,14 @@ class StoryTextPostView @JvmOverloads constructor(
TextStoryTextWatcher.install(textView)
}
fun getLinkPreviewThumbnailWidth(useLargeThumbnail: Boolean): Int {
return linkPreviewView.getThumbnailViewWidth(useLargeThumbnail)
}
fun getLinkPreviewThumbnailHeight(useLargeThumbnail: Boolean): Int {
return linkPreviewView.getThumbnailViewHeight(useLargeThumbnail)
}
fun showCloseButton() {
linkPreviewView.setCanClose(true)
}
@@ -148,12 +156,16 @@ class StoryTextPostView @JvmOverloads constructor(
postAdjustLinkPreviewTranslationY()
}
fun bindLinkPreview(linkPreview: LinkPreview?): ListenableFuture<Boolean> {
return linkPreviewView.bind(linkPreview, View.GONE)
fun bindLinkPreview(linkPreview: LinkPreview?, useLargeThumbnail: Boolean, loadThumbnail: Boolean = true): ListenableFuture<Boolean> {
return linkPreviewView.bind(linkPreview, View.GONE, useLargeThumbnail, loadThumbnail)
}
fun bindLinkPreviewState(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int) {
linkPreviewView.bind(linkPreviewState, hiddenVisibility)
fun setLinkPreviewDrawable(drawable: Drawable?, useLargeThumbnail: Boolean) {
linkPreviewView.setThumbnailDrawable(drawable, useLargeThumbnail)
}
fun bindLinkPreviewState(linkPreviewState: LinkPreviewViewModel.LinkPreviewState, hiddenVisibility: Int, useLargeThumbnail: Boolean) {
linkPreviewView.bind(linkPreviewState, hiddenVisibility, useLargeThumbnail)
}
fun postAdjustLinkPreviewTranslationY() {
@@ -186,7 +198,7 @@ class StoryTextPostView @JvmOverloads constructor(
}
private fun canDisplayText(): Boolean {
return !(linkPreviewView.isVisible && isPlaceholder)
return !(linkPreviewView.isVisible && (isPlaceholder || textView.text.isEmpty()))
}
private fun adjustLinkPreviewTranslationY() {

View File

@@ -46,7 +46,8 @@ class StoryTextPostPreviewFragment : Fragment(R.layout.stories_text_post_preview
StoryTextPostState.LoadState.INIT -> Unit
StoryTextPostState.LoadState.LOADED -> {
storyTextPostView.bindFromStoryTextPost(state.storyTextPost!!)
storyTextPostView.bindLinkPreview(state.linkPreview)
storyTextPostView.bindLinkPreview(state.linkPreview, state.storyTextPost.body.isBlank())
storyTextPostView.postAdjustLinkPreviewTranslationY()
if (state.linkPreview != null) {
storyTextPostView.setLinkPreviewClickListener {