Adds correct footer behaviour to account lists and unifies it with how timelines use them.

This commit is contained in:
Vavassor 2017-06-30 18:30:25 -04:00
parent 3955649b9c
commit 275cd51a6d
15 changed files with 164 additions and 124 deletions

View file

@ -21,7 +21,6 @@ import android.support.v7.widget.RecyclerView;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@ -29,6 +28,8 @@ import java.util.List;
public abstract class AccountAdapter extends RecyclerView.Adapter {
List<Account> accountList;
AccountActionListener accountActionListener;
FooterViewHolder.State footerState;
private String topId;
private String bottomId;
@ -36,6 +37,7 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
super();
accountList = new ArrayList<>();
this.accountActionListener = accountActionListener;
footerState = FooterViewHolder.State.END;
}
@Override
@ -119,6 +121,10 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
return null;
}
public void setFooterState(FooterViewHolder.State newFooterState) {
footerState = newFooterState;
}
@Nullable
public String getBottomId() {
return bottomId;

View file

@ -59,6 +59,9 @@ public class BlocksAdapter extends AccountAdapter {
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener, true);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
}
}

View file

@ -55,6 +55,9 @@ public class FollowAdapter extends AccountAdapter {
AccountViewHolder holder = (AccountViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
}
}

View file

@ -59,6 +59,9 @@ public class FollowRequestsAdapter extends AccountAdapter {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
}
}

View file

@ -15,18 +15,59 @@
package com.keylesspalace.tusky.adapter;
import android.graphics.drawable.Drawable;
import android.support.v7.content.res.AppCompatResources;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.keylesspalace.tusky.R;
class FooterViewHolder extends RecyclerView.ViewHolder {
public class FooterViewHolder extends RecyclerView.ViewHolder {
public enum State {
EMPTY,
END,
LOADING
}
private View container;
private ProgressBar progressBar;
private TextView endMessage;
FooterViewHolder(View itemView) {
super(itemView);
ProgressBar progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar);
if (progressBar != null) {
progressBar.setIndeterminate(true);
container = itemView.findViewById(R.id.footer_container);
progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar);
endMessage = (TextView) itemView.findViewById(R.id.footer_end_message);
Drawable top = AppCompatResources.getDrawable(itemView.getContext(),
R.drawable.elephant_friend);
if (top != null) {
top.setBounds(0, 0, top.getIntrinsicWidth() / 2, top.getIntrinsicHeight() / 2);
}
endMessage.setCompoundDrawables(null, top, null, null);
}
public void setState(State state) {
switch (state) {
case LOADING: {
container.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.VISIBLE);
endMessage.setVisibility(View.GONE);
break;
}
case END: {
container.setVisibility(View.GONE);
progressBar.setVisibility(View.GONE);
endMessage.setVisibility(View.GONE);
break;
}
case EMPTY: {
container.setVisibility(View.VISIBLE);
progressBar.setVisibility(View.GONE);
endMessage.setVisibility(View.VISIBLE);
break;
}
}
}
}

View file

@ -44,6 +44,9 @@ public class MutesAdapter extends AccountAdapter {
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
holder.setupActionListener(accountActionListener, true, position);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
}
}

View file

@ -46,16 +46,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
private static final int VIEW_TYPE_FOLLOW = 3;
public enum FooterState {
EMPTY,
END,
LOADING
}
private List<Notification> notifications;
private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener;
private FooterState footerState = FooterState.END;
private FooterViewHolder.State footerState;
private boolean mediaPreviewEnabled;
private String bottomId;
private String topId;
@ -66,6 +60,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
notifications = new ArrayList<>();
this.statusListener = statusListener;
this.notificationActionListener = notificationActionListener;
footerState = FooterViewHolder.State.END;
mediaPreviewEnabled = true;
}
@ -79,24 +74,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
return new StatusViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view;
switch (footerState) {
default:
case LOADING:
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
break;
case END: {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer_end, parent, false);
break;
}
case EMPTY: {
view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer_empty, parent, false);
break;
}
}
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_footer, parent, false);
return new FooterViewHolder(view);
}
case VIEW_TYPE_STATUS_NOTIFICATION: {
@ -140,6 +119,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
break;
}
}
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
}
}
@ -252,12 +234,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter implements Adapte
notifyDataSetChanged();
}
public void setFooterState(FooterState newFooterState) {
FooterState oldValue = footerState;
public void setFooterState(FooterViewHolder.State newFooterState) {
footerState = newFooterState;
if (footerState != oldValue) {
notifyItemChanged(notifications.size());
}
}
@Nullable

View file

@ -34,15 +34,9 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_FOOTER = 1;
public enum FooterState {
EMPTY,
END,
LOADING
}
private List<Status> statuses;
private StatusActionListener statusListener;
private FooterState footerState = FooterState.END;
private FooterViewHolder.State footerState;
private boolean mediaPreviewEnabled;
private String topId;
private String bottomId;
@ -51,6 +45,7 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
super();
statuses = new ArrayList<>();
this.statusListener = statusListener;
footerState = FooterViewHolder.State.END;
mediaPreviewEnabled = true;
}
@ -64,24 +59,8 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
return new StatusViewHolder(view);
}
case VIEW_TYPE_FOOTER: {
View view;
switch (footerState) {
default:
case LOADING:
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer, viewGroup, false);
break;
case END: {
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer_end, viewGroup, false);
break;
}
case EMPTY: {
view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer_empty, viewGroup, false);
break;
}
}
View view = LayoutInflater.from(viewGroup.getContext())
.inflate(R.layout.item_footer, viewGroup, false);
return new FooterViewHolder(view);
}
}
@ -93,6 +72,9 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
StatusViewHolder holder = (StatusViewHolder) viewHolder;
Status status = statuses.get(position);
holder.setupWithStatus(status, statusListener, mediaPreviewEnabled);
} else {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(footerState);
}
}
@ -192,12 +174,8 @@ public class TimelineAdapter extends RecyclerView.Adapter implements AdapterItem
return null;
}
public void setFooterState(FooterState newFooterState) {
FooterState oldValue = footerState;
public void setFooterState(FooterViewHolder.State newFooterState) {
footerState = newFooterState;
if (footerState != oldValue) {
notifyItemChanged(statuses.size());
}
}
public void setMediaPreviewEnabled(boolean enabled) {

View file

@ -36,6 +36,7 @@ import com.keylesspalace.tusky.adapter.AccountAdapter;
import com.keylesspalace.tusky.adapter.BlocksAdapter;
import com.keylesspalace.tusky.adapter.FollowAdapter;
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter;
import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.MutesAdapter;
import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.entity.Account;
@ -358,7 +359,12 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
}
private void onRespondToFollowRequestFailure(boolean accept, String accountId) {
String verb = (accept) ? "accept" : "reject";
String verb;
if (accept) {
verb = "accept";
} else {
verb = "reject";
}
String message = String.format("Failed to %s account id %s.", verb, accountId);
Log.e(TAG, message);
}
@ -399,6 +405,11 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
bottomFetches++;
return;
}
if (fromId != null || adapter.getItemCount() <= 1) {
setFooterState(FooterViewHolder.State.LOADING);
}
Callback<List<Account>> cb = new Callback<List<Account>>() {
@Override
public void onResponse(Call<List<Account>> call, Response<List<Account>> response) {
@ -456,6 +467,11 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
}
}
fulfillAnyQueuedFetches(fetchEnd);
if (accounts.size() == 0 && adapter.getItemCount() == 1) {
setFooterState(FooterViewHolder.State.EMPTY);
} else {
setFooterState(FooterViewHolder.State.END);
}
}
private void onFetchAccountsFailure(Exception exception, FetchEnd fetchEnd) {
@ -463,6 +479,20 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
fulfillAnyQueuedFetches(fetchEnd);
}
/* This needs to be called from the endless scroll listener, which does not allow notifying the
* adapter during the callback. So, this is the workaround. */
private void setFooterState(FooterViewHolder.State state) {
// Set the adapter to set its state when it's bound, if the current Footer is offscreen.
adapter.setFooterState(state);
// Check if it's onscreen, and update it directly if it is.
RecyclerView.ViewHolder viewHolder =
recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1);
if (viewHolder != null) {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(state);
}
}
private void onRefresh() {
fetchAccounts(null, adapter.getTopId(), FetchEnd.TOP);
}
@ -478,7 +508,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
bottomLoading = false;
if (bottomFetches > 0) {
bottomFetches--;
Log.d(TAG, "extra fetchos " + bottomFetches);
onLoadMore(recyclerView);
}
break;

View file

@ -34,6 +34,7 @@ import android.view.View;
import android.view.ViewGroup;
import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.NotificationsAdapter;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Notification;
@ -274,7 +275,7 @@ public class NotificationsFragment extends SFragment implements
}
if (fromId != null || adapter.getItemCount() <= 1) {
adapter.setFooterState(NotificationsAdapter.FooterState.LOADING);
setFooterState(FooterViewHolder.State.LOADING);
}
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, null);
@ -341,9 +342,9 @@ public class NotificationsFragment extends SFragment implements
}
fulfillAnyQueuedFetches(fetchEnd);
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
adapter.setFooterState(NotificationsAdapter.FooterState.EMPTY);
setFooterState(FooterViewHolder.State.EMPTY);
} else {
adapter.setFooterState(NotificationsAdapter.FooterState.END);
setFooterState(FooterViewHolder.State.END);
}
swipeRefreshLayout.setRefreshing(false);
}
@ -354,6 +355,20 @@ public class NotificationsFragment extends SFragment implements
fulfillAnyQueuedFetches(fetchEnd);
}
/* This needs to be called from the endless scroll listener, which does not allow notifying the
* adapter during the callback. So, this is the workaround. */
private void setFooterState(FooterViewHolder.State state) {
// Set the adapter to set its state when it's bound, if the current Footer is offscreen.
adapter.setFooterState(state);
// Check if it's onscreen, and update it directly if it is.
RecyclerView.ViewHolder viewHolder =
recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1);
if (viewHolder != null) {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(state);
}
}
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
switch (fetchEnd) {
case BOTTOM: {

View file

@ -35,6 +35,7 @@ import android.view.ViewGroup;
import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.FooterViewHolder;
import com.keylesspalace.tusky.adapter.TimelineAdapter;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
@ -359,7 +360,7 @@ public class TimelineFragment extends SFragment implements
}
if (fromId != null || adapter.getItemCount() <= 1) {
adapter.setFooterState(TimelineAdapter.FooterState.LOADING);
setFooterState(FooterViewHolder.State.LOADING);
}
Callback<List<Status>> callback = new Callback<List<Status>>() {
@ -422,9 +423,9 @@ public class TimelineFragment extends SFragment implements
}
fulfillAnyQueuedFetches(fetchEnd);
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
adapter.setFooterState(TimelineAdapter.FooterState.EMPTY);
setFooterState(FooterViewHolder.State.EMPTY);
} else {
adapter.setFooterState(TimelineAdapter.FooterState.END);
setFooterState(FooterViewHolder.State.END);
}
swipeRefreshLayout.setRefreshing(false);
}
@ -435,6 +436,20 @@ public class TimelineFragment extends SFragment implements
fulfillAnyQueuedFetches(fetchEnd);
}
/* This needs to be called from the endless scroll listener, which does not allow notifying the
* adapter during the callback. So, this is the workaround. */
private void setFooterState(FooterViewHolder.State state) {
// Set the adapter to set its state when it's bound, if the current Footer is offscreen.
adapter.setFooterState(state);
// Check if it's onscreen, and update it directly if it is.
RecyclerView.ViewHolder viewHolder =
recyclerView.findViewHolderForAdapterPosition(adapter.getItemCount() - 1);
if (viewHolder != null) {
FooterViewHolder holder = (FooterViewHolder) viewHolder;
holder.setState(state);
}
}
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
switch (fetchEnd) {
case BOTTOM: {

View file

@ -1,20 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
android:id="@+id/footer_container">
<LinearLayout
<ProgressBar
android:id="@+id/footer_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center">
<ProgressBar
android:id="@+id/footer_progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
android:layout_centerInParent="true"
android:indeterminate="true" />
</LinearLayout>
<TextView
android:id="@+id/footer_end_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/footer_empty"
android:textAlignment="center"
android:layout_centerInParent="true"
android:drawablePadding="16dp" />
</RelativeLayout>

View file

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:srcCompat="@drawable/elephant_friend" />
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/footer_empty"
android:textAlignment="center" />
</LinearLayout>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
</LinearLayout>

View file

@ -40,10 +40,7 @@
<string name="status_content_warning_show_more">Show More</string>
<string name="status_content_warning_show_less">Show Less</string>
<string name="footer_end_of_statuses">end of the statuses</string>
<string name="footer_end_of_notifications">end of the notifications</string>
<string name="footer_end_of_accounts">end of the accounts</string>
<string name="footer_empty">There are no toots here so far. Pull down to refresh!</string>
<string name="footer_empty">Nothing here. Pull down to refresh!</string>
<string name="notification_reblog_format">%s boosted your toot</string>
<string name="notification_favourite_format">%s favourited your toot</string>