diff --git a/CHANGES.md b/CHANGES.md index bb132982d8..3087b7405c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ Improvements 🙌: - Improve devices list screen - Add settings for rageshake sensibility - Fix autocompletion issues and add support for rooms, groups, and emoji (#780) + - Show skip to bottom FAB while scrolling down (#752) Other changes: - Change the way RiotX identifies a session to allow the SDK to support several sessions with the same user (#800) diff --git a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt index b02e3c9366..627d757574 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/Debouncer.kt @@ -19,16 +19,14 @@ package im.vector.riotx.core.utils import android.os.Handler -internal class Debouncer(private val handler: Handler) { +class Debouncer(private val handler: Handler) { private val runnables = HashMap() fun debounce(identifier: String, millis: Long, r: Runnable): Boolean { - if (runnables.containsKey(identifier)) { - // debounce - val old = runnables[identifier] - handler.removeCallbacks(old) - } + // debounce + cancel(identifier) + insertRunnable(identifier, r, millis) return true } @@ -37,6 +35,14 @@ internal class Debouncer(private val handler: Handler) { handler.removeCallbacksAndMessages(null) } + fun cancel(identifier: String) { + if (runnables.containsKey(identifier)) { + val old = runnables[identifier] + handler.removeCallbacks(old) + runnables.remove(identifier) + } + } + private fun insertRunnable(identifier: String, r: Runnable, millis: Long) { val chained = Runnable { handler.post(r) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt new file mode 100644 index 0000000000..4be5502678 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/JumpToBottomViewVisibilityManager.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.home.room.detail + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.FloatingActionButton +import im.vector.riotx.core.utils.Debouncer +import timber.log.Timber + +/** + * Show or hide the jumpToBottomView, depending on the scrolling and if the timeline is displaying the more recent event + * - When user scrolls up (i.e. going to the past): hide + * - When user scrolls down: show if not displaying last event + * - When user stops scrolling: show if not displaying last event + */ +class JumpToBottomViewVisibilityManager( + private val jumpToBottomView: FloatingActionButton, + private val debouncer: Debouncer, + recyclerView: RecyclerView, + private val layoutManager: LinearLayoutManager) { + + init { + recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + debouncer.cancel("jump_to_bottom_visibility") + + val scrollingToPast = dy < 0 + + if (scrollingToPast) { + jumpToBottomView.hide() + } else { + maybeShowJumpToBottomViewVisibility() + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + when (newState) { + RecyclerView.SCROLL_STATE_IDLE -> { + maybeShowJumpToBottomViewVisibilityWithDelay() + } + RecyclerView.SCROLL_STATE_DRAGGING, + RecyclerView.SCROLL_STATE_SETTLING -> Unit + } + } + }) + } + + fun maybeShowJumpToBottomViewVisibilityWithDelay() { + debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { + maybeShowJumpToBottomViewVisibility() + }) + } + + private fun maybeShowJumpToBottomViewVisibility() { + Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") + if (layoutManager.findFirstVisibleItemPosition() != 0) { + jumpToBottomView.show() + } else { + jumpToBottomView.hide() + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 4414c48205..a956d0e2e9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -181,6 +181,7 @@ class RoomDetailFragment @Inject constructor( private lateinit var sharedActionViewModel: MessageSharedActionViewModel private lateinit var layoutManager: LinearLayoutManager + private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager private var modelBuildListener: OnModelBuildFinishedListener? = null private lateinit var attachmentsHelper: AttachmentsHelper @@ -312,6 +313,13 @@ class RoomDetailFragment @Inject constructor( } } } + + jumpToBottomViewVisibilityManager = JumpToBottomViewVisibilityManager( + jumpToBottomView, + debouncer, + recyclerView, + layoutManager + ) } private fun setupJumpToReadMarkerView() { @@ -474,25 +482,11 @@ class RoomDetailFragment @Inject constructor( it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnHighlightedEventCallback) updateJumpToReadMarkerViewVisibility() - updateJumpToBottomViewVisibility() + jumpToBottomViewVisibilityManager.maybeShowJumpToBottomViewVisibilityWithDelay() } timelineEventController.addModelBuildListener(modelBuildListener) recyclerView.adapter = timelineEventController.adapter - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - when (newState) { - RecyclerView.SCROLL_STATE_IDLE -> { - updateJumpToBottomViewVisibility() - } - RecyclerView.SCROLL_STATE_DRAGGING, - RecyclerView.SCROLL_STATE_SETTLING -> { - jumpToBottomView.hide() - } - } - } - }) - timelineEventController.callback = this if (vectorPreferences.swipeToReplyIsEnabled()) { @@ -547,17 +541,6 @@ class RoomDetailFragment @Inject constructor( } } - private fun updateJumpToBottomViewVisibility() { - debouncer.debounce("jump_to_bottom_visibility", 250, Runnable { - Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}") - if (layoutManager.findFirstVisibleItemPosition() != 0) { - jumpToBottomView.show() - } else { - jumpToBottomView.hide() - } - }) - } - private fun setupComposer() { autoCompleter.setup(composerLayout.composerEditText, this)