diff --git a/CHANGES.md b/CHANGES.md index 23bc0427e9..a6f8982a27 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ Features ✨: Improvements 🙌: - PIN code: request PIN code if phone has been locked + - Small optimisation of scrolling experience in timeline (#2114) Bugfix 🐛: - Fix Splash layout on small screens diff --git a/vector/build.gradle b/vector/build.gradle index 0c7985b45d..30ca90e1d8 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -326,6 +326,7 @@ dependencies { implementation 'com.jakewharton.rxbinding3:rxbinding-material:3.0.0' implementation("com.airbnb.android:epoxy:$epoxy_version") + implementation "com.airbnb.android:epoxy-glide-preloading:$epoxy_version" kapt "com.airbnb.android:epoxy-processor:$epoxy_version" implementation "com.airbnb.android:epoxy-paging:$epoxy_version" implementation 'com.airbnb.android:mvrx:1.3.0' diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 7c3ac6011e..e7140f06f4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -53,6 +53,8 @@ import androidx.recyclerview.widget.RecyclerView import butterknife.BindView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.OnModelBuildFinishedListener +import com.airbnb.epoxy.addGlidePreloader +import com.airbnb.epoxy.glidePreloader import com.airbnb.mvrx.Async import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -75,6 +77,7 @@ import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.extensions.trackItemsVisibilityChange import im.vector.app.core.glide.GlideApp +import im.vector.app.core.glide.GlideRequests import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider @@ -218,7 +221,8 @@ class RoomDetailFragment @Inject constructor( private val colorProvider: ColorProvider, private val notificationUtils: NotificationUtils, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, - private val matrixItemColorProvider: MatrixItemColorProvider + private val matrixItemColorProvider: MatrixItemColorProvider, + private val imageContentRenderer: ImageContentRenderer ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -921,6 +925,16 @@ class RoomDetailFragment @Inject constructor( val touchHelper = ItemTouchHelper(swipeCallback) touchHelper.attachToRecyclerView(recyclerView) } + recyclerView.addGlidePreloader( + epoxyController = timelineEventController, + requestManager = GlideApp.with(this), + preloader = glidePreloader { requestManager, epoxyModel: MessageImageVideoItem, _ -> + imageContentRenderer.createGlideRequest( + epoxyModel.mediaData, + ImageContentRenderer.Mode.THUMBNAIL, + requestManager as GlideRequests + ) + }) } private fun updateJumpToReadMarkerViewVisibility() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index be59128c26..56de0f7829 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -56,6 +56,8 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject +private const val DEFAULT_PREFETCH_THRESHOLD = 30 + class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder, @@ -116,6 +118,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private var unreadState: UnreadState = UnreadState.Unknown private var positionOfReadMarker: Int? = null private var eventIdToHighlight: String? = null + private var previousModelsSize = 0 var callback: Callback? = null var timeline: Timeline? = null @@ -191,6 +194,29 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec models.add(position, readMarker) } } + val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false + if (shouldAddBackwardPrefetch) { + val indexOfPrefetchBackward = (previousModelsSize - 1) + .coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD) + .coerceAtLeast(0) + + val loadingItem = LoadingItem_() + .id("prefetch_backward_loading${System.currentTimeMillis()}") + .showLoader(false) + .setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS) + + models.add(indexOfPrefetchBackward, loadingItem) + } + val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false + if (shouldAddForwardPrefetch) { + val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1) + val loadingItem = LoadingItem_() + .id("prefetch_forward_loading${System.currentTimeMillis()}") + .showLoader(false) + .setVisibilityStateChangedListener(Timeline.Direction.FORWARDS) + models.add(indexOfPrefetchForward, loadingItem) + } + previousModelsSize = models.size } fun update(viewState: RoomDetailViewState) { @@ -355,9 +381,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec return shouldAdd } - /** - * Return true if added - */ private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ { return onVisibilityStateChanged { _, _, visibilityState -> if (visibilityState == VisibilityState.VISIBLE) { diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 0336e2d03c..d6ab24bfbd 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -33,6 +33,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequest +import im.vector.app.core.glide.GlideRequests import im.vector.app.core.ui.model.Size import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.isLocalFile @@ -206,12 +207,14 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: .into(imageView) } - private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest { + fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest { + return createGlideRequest(data, mode, GlideApp.with(imageView), size) + } + + fun createGlideRequest(data: Data, mode: Mode, glideRequests: GlideRequests, size: Size = processSize(data, mode)): GlideRequest { return if (data.elementToDecrypt != null) { // Encrypted image - GlideApp - .with(imageView) - .load(data) + glideRequests.load(data) } else { // Clear image val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() @@ -223,15 +226,12 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder: // Fallback to base url ?: data.url.takeIf { it?.startsWith("content://") == true } - GlideApp - .with(imageView) + glideRequests .load(resolvedUrl) .apply { if (mode == Mode.THUMBNAIL) { error( - GlideApp - .with(imageView) - .load(resolveUrl(data)) + glideRequests.load(resolveUrl(data)) ) } }