From f751b98a586ac8adc75d927e327d8117b322bb5a Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sat, 17 Feb 2018 12:51:22 +0100
Subject: [PATCH] =?UTF-8?q?Improved=20message=20view=20-=20fixed=20scroll?=
 =?UTF-8?q?=20behaviour=20-=20use=20Paging=E2=80=99s=20BoundaryCallback=20?=
 =?UTF-8?q?for=20loadMore=20detection?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 app/build.gradle.kts                          |  6 +-
 .../persistence/AccountDatabase.kt            |  4 +-
 .../persistence/QuasselDatabase.kt            |  6 +-
 .../quasseldroid_ng/ui/chat/MessageAdapter.kt |  4 -
 .../ui/chat/MessageListFragment.kt            | 94 +++++++++++--------
 .../ui/setup/accounts/AccountViewModel.kt     | 10 +-
 app/src/main/res/layout/fragment_messages.xml |  1 +
 7 files changed, 68 insertions(+), 57 deletions(-)

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7f0c3f784..c96cc3e24 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -106,9 +106,9 @@ dependencies {
   implementation(appArch("lifecycle", "reactivestreams"))
   kapt(appArch("lifecycle", "compiler"))
 
-  implementation(appArch("persistence.room", "runtime", "1.0.0"))
-  implementation(appArch("persistence.room", "rxjava2", "1.0.0"))
-  kapt(appArch("persistence.room", "compiler", "1.0.0"))
+  implementation(appArch("persistence.room", "runtime", "1.1.0-alpha2"))
+  implementation(appArch("persistence.room", "rxjava2", "1.1.0-alpha2"))
+  kapt(appArch("persistence.room", "compiler", "1.1.0-alpha2"))
 
   implementation(appArch("paging", "runtime", version = "1.0.0-alpha5")) {
     exclude(group = "junit", module = "junit")
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/persistence/AccountDatabase.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/persistence/AccountDatabase.kt
index d5429e045..ebe1e6100 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/persistence/AccountDatabase.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/persistence/AccountDatabase.kt
@@ -1,6 +1,6 @@
 package de.kuschku.quasseldroid_ng.persistence
 
-import android.arch.paging.LivePagedListProvider
+import android.arch.paging.DataSource
 import android.arch.persistence.room.*
 import android.content.Context
 
@@ -32,7 +32,7 @@ abstract class AccountDatabase : RoomDatabase() {
     fun findById(id: Long): AccountDatabase.Account?
 
     @Query("SELECT * FROM account ORDER BY lastUsed DESC")
-    fun all(): LivePagedListProvider<Int, Account>
+    fun all(): DataSource.Factory<Int, Account>
 
     @Delete
     fun delete(account: AccountDatabase.Account)
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/persistence/QuasselDatabase.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/persistence/QuasselDatabase.kt
index 714246e92..3f91598da 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/persistence/QuasselDatabase.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/persistence/QuasselDatabase.kt
@@ -1,6 +1,6 @@
 package de.kuschku.quasseldroid_ng.persistence
 
-import android.arch.paging.LivePagedListProvider
+import android.arch.paging.DataSource
 import android.arch.persistence.room.*
 import android.content.Context
 import android.support.annotation.IntRange
@@ -60,8 +60,8 @@ abstract class QuasselDatabase : RoomDatabase() {
     @Query("SELECT * FROM message WHERE bufferId = :bufferId ORDER BY messageId ASC")
     fun findByBufferId(bufferId: Int): List<DatabaseMessage>
 
-    @Query("SELECT * FROM message WHERE bufferId = :bufferId ORDER BY messageId ASC")
-    fun findByBufferIdPaged(bufferId: Int): LivePagedListProvider<Int, DatabaseMessage>
+    @Query("SELECT * FROM message WHERE bufferId = :bufferId ORDER BY messageId DESC")
+    fun findByBufferIdPaged(bufferId: Int): DataSource.Factory<Int, DatabaseMessage>
 
     @Query("SELECT * FROM message WHERE bufferId = :bufferId ORDER BY messageId DESC LIMIT 1")
     fun findLastByBufferId(bufferId: Int): DatabaseMessage?
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageAdapter.kt
index 28720cc90..85428e267 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageAdapter.kt
@@ -21,10 +21,6 @@ class MessageAdapter(context: Context) :
 
   private val messageCache = LruCache<Int, FormattedMessage>(512)
 
-  init {
-    setHasStableIds(true)
-  }
-
   override fun onBindViewHolder(holder: QuasselMessageViewHolder, position: Int) {
     getItem(position)?.let {
       messageRenderer.bind(
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageListFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageListFragment.kt
index 1150ac143..a71fe9000 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageListFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/MessageListFragment.kt
@@ -3,6 +3,7 @@ package de.kuschku.quasseldroid_ng.ui.chat
 import android.arch.lifecycle.LiveData
 import android.arch.lifecycle.MutableLiveData
 import android.arch.lifecycle.Observer
+import android.arch.paging.LivePagedListBuilder
 import android.arch.paging.PagedList
 import android.os.Bundle
 import android.support.design.widget.FloatingActionButton
@@ -14,13 +15,10 @@ import android.view.ViewGroup
 import butterknife.BindView
 import butterknife.ButterKnife
 import de.kuschku.libquassel.protocol.BufferId
-import de.kuschku.libquassel.session.Backend
-import de.kuschku.libquassel.session.SessionManager
 import de.kuschku.quasseldroid_ng.R
 import de.kuschku.quasseldroid_ng.persistence.QuasselDatabase
 import de.kuschku.quasseldroid_ng.util.AndroidHandlerThread
 import de.kuschku.quasseldroid_ng.util.helper.invoke
-import de.kuschku.quasseldroid_ng.util.helper.map
 import de.kuschku.quasseldroid_ng.util.helper.switchMap
 import de.kuschku.quasseldroid_ng.util.helper.toggle
 import de.kuschku.quasseldroid_ng.util.service.ServiceBoundFragment
@@ -39,37 +37,70 @@ class MessageListFragment : ServiceBoundFragment() {
   @BindView(R.id.scrollDown)
   lateinit var scrollDown: FloatingActionButton
 
-  private val sessionManager: LiveData<SessionManager?> = backend.map(Backend::sessionManager)
-
   override fun onCreate(savedInstanceState: Bundle?) {
     handler.onCreate()
     super.onCreate(savedInstanceState)
     setHasOptionsMenu(true)
   }
 
+  private val boundaryCallback = object :
+    PagedList.BoundaryCallback<QuasselDatabase.DatabaseMessage>() {
+    override fun onItemAtFrontLoaded(itemAtFront: QuasselDatabase.DatabaseMessage)
+      = Unit
+
+    override fun onItemAtEndLoaded(itemAtEnd: QuasselDatabase.DatabaseMessage)
+      = loadMore()
+  }
+
   override fun onCreateView(inflater: LayoutInflater,
                             container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {
     val view = inflater.inflate(R.layout.fragment_messages, container, false)
     ButterKnife.bind(this, view)
 
+    val adapter = MessageAdapter(context!!)
+
+    messageList.adapter = adapter
+    val linearLayoutManager = LinearLayoutManager(context)
+    linearLayoutManager.reverseLayout = true
+    messageList.layoutManager = linearLayoutManager
+
+    messageList.addOnScrollListener(
+      object : RecyclerView.OnScrollListener() {
+        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
+          val canScrollDown = recyclerView.canScrollVertically(1)
+          val isScrollingDown = dy > 0
+
+          scrollDown.visibility = View.VISIBLE
+          scrollDown.toggle(canScrollDown && isScrollingDown)
+        }
+      }
+    )
+
     database = QuasselDatabase.Creator.init(context!!.applicationContext)
     val data = buffer.switchMap {
-      database.message().findByBufferIdPaged(it).create(
-        Int.MAX_VALUE,
-        PagedList.Config.Builder()
-          .setPageSize(50)
-          .setEnablePlaceholders(false)
-          .setPrefetchDistance(50)
-          .build()
-      )
+      LivePagedListBuilder(database.message().findByBufferIdPaged(it), 20)
+        .setBoundaryCallback(boundaryCallback)
+        .setInitialLoadKey(null)
+        .build()
     }
 
-    val adapter = MessageAdapter(context!!)
-
     data.observe(
       this, Observer { list ->
+      val findFirstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition()
       adapter.setList(list)
+      if (findFirstVisibleItemPosition < 2) {
+        activity?.runOnUiThread {
+          messageList.smoothScrollToPosition(0)
+        }
+        handler.postDelayed(
+          {
+            activity?.runOnUiThread {
+              messageList.smoothScrollToPosition(0)
+            }
+          }, 16
+        )
+      }
     }
     )
 
@@ -77,36 +108,23 @@ class MessageListFragment : ServiceBoundFragment() {
       this, Observer {
       handler.post {
         // Try loading messages when switching to isEmpty buffer
-        if (it != null && database.message().bufferSize(it) == 0) {
-          loadMore()
+        if (it != null) {
+          if (database.message().bufferSize(it) == 0) {
+            loadMore()
+          }
+          activity?.runOnUiThread {
+            messageList.scrollToPosition(0)
+          }
         }
       }
     }
     )
 
-    var recyclerViewMeasuredHeight = 0
-    val scrollDownListener = object : RecyclerView.OnScrollListener() {
-      override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
-        if (!recyclerView.canScrollVertically(-1)) {
-          loadMore()
-        }
-        if (recyclerViewMeasuredHeight == 0)
-          recyclerViewMeasuredHeight = recyclerView.measuredHeight
-        val canScrollDown = recyclerView.canScrollVertically(1)
-        val isScrollingDown = dy > 0
-        val scrollOffsetFromBottom = recyclerView.computeVerticalScrollRange() - recyclerView.computeVerticalScrollOffset() - recyclerViewMeasuredHeight
-        val isMoreThanOneScreenFromBottom = scrollOffsetFromBottom > recyclerViewMeasuredHeight
-        val smartVisibility = scrollDown.visibility == View.VISIBLE || isMoreThanOneScreenFromBottom
-        scrollDown.toggle(canScrollDown && isScrollingDown && smartVisibility)
-      }
+    scrollDown.hide()
+    scrollDown.setOnClickListener {
+      messageList.scrollToPosition(0)
     }
 
-    messageList.adapter = adapter
-    messageList.layoutManager = LinearLayoutManager(context)
-    messageList.addOnScrollListener(scrollDownListener)
-
-    scrollDown.setOnClickListener { messageList.scrollToPosition(adapter.itemCount) }
-
     return view
   }
 
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountViewModel.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountViewModel.kt
index cf2aca05e..d873980ca 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountViewModel.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/setup/accounts/AccountViewModel.kt
@@ -3,6 +3,7 @@ package de.kuschku.quasseldroid_ng.ui.setup.accounts
 import android.app.Application
 import android.arch.lifecycle.AndroidViewModel
 import android.arch.lifecycle.LiveData
+import android.arch.paging.LivePagedListBuilder
 import android.arch.paging.PagedList
 import de.kuschku.quasseldroid_ng.persistence.AccountDatabase
 
@@ -10,11 +11,6 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
   private val database: AccountDatabase = AccountDatabase.Creator.init(
     getApplication()
   )
-  val accounts: LiveData<PagedList<AccountDatabase.Account>> = database.accounts().all().create(
-    0,
-    PagedList.Config.Builder()
-      .setPageSize(50)
-      .setPrefetchDistance(50)
-      .build()
-  )
+  val accounts: LiveData<PagedList<AccountDatabase.Account>>
+    = LivePagedListBuilder(database.accounts().all(), 20).build()
 }
diff --git a/app/src/main/res/layout/fragment_messages.xml b/app/src/main/res/layout/fragment_messages.xml
index 2470d8d7d..e8323981d 100644
--- a/app/src/main/res/layout/fragment_messages.xml
+++ b/app/src/main/res/layout/fragment_messages.xml
@@ -24,6 +24,7 @@
     android:layout_marginEnd="12dp"
     android:layout_marginRight="12dp"
     android:tint="@color/colorFillDark"
+    android:visibility="gone"
     app:backgroundTint="#8A808080"
     app:elevation="0dip"
     app:fabSize="mini"
-- 
GitLab