Description improvements (#1846)

* Increase character limit for media descriptions to 1500

It was increased in Mastodon 3.0.0 which was released in October 2019.

* Improve image description view

Since media descriptions can be longer now, we need to adjust the UI.
It is a common problem that description takes up the whole screen, it's
hard for readers and also discourages people from adding descriptions.

This commit uses bottom sheet to hide most of the description. Since we
know how much screen space it will cover, we can use darker background
which makes reading text easier.

* Adjust description handle

* Fix unable to dismiss image caption
main
Ivan Kupalov 4 years ago committed by Alibek Omarov
parent 8b8958a9c2
commit 22b074f172
  1. 2
      app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
  2. 5
      app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt
  3. 31
      app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt
  4. 18
      app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt
  5. 20
      app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
  6. 10
      app/src/main/res/drawable/description_bg_expanded.xml
  7. 9
      app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml
  8. 60
      app/src/main/res/layout/fragment_view_image.xml

@ -805,7 +805,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
itemView.setAccessibilityDelegate(null);
} else {
if (payloads instanceof List)
for (Object item : (List) payloads) {
for (Object item : (List<?>) payloads) {
if (Key.KEY_CREATED.equals(item)) {
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
}

@ -41,9 +41,8 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.util.withLifecycleContext
import java.io.File
// https://github.com/tootsuite/mastodon/blob/1656663/app/models/media_attachment.rb#L94
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 420
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
fun <T> T.makeCaptionDialog(existingDescription: String?,
previewUri: Uri,

@ -22,16 +22,9 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.TextView
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
import com.github.piasy.biv.BigImageViewer
import com.github.piasy.biv.loader.ImageLoader
@ -69,15 +62,19 @@ class ViewImageFragment : ViewMediaFragment() {
private var previewUri = Uri.EMPTY
private var showingPreview = false
override lateinit var descriptionView: TextView
override fun onAttach(context: Context) {
super.onAttach(context)
photoActionsListener = context as PhotoActionsListener
}
override fun setupMediaView(url: String, previewUrl: String?) {
descriptionView = mediaDescription
override fun setupMediaView(url: String,
previewUrl: String?,
description: String?,
showingDescription: Boolean) {
photoView.transitionName = url
mediaDescription.text = description
captionSheet.visible(showingDescription)
startedTransition = false
uri = Uri.parse(url)
if(previewUrl != null && !previewUrl.equals(url)) {
@ -91,8 +88,6 @@ class ViewImageFragment : ViewMediaFragment() {
return inflater.inflate(R.layout.fragment_view_image, container, false)
}
private lateinit var gestureDetector : GestureDetector
private val imageOnTouchListener = object : View.OnTouchListener {
private var lastY = 0.0f
private var swipeStartedWithOneFinger = false
@ -100,7 +95,6 @@ class ViewImageFragment : ViewMediaFragment() {
override fun onTouch(v: View, event: MotionEvent): Boolean {
// This part is for scaling/translating on vertical move.
// We use raw coordinates to get the correct ones during scaling
gestureDetector.onTouchEvent(event)
if(event.pointerCount != 1) {
onGestureEnd()
@ -143,13 +137,6 @@ class ViewImageFragment : ViewMediaFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
gestureDetector = GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
onMediaTap()
return true
}
})
photoView.setImageLoaderCallback(imageLoaderCallback)
photoView.setImageViewFactory(GlideImageViewFactory())
@ -190,10 +177,10 @@ class ViewImageFragment : ViewMediaFragment() {
}
isDescriptionVisible = showingDescription && visible
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
descriptionView.animate().alpha(alpha)
captionSheet.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
descriptionView.visible(isDescriptionVisible)
captionSheet.visible(isDescriptionVisible)
animation.removeListener(this)
}
})

@ -17,18 +17,20 @@ package com.keylesspalace.tusky.fragment
import android.os.Bundle
import android.text.TextUtils
import android.widget.TextView
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.visible
abstract class ViewMediaFragment : BaseFragment() {
private var toolbarVisibiltyDisposable: Function0<Boolean>? = null
abstract fun setupMediaView(url: String, previewUrl: String?)
abstract fun setupMediaView(
url: String,
previewUrl: String?,
description: String?,
showingDescription: Boolean
)
abstract fun onToolbarVisibilityChange(visible: Boolean)
abstract val descriptionView: TextView
protected var showingDescription = false
protected var isDescriptionVisible = false
@ -36,6 +38,7 @@ abstract class ViewMediaFragment : BaseFragment() {
companion object {
@JvmStatic
protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition"
@JvmStatic
protected val ARG_ATTACHMENT = "attach"
@JvmStatic
@ -74,13 +77,10 @@ abstract class ViewMediaFragment : BaseFragment() {
protected fun finalizeViewSetup(url: String, previewUrl: String?, description: String?) {
val mediaActivity = activity as ViewMediaActivity
setupMediaView(url, previewUrl)
descriptionView.text = description ?: ""
showingDescription = !TextUtils.isEmpty(description)
isDescriptionVisible = showingDescription
descriptionView.visible(showingDescription && mediaActivity.isToolbarVisible)
setupMediaView(url, previewUrl, description, showingDescription && mediaActivity.isToolbarVisible)
toolbarVisibiltyDisposable = (activity as ViewMediaActivity)
.addToolbarVisibilityListener { isVisible ->

@ -26,7 +26,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.MediaController
import android.widget.TextView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment
@ -47,7 +46,6 @@ class ViewVideoFragment : ViewMediaFragment() {
}
private lateinit var mediaActivity: ViewMediaActivity
private val TOOLBAR_HIDE_DELAY_MS = 3000L
override lateinit var descriptionView : TextView
private lateinit var mediaController : MediaController
private var isAudio = false
@ -71,8 +69,14 @@ class ViewVideoFragment : ViewMediaFragment() {
}
@SuppressLint("ClickableViewAccessibility")
override fun setupMediaView(url: String, previewUrl: String?) {
descriptionView = mediaDescription
override fun setupMediaView(
url: String,
previewUrl: String?,
description: String?,
showingDescription: Boolean
) {
mediaDescription.text = description
mediaDescription.visible(showingDescription)
videoView.transitionName = url
videoView.setVideoPath(url)
@ -178,14 +182,14 @@ class ViewVideoFragment : ViewMediaFragment() {
val alpha = if (isDescriptionVisible) 1.0f else 0.0f
if (isDescriptionVisible) {
// If to be visible, need to make visible immediately and animate alpha
descriptionView.alpha = 0.0f
descriptionView.visible(isDescriptionVisible)
mediaDescription.alpha = 0.0f
mediaDescription.visible(isDescriptionVisible)
}
descriptionView.animate().alpha(alpha)
mediaDescription.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
descriptionView.visible(isDescriptionVisible)
mediaDescription.visible(isDescriptionVisible)
animation.removeListener(this)
}
})

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:topLeftRadius="6dp"
android:topRightRadius="6dp" />
<stroke
android:color="?attr/colorBackgroundAccent"
android:width="1dp" />
<solid android:color="#B3000000" />
</shape>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="8dp"
android:height="1dp" />
<solid android:color="?attr/colorBackgroundAccent" />
<corners android:radius="1dp" />
</shape>

@ -26,18 +26,56 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/mediaDescription"
<!-- This should be inside CoordinatorLayout for two reasons:
1. TouchImageView really wants some constraints ans has no size otherwise
2. We don't want sheet to overlap with appbar but the only way to do it with autosizing
is to gibe parent some margin. -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#60000000"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:padding="8dp"
android:textAlignment="center"
android:textColor="#eee"
android:textSize="?attr/status_text_medium"
android:layout_height="0dp"
android:layout_marginTop="70dp"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="Some media description" />
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:id="@+id/captionSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/description_bg_expanded"
android:orientation="vertical"
app:behavior_peekHeight="90dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<View
android:layout_width="24dp"
android:layout_height="3dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_gravity="center_horizontal"
android:importantForAccessibility="no"
android:background="@drawable/ic_drag_indicator_horiz_24dp" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/mediaDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textColor="#eee"
android:textSize="?attr/status_text_medium"
tools:text="Some media description which might get quite long so that it won't easily fit in one line" />
</androidx.core.widget.NestedScrollView>
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Loading…
Cancel
Save