mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
When using the database as `PagingSource`, it can happen that there are null items, e.g. when the user jumps to the top and the items are no longer in memory and need to be re-queried. We have ignored this fact until now, leading to subtle bugs where the adapter just shows a completely empty `ViewHolder`, or worse, a recycled ViewHolder that has not been updated and shows the wrong post. Usually these are only visible for a split second but it can take longer in some cases e.g. on slow devices. Here is how the placeholders look: <img src="https://github.com/user-attachments/assets/58d3434f-916f-44a5-ad82-2a4a759e39d8" width="320"/> Note: I would prefer to turn this behavior with the null items off, but we tried that once and it led to even worse bugs: https://github.com/tuskyapp/Tusky/pull/4471pull/5086/head
34 changed files with 364 additions and 123 deletions
@ -0,0 +1,46 @@
|
||||
/* Copyright 2021 Tusky Contributors |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||
* License, or (at your option) any later version. |
||||
* |
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||
* Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||
* see <http://www.gnu.org/licenses>. */ |
||||
package com.keylesspalace.tusky.adapter |
||||
|
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import com.keylesspalace.tusky.databinding.ItemLoadMoreBinding |
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener |
||||
import com.keylesspalace.tusky.util.hide |
||||
import com.keylesspalace.tusky.util.visible |
||||
|
||||
/** |
||||
* Placeholder for missing parts in timelines. |
||||
* |
||||
* Displays a "Load more" button to load the gap, or a |
||||
* circular progress bar if the missing page is being loaded. |
||||
*/ |
||||
class LoadMoreViewHolder( |
||||
private val binding: ItemLoadMoreBinding, |
||||
listener: StatusActionListener |
||||
) : RecyclerView.ViewHolder(binding.root) { |
||||
|
||||
init { |
||||
binding.loadMoreButton.setOnClickListener { |
||||
binding.loadMoreButton.hide() |
||||
binding.loadMoreProgressBar.show() |
||||
listener.onLoadMore(bindingAdapterPosition) |
||||
} |
||||
} |
||||
|
||||
fun setup(loading: Boolean) { |
||||
binding.loadMoreButton.visible(!loading) |
||||
binding.loadMoreProgressBar.visible(loading) |
||||
} |
||||
} |
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<solid android:color="?attr/placeholderColor" /> |
||||
<corners android:radius="6dp"/> |
||||
</shape> |
||||
@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto" |
||||
xmlns:tools="http://schemas.android.com/tools" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
||||
android:importantForAccessibility="noHideDescendants"> |
||||
|
||||
<TextView |
||||
android:id="@+id/topPlaceholder" |
||||
android:layout_width="200dp" |
||||
android:layout_height="wrap_content" |
||||
android:layout_marginStart="14dp" |
||||
android:layout_marginTop="14dp" |
||||
android:layout_marginEnd="14dp" |
||||
android:background="@drawable/text_placeholder" |
||||
android:lines="1" |
||||
android:textColor="?android:textColorPrimary" |
||||
android:textSize="?attr/status_text_medium" |
||||
app:layout_constraintStart_toStartOf="parent" |
||||
app:layout_constraintTop_toTopOf="parent" /> |
||||
|
||||
<ImageView |
||||
android:id="@+id/avatarPlaceholder" |
||||
android:layout_width="@dimen/timeline_status_avatar_width" |
||||
android:layout_height="@dimen/timeline_status_avatar_height" |
||||
android:layout_marginStart="14dp" |
||||
android:layout_marginTop="@dimen/account_avatar_margin" |
||||
android:importantForAccessibility="no" |
||||
android:src="@drawable/text_placeholder" |
||||
app:layout_constraintStart_toStartOf="parent" |
||||
app:layout_constraintTop_toBottomOf="@id/topPlaceholder" /> |
||||
|
||||
<TextView |
||||
android:id="@+id/namePlaceholder" |
||||
android:layout_width="0dp" |
||||
android:layout_height="wrap_content" |
||||
android:layout_marginStart="14dp" |
||||
android:layout_marginTop="14dp" |
||||
android:layout_marginEnd="14dp" |
||||
android:background="@drawable/text_placeholder" |
||||
android:lines="1" |
||||
android:textColor="?android:textColorPrimary" |
||||
android:textSize="?attr/status_text_medium" |
||||
app:layout_constrainedWidth="true" |
||||
app:layout_constraintEnd_toEndOf="parent" |
||||
app:layout_constraintStart_toEndOf="@id/avatarPlaceholder" |
||||
app:layout_constraintTop_toBottomOf="@id/topPlaceholder" /> |
||||
|
||||
<TextView |
||||
android:id="@+id/contentPlaceholder" |
||||
android:layout_width="0dp" |
||||
android:layout_height="wrap_content" |
||||
android:layout_marginStart="14dp" |
||||
android:layout_marginTop="14dp" |
||||
android:layout_marginEnd="14dp" |
||||
android:background="@drawable/text_placeholder" |
||||
android:lines="3" |
||||
android:textColor="?android:textColorPrimary" |
||||
android:textSize="?attr/status_text_medium" |
||||
app:layout_constraintEnd_toEndOf="parent" |
||||
app:layout_constraintStart_toEndOf="@id/avatarPlaceholder" |
||||
app:layout_constraintTop_toBottomOf="@id/namePlaceholder" /> |
||||
|
||||
|
||||
<ImageButton |
||||
android:id="@+id/replyButtonPlaceholder" |
||||
style="@style/TuskyImageButton" |
||||
android:layout_width="52dp" |
||||
android:layout_height="48dp" |
||||
android:layout_marginStart="-14dp" |
||||
android:layout_marginTop="2dp" |
||||
android:clickable="false" |
||||
android:importantForAccessibility="no" |
||||
app:layout_constraintBottom_toBottomOf="parent" |
||||
app:layout_constraintEnd_toStartOf="@id/reblogButtonPlaceholder" |
||||
app:layout_constraintHorizontal_chainStyle="spread_inside" |
||||
app:layout_constraintStart_toStartOf="@id/namePlaceholder" |
||||
app:layout_constraintTop_toBottomOf="@id/contentPlaceholder" |
||||
app:srcCompat="@drawable/ic_reply_24dp" |
||||
app:tint="?attr/placeholderColor" |
||||
tools:ignore="NegativeMargin" /> |
||||
|
||||
<ImageButton |
||||
android:id="@+id/reblogButtonPlaceholder" |
||||
style="@style/TuskyImageButton" |
||||
android:layout_width="52dp" |
||||
android:layout_height="48dp" |
||||
android:clickable="false" |
||||
android:importantForAccessibility="no" |
||||
app:layout_constraintEnd_toStartOf="@id/favouriteButtonPlaceholder" |
||||
app:layout_constraintStart_toEndOf="@id/replyButtonPlaceholder" |
||||
app:layout_constraintTop_toTopOf="@id/replyButtonPlaceholder" |
||||
app:srcCompat="@drawable/ic_repeat_24dp" |
||||
app:tint="?attr/placeholderColor" /> |
||||
|
||||
<ImageButton |
||||
android:id="@+id/favouriteButtonPlaceholder" |
||||
style="@style/TuskyImageButton" |
||||
android:layout_width="52dp" |
||||
android:layout_height="48dp" |
||||
android:clickable="false" |
||||
android:importantForAccessibility="no" |
||||
app:layout_constraintEnd_toStartOf="@id/status_bookmark" |
||||
app:layout_constraintStart_toEndOf="@id/reblogButtonPlaceholder" |
||||
app:layout_constraintTop_toTopOf="@id/reblogButtonPlaceholder" |
||||
app:srcCompat="@drawable/ic_star_24dp" |
||||
app:tint="?attr/placeholderColor" /> |
||||
|
||||
<ImageButton |
||||
android:id="@+id/status_bookmark" |
||||
style="@style/TuskyImageButton" |
||||
android:layout_width="52dp" |
||||
android:layout_height="48dp" |
||||
android:clickable="false" |
||||
android:clipToPadding="false" |
||||
android:importantForAccessibility="no" |
||||
app:layout_constraintEnd_toStartOf="@id/moreButtonPlaceHolder" |
||||
app:layout_constraintStart_toEndOf="@id/favouriteButtonPlaceholder" |
||||
app:layout_constraintTop_toTopOf="@id/replyButtonPlaceholder" |
||||
app:srcCompat="@drawable/ic_bookmark_24dp" |
||||
app:tint="?attr/placeholderColor" |
||||
sparkbutton:activeImage="@drawable/ic_bookmark_24dp_filled" /> |
||||
|
||||
<ImageButton |
||||
android:id="@+id/moreButtonPlaceHolder" |
||||
style="@style/TuskyImageButton" |
||||
android:layout_width="52dp" |
||||
android:layout_height="48dp" |
||||
android:clickable="false" |
||||
android:importantForAccessibility="no" |
||||
app:layout_constraintBottom_toBottomOf="@id/replyButtonPlaceholder" |
||||
app:layout_constraintEnd_toEndOf="parent" |
||||
app:layout_constraintStart_toEndOf="@id/status_bookmark" |
||||
app:layout_constraintTop_toTopOf="@id/replyButtonPlaceholder" |
||||
app:srcCompat="@drawable/ic_more_horiz_24dp" |
||||
app:tint="?attr/placeholderColor" /> |
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout> |
||||
Loading…
Reference in new issue