mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Transfer Control View Improvements.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user