From 275cd51a6db2a1f119ff01e3091603fda32860db Mon Sep 17 00:00:00 2001 From: Vavassor Date: Fri, 30 Jun 2017 18:30:25 -0400 Subject: [PATCH] Adds correct footer behaviour to account lists and unifies it with how timelines use them. --- .../tusky/adapter/AccountAdapter.java | 8 ++- .../tusky/adapter/BlocksAdapter.java | 3 ++ .../tusky/adapter/FollowAdapter.java | 3 ++ .../tusky/adapter/FollowRequestsAdapter.java | 3 ++ .../tusky/adapter/FooterViewHolder.java | 49 +++++++++++++++++-- .../tusky/adapter/MutesAdapter.java | 3 ++ .../tusky/adapter/NotificationsAdapter.java | 38 +++----------- .../tusky/adapter/TimelineAdapter.java | 38 +++----------- .../tusky/fragment/AccountListFragment.java | 33 ++++++++++++- .../tusky/fragment/NotificationsFragment.java | 21 ++++++-- .../tusky/fragment/TimelineFragment.java | 21 ++++++-- app/src/main/res/layout/item_footer.xml | 29 ++++++----- app/src/main/res/layout/item_footer_empty.xml | 25 ---------- app/src/main/res/layout/item_footer_end.xml | 9 ---- app/src/main/res/values/strings.xml | 5 +- 15 files changed, 164 insertions(+), 124 deletions(-) delete mode 100644 app/src/main/res/layout/item_footer_empty.xml delete mode 100644 app/src/main/res/layout/item_footer_end.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java index 8c448232b..a46f5075e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java @@ -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 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; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java index 91a718ec8..f4e70fa7c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java @@ -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); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java index 7df84c6cc..c494dddef 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java @@ -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); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index 269ac3376..c4578e520 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -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); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FooterViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FooterViewHolder.java index 5ff1187e7..baa68d535 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FooterViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FooterViewHolder.java @@ -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; + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java index b1499c9e0..e7719775f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -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); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 36309fc11..10fe9f0ab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -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 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 diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index 54854bf68..aaabf0b68 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -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 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) { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java index 0b8fa1929..075026b42 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.java @@ -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> cb = new Callback>() { @Override public void onResponse(Call> call, Response> 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; diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 2161260e7..133ad5cad 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -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> 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: { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 5e72f470e..759780c26 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -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> callback = new Callback>() { @@ -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: { diff --git a/app/src/main/res/layout/item_footer.xml b/app/src/main/res/layout/item_footer.xml index 69204b57d..a1aa501e6 100644 --- a/app/src/main/res/layout/item_footer.xml +++ b/app/src/main/res/layout/item_footer.xml @@ -1,20 +1,23 @@ - + android:id="@+id/footer_container"> - - - + android:layout_centerInParent="true" + android:indeterminate="true" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_footer_empty.xml b/app/src/main/res/layout/item_footer_empty.xml deleted file mode 100644 index 1c5606d22..000000000 --- a/app/src/main/res/layout/item_footer_empty.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_footer_end.xml b/app/src/main/res/layout/item_footer_end.xml deleted file mode 100644 index 584de324b..000000000 --- a/app/src/main/res/layout/item_footer_end.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3baf057a..fe95f74cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,10 +40,7 @@ Show More Show Less - end of the statuses - end of the notifications - end of the accounts - There are no toots here so far. Pull down to refresh! + Nothing here. Pull down to refresh! %s boosted your toot %s favourited your toot