Load timeline gaps depending on scroll direction (AND-190)
Closes #70, closes #154, closes #147, closes #281
This commit is contained in:
parent
57da77b642
commit
ad04433944
|
@ -73,6 +73,7 @@ import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
|
@ -274,11 +275,35 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||||
newPostsBtnWrap.setOnHideButtonListener(this::hideNewPostsButton);
|
newPostsBtnWrap.setOnHideButtonListener(this::hideNewPostsButton);
|
||||||
updateToolbarLogo();
|
updateToolbarLogo();
|
||||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||||
|
private HashSet<GapStatusDisplayItem> gaps=new HashSet<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||||
if(newPostsBtnShown && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
|
if(newPostsBtnShown && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
|
||||||
hideNewPostsButton();
|
hideNewPostsButton();
|
||||||
}
|
}
|
||||||
|
for(StatusDisplayItem item:displayItems){
|
||||||
|
if(item instanceof GapStatusDisplayItem gap){
|
||||||
|
gaps.add(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(gaps.isEmpty())
|
||||||
|
return;
|
||||||
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
|
View child=list.getChildAt(i);
|
||||||
|
if(list.getChildViewHolder(child) instanceof GapStatusDisplayItem.Holder holder){
|
||||||
|
GapStatusDisplayItem gap=holder.getItem();
|
||||||
|
if(!gap.visible){
|
||||||
|
gap.visible=true;
|
||||||
|
gap.enteredFromTop=child.getTop()<list.getHeight()/2;
|
||||||
|
gaps.remove(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(GapStatusDisplayItem gap:gaps){
|
||||||
|
gap.visible=false;
|
||||||
|
}
|
||||||
|
gaps.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
View bottomOverlays=view.findViewById(R.id.bottom_overlays);
|
View bottomOverlays=view.findViewById(R.id.bottom_overlays);
|
||||||
|
@ -425,13 +450,23 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||||
public void onGapClick(GapStatusDisplayItem.Holder item){
|
public void onGapClick(GapStatusDisplayItem.Holder item){
|
||||||
if(dataLoading)
|
if(dataLoading)
|
||||||
return;
|
return;
|
||||||
item.getItem().loading=true;
|
GapStatusDisplayItem gap=item.getItem();
|
||||||
|
gap.loading=true;
|
||||||
V.setVisibilityAnimated(item.progress, View.VISIBLE);
|
V.setVisibilityAnimated(item.progress, View.VISIBLE);
|
||||||
V.setVisibilityAnimated(item.text, View.GONE);
|
V.setVisibilityAnimated(item.text, View.GONE);
|
||||||
GapStatusDisplayItem gap=item.getItem();
|
|
||||||
dataLoading=true;
|
dataLoading=true;
|
||||||
boolean needCache=listMode==ListMode.FOLLOWING;
|
boolean needCache=listMode==ListMode.FOLLOWING;
|
||||||
loadAdditionalPosts(item.getItemID(), null, 20, null, new Callback<>(){
|
boolean insertBelowGap=!gap.enteredFromTop;
|
||||||
|
String maxID, sinceID;
|
||||||
|
if(gap.enteredFromTop){
|
||||||
|
maxID=item.getItemID();
|
||||||
|
sinceID=null;
|
||||||
|
}else{
|
||||||
|
maxID=null;
|
||||||
|
int gapPos=displayItems.indexOf(gap);
|
||||||
|
sinceID=displayItems.get(gapPos+1).parentID;
|
||||||
|
}
|
||||||
|
loadAdditionalPosts(maxID, null, 20, sinceID, new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
|
|
||||||
|
@ -449,9 +484,9 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||||
if(gapStatus!=null){
|
if(gapStatus!=null){
|
||||||
gapStatus.hasGapAfter=false;
|
gapStatus.hasGapAfter=false;
|
||||||
if(needCache)
|
if(needCache)
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(gapStatus), false);
|
||||||
}
|
}
|
||||||
}else{
|
}else if(insertBelowGap){
|
||||||
Set<String> idsBelowGap=new HashSet<>();
|
Set<String> idsBelowGap=new HashSet<>();
|
||||||
boolean belowGap=false;
|
boolean belowGap=false;
|
||||||
int gapPostIndex=0;
|
int gapPostIndex=0;
|
||||||
|
@ -462,7 +497,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||||
belowGap=true;
|
belowGap=true;
|
||||||
s.hasGapAfter=false;
|
s.hasGapAfter=false;
|
||||||
if(needCache)
|
if(needCache)
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(s), false);
|
||||||
}else{
|
}else{
|
||||||
gapPostIndex++;
|
gapPostIndex++;
|
||||||
}
|
}
|
||||||
|
@ -480,8 +515,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||||
}
|
}
|
||||||
if(needCache)
|
if(needCache)
|
||||||
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
|
||||||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1); // Get a sub-list that contains the gap item
|
||||||
targetList.clear();
|
targetList.clear(); // remove the gap item
|
||||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||||
for(Status s:result){
|
for(Status s:result){
|
||||||
if(idsBelowGap.contains(s.id))
|
if(idsBelowGap.contains(s.id))
|
||||||
|
@ -498,6 +533,60 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||||
}
|
}
|
||||||
if(needCache)
|
if(needCache)
|
||||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||||
|
}else{
|
||||||
|
Set<String> idsAboveGap=new HashSet<>();
|
||||||
|
int gapPostIndex=0;
|
||||||
|
Status gapPost=null;
|
||||||
|
for(Status s:data){
|
||||||
|
if(s.id.equals(gap.parentID)){
|
||||||
|
gapPost=s;
|
||||||
|
break;
|
||||||
|
}else{
|
||||||
|
idsAboveGap.add(s.id);
|
||||||
|
gapPostIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(gapPost==null)
|
||||||
|
return;
|
||||||
|
boolean needAdjustScroll=false;
|
||||||
|
int scrollTop=0;
|
||||||
|
for(int i=0;i<list.getChildCount();i++){
|
||||||
|
View child=list.getChildAt(i);
|
||||||
|
if(list.getChildViewHolder(child) instanceof GapStatusDisplayItem.Holder gapHolder && gapHolder.getItem()==gap){
|
||||||
|
needAdjustScroll=true;
|
||||||
|
scrollTop=child.getBottom()+1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<StatusDisplayItem> targetList=displayItems.subList(gapPos+1, gapPos+1);
|
||||||
|
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||||
|
for(int i=result.size()-1;i>=0;i--){
|
||||||
|
Status s=result.get(i);
|
||||||
|
if(idsAboveGap.contains(s.id))
|
||||||
|
break;
|
||||||
|
targetList.addAll(0, buildDisplayItems(s));
|
||||||
|
insertedPosts.add(0, s);
|
||||||
|
}
|
||||||
|
boolean gapRemoved=false;
|
||||||
|
if(insertedPosts.size()<result.size()){ // There was an intersection, remove the gap
|
||||||
|
gapRemoved=true;
|
||||||
|
gapPost.hasGapAfter=false;
|
||||||
|
if(needCache)
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(gapPost), false);
|
||||||
|
displayItems.remove(gapPos);
|
||||||
|
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
|
||||||
|
}else{
|
||||||
|
gap.loading=false;
|
||||||
|
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
|
||||||
|
}
|
||||||
|
if(!insertedPosts.isEmpty()){
|
||||||
|
if(needCache)
|
||||||
|
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
|
||||||
|
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+(gapRemoved ? 0 : 1), targetList.size());
|
||||||
|
if(needAdjustScroll){
|
||||||
|
((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(getMainAdapterOffset()+gapPos+(gapRemoved ? 0 : 1)+targetList.size(), scrollTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
|
||||||
// Mind the gap!
|
// Mind the gap!
|
||||||
public class GapStatusDisplayItem extends StatusDisplayItem{
|
public class GapStatusDisplayItem extends StatusDisplayItem{
|
||||||
public boolean loading;
|
public boolean loading;
|
||||||
|
public boolean enteredFromTop; // While the user was scrolling, did the gap item pop out from behind the top edge of the list?
|
||||||
|
public boolean visible; // Is this item currently within the viewport of the RecyclerView (and has a bound view)?
|
||||||
|
|
||||||
public GapStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
|
public GapStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
|
||||||
super(parentID, parentFragment);
|
super(parentID, parentFragment);
|
||||||
|
|
Loading…
Reference in New Issue