Transfer Control View Improvements.

This commit is contained in:
Nicholas
2023-10-13 10:03:42 -04:00
committed by GitHub
parent 5cc85cc860
commit af063b2e9e
6 changed files with 178 additions and 86 deletions

View File

@@ -13,9 +13,6 @@ import android.view.LayoutInflater
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@@ -29,25 +26,20 @@ import org.thoughtcrime.securesms.events.PartProgressEvent
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.rx.RxStore
import org.thoughtcrime.securesms.util.visible
import java.util.UUID
class TransferControlView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
private val uuid = UUID.randomUUID().toString()
private val binding: TransferControlsViewBinding
private val store = RxStore(TransferControlViewState())
private val disposables = CompositeDisposable().apply {
add(store)
}
private var previousState = TransferControlViewState()
private var state = TransferControlViewState()
init {
tag = uuid
binding = TransferControlsViewBinding.inflate(LayoutInflater.from(context), this)
visibility = GONE
isLongClickable = false
disposables += store.stateFlowable.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe {
applyState(it)
}
addOnAttachStateChangeListener(RecyclerViewParentTransitionController(child = this))
}
@@ -60,11 +52,20 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
EventBus.getDefault().unregister(this)
disposables.clear()
}
private fun updateState(stateFactory: (TransferControlViewState) -> TransferControlViewState) {
val newState = stateFactory.invoke(state)
if (newState != state) {
applyState(newState)
}
state = newState
}
private fun applyState(currentState: TransferControlViewState) {
when (deriveMode(currentState)) {
val mode = deriveMode(currentState)
verboseLog("New state applying, mode = $mode")
when (mode) {
Mode.PENDING_GALLERY -> displayPendingGallery(currentState)
Mode.PENDING_GALLERY_CONTAINS_PLAYABLE -> displayPendingGalleryWithPlayable(currentState)
Mode.PENDING_SINGLE_ITEM -> displayPendingSingleItem(currentState)
@@ -76,18 +77,18 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
Mode.UPLOADING_SINGLE_ITEM -> displayUploadingSingleItem(currentState)
Mode.RETRY_DOWNLOADING -> displayRetry(currentState, false)
Mode.RETRY_UPLOADING -> displayRetry(currentState, true)
Mode.GONE -> displayChildrenAsGone(currentState, previousState)
Mode.GONE -> displayChildrenAsGone()
}
previousState = currentState
}
private fun deriveMode(currentState: TransferControlViewState): Mode {
if (currentState.slides.isEmpty()) {
verboseLog("Setting empty slide deck to GONE")
return Mode.GONE
}
if (currentState.slides.all { it.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE }) {
verboseLog("Setting slide deck that's finished to GONE\n\t${slidesAsListOfTimestamps(currentState.slides)}")
return Mode.GONE
}
@@ -190,15 +191,17 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
}
AttachmentTable.TRANSFER_PROGRESS_DONE -> {
verboseLog("[Case 2] Setting slide deck that's finished to GONE\t${slidesAsListOfTimestamps(currentState.slides)}")
return Mode.GONE
}
}
}
} else {
verboseLog("Setting slide deck to GONE because isVisible is false:\t${slidesAsListOfTimestamps(currentState.slides)}")
return Mode.GONE
}
Log.i(TAG, "Hit default mode case, this should not happen.")
Log.i(TAG, "[$uuid] Hit default mode case, this should not happen.")
return Mode.GONE
}
@@ -212,6 +215,11 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
secondaryDetailsText = currentState.showSecondaryText
)
binding.primaryDetailsText.translationX = if (ViewUtil.isLtr(this)) {
ViewUtil.dpToPx(-8).toFloat()
} else {
ViewUtil.dpToPx(8).toFloat()
}
val remainingSlides = currentState.slides.filterNot { it.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE }
val downloadCount = remainingSlides.size
binding.primaryDetailsText.text = context.resources.getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount)
@@ -374,7 +382,7 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
binding.secondaryDetailsText.text = resources.getString(R.string.NetworkFailure__retry)
}
private fun displayChildrenAsGone(currentState: TransferControlViewState, prevState: TransferControlViewState) {
private fun displayChildrenAsGone() {
children.forEach {
it.visible = false
}
@@ -427,37 +435,44 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
override fun setFocusable(focusable: Boolean) {
super.setFocusable(false)
store.update { it.copy(isFocusable = focusable) }
verboseLog("setFocusable update: $focusable")
updateState { it.copy(isFocusable = focusable) }
}
override fun setClickable(clickable: Boolean) {
super.setClickable(false)
store.update { it.copy(isClickable = clickable) }
verboseLog("setClickable update: $clickable")
updateState { it.copy(isClickable = clickable) }
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onEventAsync(event: PartProgressEvent) {
val attachment = event.attachment
store.update {
updateState {
verboseLog("onEventAsync update")
if (!it.networkProgress.containsKey(attachment)) {
return@update it
verboseLog("onEventAsync update ignored")
return@updateState it
}
if (event.type == PartProgressEvent.Type.COMPRESSION) {
val mutableMap = it.compressionProgress.toMutableMap()
mutableMap[attachment] = Progress.fromEvent(event)
return@update it.copy(compressionProgress = mutableMap.toMap())
verboseLog("onEventAsync compression update")
return@updateState it.copy(compressionProgress = mutableMap.toMap())
} else {
val mutableMap = it.networkProgress.toMutableMap()
mutableMap[attachment] = Progress.fromEvent(event)
return@update it.copy(networkProgress = mutableMap.toMap())
verboseLog("onEventAsync network update")
return@updateState it.copy(networkProgress = mutableMap.toMap())
}
}
}
fun setSlides(slides: List<Slide>) {
require(slides.isNotEmpty()) { "Must provide at least one slide." }
store.update { state ->
require(slides.isNotEmpty()) { "[$uuid] Must provide at least one slide." }
updateState { state ->
verboseLog("State update for new slides: ${slidesAsListOfTimestamps(slides)}")
val isNewSlideSet = !isUpdateToExistingSet(state, slides)
val networkProgress: MutableMap<Attachment, Progress> = if (isNewSlideSet) HashMap() else state.networkProgress.toMutableMap()
if (isNewSlideSet) {
@@ -476,14 +491,25 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
val playableWhileDownloading = allStreamableOrDone
val isOutgoing = slides.any { it.asAttachment().uploadTimestamp == 0L }
state.copy(
val result = state.copy(
slides = slides,
networkProgress = networkProgress,
compressionProgress = compressionProgress,
playableWhileDownloading = playableWhileDownloading,
isOutgoing = isOutgoing
)
verboseLog("New state calculated and being returned for new slides: ${slidesAsListOfTimestamps(slides)}\n$result")
return@updateState result
}
verboseLog("End of setSlides() for ${slidesAsListOfTimestamps(slides)}")
}
private fun slidesAsListOfTimestamps(slides: List<Slide>): String {
if (!VERBOSE_DEVELOPMENT_LOGGING) {
return ""
}
return slides.map { it.asAttachment().uploadTimestamp }.joinToString()
}
private fun isUpdateToExistingSet(currentState: TransferControlViewState, slides: List<Slide>): Boolean {
@@ -499,38 +525,53 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
}
fun setDownloadClickListener(listener: OnClickListener) {
store.update {
it.copy(downloadClickedListener = listener)
verboseLog("downloadClickListener update")
updateState {
it.copy(
downloadClickedListener = listener
)
}
}
fun setCancelClickListener(listener: OnClickListener) {
store.update {
it.copy(cancelDownloadClickedListener = listener)
verboseLog("cancelClickListener update")
updateState {
it.copy(
cancelDownloadClickedListener = listener
)
}
}
fun setInstantPlaybackClickListener(listener: OnClickListener) {
store.update {
it.copy(instantPlaybackClickListener = listener)
verboseLog("instantPlaybackClickListener update")
updateState {
it.copy(
instantPlaybackClickListener = listener
)
}
}
fun clear() {
clearAnimation()
visibility = GONE
store.update { TransferControlViewState() }
updateState { TransferControlViewState() }
}
fun setShowSecondaryText(showSecondaryText: Boolean) {
store.update {
it.copy(showSecondaryText = showSecondaryText)
verboseLog("showSecondaryText update: $showSecondaryText")
updateState {
it.copy(
showSecondaryText = showSecondaryText
)
}
}
fun setVisible(isVisible: Boolean) {
store.update {
it.copy(isVisible = isVisible)
verboseLog("showSecondaryText update: $isVisible")
updateState {
it.copy(
isVisible = isVisible
)
}
}
@@ -558,12 +599,22 @@ class TransferControlView @JvmOverloads constructor(context: Context, attrs: Att
}
}
/**
* This is an extremely chatty logging mode for local development. Each view is assigned a UUID so that you can filter by view inside a conversation.
*/
private fun verboseLog(message: String) {
if (VERBOSE_DEVELOPMENT_LOGGING) {
Log.d(TAG, "[$uuid] $message")
}
}
companion object {
private const val TAG = "TransferControlView"
private const val VERBOSE_DEVELOPMENT_LOGGING = false
private const val UPLOAD_TASK_WEIGHT = 1
/**
* A weighting compared to [.UPLOAD_TASK_WEIGHT]
* A weighting compared to [UPLOAD_TASK_WEIGHT]
*/
private const val COMPRESSION_TASK_WEIGHT = 3

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.components.transfercontrols
import android.content.Context
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PorterDuff
@@ -16,7 +17,6 @@ import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.withTranslation
import org.signal.core.util.dp
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import kotlin.math.roundToInt
@@ -32,28 +32,55 @@ class TransferProgressView @JvmOverloads constructor(
) : View(context, attrs, defStyleAttr, defStyleRes) {
companion object {
const val TAG = "TransferProgressView"
private const val PROGRESS_ARC_STROKE_WIDTH = 1.5f
private const val ICON_INSET_PERCENT = 0.2f
private const val PROGRESS_ARC_STROKE_WIDTH_DP = 1.5f
private const val ICON_SIZE_DP = 24f
private const val STOP_CORNER_RADIUS_DP = 4f
private const val PROGRESS_BAR_INSET_DP = 3
}
private val iconColor: Int
private val progressColor: Int
private val trackColor: Int
private val stopIconPaint: Paint
private val progressPaint: Paint
private val trackPaint: Paint
private val progressArcStrokeWidth: Float
private val iconSize: Float
private val stopIconSize: Float
private val stopIconCornerRadius: Float
private val progressRect = RectF()
private val stopIconRect = RectF()
private val progressPaint = progressPaint()
private val stopIconPaint = stopIconPaint()
private val trackPaint = trackPaint()
private val downloadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_24)
private val uploadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up_16)
private var progressPercent = 0f
private var currentState = State.UNINITIALIZED
private val downloadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_down_24)
private val uploadDrawable = ContextCompat.getDrawable(context, R.drawable.ic_arrow_up_16)
var startClickListener: OnClickListener? = null
var cancelClickListener: OnClickListener? = null
init {
val tint = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
val filter = PorterDuffColorFilter(tint, PorterDuff.Mode.SRC_ATOP)
val displayDensity = Resources.getSystem().displayMetrics.density
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.TransferProgressView, 0, 0)
val signalCustomColor = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
val signalTransparent2 = ContextCompat.getColor(context, R.color.signal_colorTransparent2)
iconColor = typedArray.getColor(R.styleable.TransferProgressView_transferIconColor, signalCustomColor)
progressColor = typedArray.getColor(R.styleable.TransferProgressView_progressColor, signalCustomColor)
trackColor = typedArray.getColor(R.styleable.TransferProgressView_trackColor, signalTransparent2)
progressArcStrokeWidth = typedArray.getDimension(R.styleable.TransferProgressView_progressArcWidth, PROGRESS_ARC_STROKE_WIDTH_DP * displayDensity)
iconSize = typedArray.getDimension(R.styleable.TransferProgressView_iconSize, ICON_SIZE_DP * displayDensity)
stopIconSize = typedArray.getDimension(R.styleable.TransferProgressView_stopIconSize, ICON_SIZE_DP * displayDensity)
stopIconCornerRadius = typedArray.getDimension(R.styleable.TransferProgressView_stopIconCornerRadius, STOP_CORNER_RADIUS_DP * displayDensity)
typedArray.recycle()
progressPaint = progressPaint(progressColor)
stopIconPaint = stopIconPaint(iconColor)
trackPaint = trackPaint(trackColor)
val filter = PorterDuffColorFilter(iconColor, PorterDuff.Mode.SRC_ATOP)
downloadDrawable?.colorFilter = filter
uploadDrawable?.colorFilter = filter
}
@@ -100,49 +127,46 @@ class TransferProgressView @JvmOverloads constructor(
}
private fun drawProgress(canvas: Canvas, progressPercent: Float) {
val miniIcon = height < 32.dp
val stopIconCornerRadius = if (miniIcon) 1f.dp else 4f.dp
val iconSize: Float = if (miniIcon) 5.5f.dp else 16f.dp
stopIconRect.set(0f, 0f, iconSize, iconSize)
stopIconRect.set(0f, 0f, stopIconSize, stopIconSize)
canvas.withTranslation(width / 2 - (iconSize / 2), height / 2 - (iconSize / 2)) {
canvas.withTranslation(width / 2 - (stopIconSize / 2), height / 2 - (stopIconSize / 2)) {
drawRoundRect(stopIconRect, stopIconCornerRadius, stopIconCornerRadius, stopIconPaint)
}
val widthDp = PROGRESS_ARC_STROKE_WIDTH.dp
val inset = 2.dp
progressRect.top = widthDp + inset
progressRect.left = widthDp + inset
progressRect.right = (width - widthDp) - inset
progressRect.bottom = (height - widthDp) - inset
val trackWidthScaled = progressArcStrokeWidth
val inset: Float = PROGRESS_BAR_INSET_DP * Resources.getSystem().displayMetrics.density
progressRect.left = trackWidthScaled + inset
progressRect.top = trackWidthScaled + inset
progressRect.right = (width - trackWidthScaled) - inset
progressRect.bottom = (height - trackWidthScaled) - inset
canvas.drawArc(progressRect, 0f, 360f, false, trackPaint)
canvas.drawArc(progressRect, 270f, 360f * progressPercent, false, progressPaint)
}
private fun stopIconPaint(): Paint {
private fun stopIconPaint(paintColor: Int): Paint {
val stopIconPaint = Paint()
stopIconPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
stopIconPaint.color = paintColor
stopIconPaint.isAntiAlias = true
stopIconPaint.style = Paint.Style.FILL
return stopIconPaint
}
private fun trackPaint(): Paint {
private fun trackPaint(trackColor: Int): Paint {
val trackPaint = Paint()
trackPaint.color = ContextCompat.getColor(context, R.color.signal_colorTransparent2)
trackPaint.color = trackColor
trackPaint.isAntiAlias = true
trackPaint.style = Paint.Style.STROKE
trackPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp
trackPaint.strokeWidth = progressArcStrokeWidth
return trackPaint
}
private fun progressPaint(): Paint {
private fun progressPaint(progressColor: Int): Paint {
val progressPaint = Paint()
progressPaint.color = ContextCompat.getColor(context, R.color.signal_colorOnCustom)
progressPaint.color = progressColor
progressPaint.isAntiAlias = true
progressPaint.style = Paint.Style.STROKE
progressPaint.strokeWidth = PROGRESS_ARC_STROKE_WIDTH.dp
progressPaint.strokeWidth = progressArcStrokeWidth
return progressPaint
}
@@ -152,13 +176,18 @@ class TransferProgressView @JvmOverloads constructor(
return
}
drawable.setBounds(
(width * ICON_INSET_PERCENT).roundToInt(),
(height * ICON_INSET_PERCENT).roundToInt(),
(width * (1 - ICON_INSET_PERCENT)).roundToInt(),
(height * (1 - ICON_INSET_PERCENT)).roundToInt()
)
val centerX = width / 2f
val centerY = height / 2f
// 0, 0 is the top left corner
// width, height is the bottom right
val halfIconSize = (iconSize / 2f)
val left = (centerX - halfIconSize).roundToInt().coerceAtLeast(0)
val top = (centerY - halfIconSize).roundToInt().coerceAtLeast(0)
val right = (centerX + halfIconSize).roundToInt().coerceAtMost(width)
val bottom = (centerY + halfIconSize).roundToInt().coerceAtMost(height)
drawable.setBounds(left, top, right, bottom)
drawable.draw(canvas)
}