我正在研发一款在回收商视图中使用无限滚动的Android应用。该应用程序是我的大学的新闻阅读器应用程序,允许学生阅读大学新闻报纸上的文章。我通过访问我们网站的REST api来收集这些文章。
我一直在使用我从这里得到的无限滚动视图:https://github.com/codepath/android_guides/wiki/Endless-Scrolling-with-AdapterViews-and-RecyclerView,但继续遇到与它相同的问题。大约1/5次,当我刷新滚动视图时,它将从新闻结果的第二页而不是第一页开始。我已经对此进行了大量的研究,但是还没有能够找到任何东西,而且我已经玩过无尽卷轴监听器的源代码无济于事。
下面我附上了我的项目的相关源代码和一系列显示问题的图片。
代码:
无限滚动侦听器。这是从上面的github链接中获取的。
package hu.ait.macweekly.listeners;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
/**
* Code from https://github.com/codepath/android_guides/wiki/Endless-Scrolling-with-AdapterViews-and-RecyclerView
*/
public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener {
public static final String NO_SEARCH = "";
public static final int NO_CATEGORY = -1;
// The minimum amount of items to have below your current scroll position
// before loading more.
private int visibleThreshold = 25;
// The current offset index of data you have loaded
private int currentPage = 0;
// The total number of items in the dataset after the last load
private int previousTotalItemCount = 0;
// True if we are still waiting for the last set of data to load.
private boolean loading = true;
// Sets the starting page index
private int startingPageIndex = 0;
// Sets category
private int categoryId = NO_CATEGORY;
// Sets search
private String searchString = NO_SEARCH;
RecyclerView.LayoutManager mLayoutManager;
public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) {
this.mLayoutManager = layoutManager;
}
public int getLastVisibleItem(int[] lastVisibleItemPositions) {
int maxSize = 0;
for (int i = 0; i < lastVisibleItemPositions.length; i++) {
if (i == 0) {
maxSize = lastVisibleItemPositions[i];
}
else if (lastVisibleItemPositions[i] > maxSize) {
maxSize = lastVisibleItemPositions[i];
}
}
return maxSize;
}
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
// but first we check if we are waiting for the previous load to finish.
@Override
public void onScrolled(RecyclerView view, int dx, int dy) {
int lastVisibleItemPosition = 0;
int totalItemCount = mLayoutManager.getItemCount();
if (mLayoutManager instanceof StaggeredGridLayoutManager) {
int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null);
// get maximum element within the list
lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions);
} else if (mLayoutManager instanceof GridLayoutManager) {
lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition();
} else if (mLayoutManager instanceof LinearLayoutManager) {
lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition();
}
// If the total item count is zero and the previous isn't, assume the
// list is invalidated and should be reset back to initial state
if (totalItemCount < previousTotalItemCount) {
this.currentPage = this.startingPageIndex;
this.previousTotalItemCount = totalItemCount;
if (totalItemCount == 0) {
this.loading = true;
}
}
// If it’s still loading, we check to see if the dataset count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && (totalItemCount > previousTotalItemCount)) {
loading = false;
previousTotalItemCount = totalItemCount;
}
// If it isn’t currently loading, we check to see if we have breached
// the visibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) {
currentPage++;
onLoadMore(currentPage, totalItemCount, view, categoryId, searchString);
loading = true;
}
}
// Call this method whenever performing new searches
public void resetState(RecyclerView view, int categoryId, String searchString) {
this.categoryId = categoryId;
this.searchString = searchString;
this.currentPage = this.startingPageIndex;
this.previousTotalItemCount = 0;
this.loading = true;
onScrolled(view, 0, 0);
}
// Defines the process for actually loading more data based on page
public abstract void onLoadMore(int page, int totalItemsCount, RecyclerView view, int categoryId, String searchString);
}
主: (需要注意的重要事项是onLoadMore()的声明和mEndlessScrollListener的初始化,但我包含了所有内容以防万一。)
package hu.ait.macweekly;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.NavigationView;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import hu.ait.macweekly.adapter.ArticleRecyclerAdapter;
import hu.ait.macweekly.data.Article;
import hu.ait.macweekly.data.GuestAuthor;
import hu.ait.macweekly.listeners.ArticleViewClickListener;
import hu.ait.macweekly.listeners.EndlessRecyclerViewScrollListener;
import hu.ait.macweekly.network.NewsAPI;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class MainActivity extends BaseActivity
implements NavigationView.OnNavigationItemSelectedListener,
ArticleViewClickListener {
boolean showingNewsFeed = false;
// Constants
private final String LOG_TAG = "MainActivity - ";
private final int ARTICLES_PER_CALL = 25;
// Members
private NewsAPI newsAPI;
private ArticleRecyclerAdapter mArticleAdapter;
private EndlessRecyclerViewScrollListener mEndlessScrollListener;
// Views
@BindView(R.id.main_content) RecyclerView mMainContent;
@BindView(R.id.refresh_view) SwipeRefreshLayout mSwipeRefreshLayout;
@BindView(R.id.newsFeedErrorView) LinearLayout mErrorView;
@BindView(R.id.errorButton) Button mButtonView;
// Code
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initContentViews();
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
prepareDrawer(toolbar);
prepareNavView();
prepareNewsAPI();
prepareContentViews();
}
private void prepareContentViews() {
mArticleAdapter = new ArticleRecyclerAdapter(getApplicationContext(), this);
mArticleAdapter.setDataSet(new ArrayList<Article>());
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
mMainContent.setLayoutManager(linearLayoutManager);
mMainContent.setAdapter(mArticleAdapter);
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
// callNewsAPI();
resetArticlesClear();
}
});
mSwipeRefreshLayout.post(new Runnable() { // TODO: 10/29/17 Need this?
@Override
public void run() {
mSwipeRefreshLayout.setRefreshing(true);
}
});
mEndlessScrollListener = new EndlessRecyclerViewScrollListener(linearLayoutManager) {
@Override
public void onLoadMore(int page, int totalItemsCount, RecyclerView view, int categoryId, String searchString) {
addArticles(page, categoryId, searchString);
}
};
mMainContent.addOnScrollListener(mEndlessScrollListener);
mButtonView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
resetArticlesClear();
}
});
}
private void initContentViews() {
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
private void prepareNavView() {
NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);
}
private void prepareDrawer(Toolbar toolbar) {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawer.setDrawerListener(toggle);
toggle.syncState();
// drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); // TODO: 10/30/17 Turn this back on when feature finished
}
public void prepareNewsAPI() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://themacweekly.com")
.addConverterFactory(GsonConverterFactory.create())
.build();
newsAPI = retrofit.create(NewsAPI.class);
}
@Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else {
super.onBackPressed();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_refresh) {
resetArticlesWithSearch("Aarohi");
return true;
}else if (id == R.id.about_page) {
goToAboutPage();
return true;
}
return super.onOptionsItemSelected(item);
}
private void goToAboutPage() {
Intent aboutPageIntent = new Intent(this, AboutPage.class);
startActivity(aboutPageIntent);
}
@SuppressWarnings("StatementWithEmptyBody")
@Override
public boolean onNavigationItemSelected(MenuItem item) {
// Handle navigation view item clicks here.
int id = item.getItemId();
if (id == R.id.nav_camera) {
// Handle the camera action
resetArticlesWithCategory(4);
} else if (id == R.id.nav_gallery) {
} else if (id == R.id.nav_slideshow) {
} else if (id == R.id.nav_manage) {
} else if (id == R.id.nav_send) {
}
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
return true;
}
public void showNewsFeed() {
mErrorView.setVisibility(View.GONE);
mMainContent.setVisibility(View.VISIBLE);
showingNewsFeed = true;
}
public void showErrorScreen() {
mMainContent.setVisibility(View.GONE);
mErrorView.setVisibility(View.VISIBLE);
showingNewsFeed = false;
}
public interface ArticleCallback {
void onSuccess(List<Article> articles);
void onFailure();
}
private void callNewsAPI(final int pageNum, int categoryId, String searchStr, final ArticleCallback articleCallback) {
final Call<List<Article>> articleCall;
if(categoryId != EndlessRecyclerViewScrollListener.NO_CATEGORY // Here we build our articleCall based on what information is passed to us
&& !searchStr.equals(EndlessRecyclerViewScrollListener.NO_SEARCH)) { // If we have category or search string, use those...
articleCall = newsAPI.getArticles(pageNum, ARTICLES_PER_CALL, categoryId, searchStr);
} else if(categoryId != EndlessRecyclerViewScrollListener.NO_CATEGORY) {
articleCall = newsAPI.getArticles(pageNum, ARTICLES_PER_CALL, categoryId);
} else if(!searchStr.equals(EndlessRecyclerViewScrollListener.NO_SEARCH)) {
articleCall = newsAPI.getArticles(pageNum, ARTICLES_PER_CALL, searchStr);
} else {
articleCall = newsAPI.getArticles(pageNum, ARTICLES_PER_CALL);
}
Log.d(LOG_TAG, "Sent article api call ----------------");
articleCall.enqueue(new Callback<List<Article>>() {
@Override
public void onResponse(Call<List<Article>> call, Response<List<Article>> response) {
mSwipeRefreshLayout.setRefreshing(false);
if (response.body() != null) {
Log.d(LOG_TAG, "Got response back. Page: "+pageNum+" -----------------");
List<Article> uncleanedResponse = response.body();
List<Article> cleanedResponse = cleanResponse(uncleanedResponse);
if(!showingNewsFeed) showNewsFeed();
articleCallback.onSuccess(cleanedResponse);
} else {
Log.e(LOG_TAG, "api response body is null. Page: "+pageNum);
articleCallback.onFailure();
}
}
@Override
public void onFailure(Call<List<Article>> call, Throwable t) {
Log.e(LOG_TAG, "call failed. Could not retrieve page. Page: "+pageNum);
mSwipeRefreshLayout.setRefreshing(false);
articleCallback.onFailure();
}
});
}
private void resetArticlesClear() {
resetArticles(EndlessRecyclerViewScrollListener.NO_CATEGORY, EndlessRecyclerViewScrollListener.NO_SEARCH);
}
private void resetArticlesWithCategory(int categoryId) {
resetArticles(categoryId, EndlessRecyclerViewScrollListener.NO_SEARCH);
}
private void resetArticlesWithSearch(String searchString) {
resetArticles(EndlessRecyclerViewScrollListener.NO_CATEGORY, searchString);
}
private void resetArticlesWithCatAndSearch(int categoryId, String searchString) {
resetArticles(categoryId, searchString);
}
private void resetArticles(int categoryId, String searchString) {
mArticleAdapter.clearDataSet();
mArticleAdapter.notifyDataSetChanged();
showNewsFeed();
mEndlessScrollListener.resetState(mMainContent, categoryId, searchString);
}
private void addArticles(int pageNum, int categoryId, String searchString) {
final int startSize = mArticleAdapter.getItemCount();
ArticleCallback articleCallback = new ArticleCallback() {
@Override
public void onSuccess(List<Article> articles) {
if (!showingNewsFeed) showNewsFeed();
mArticleAdapter.addToDataSet(articles);
mArticleAdapter.notifyItemRangeChanged(startSize, ARTICLES_PER_CALL);
}
@Override
public void onFailure() {
if (mArticleAdapter.getDataSet().size() == 0) showErrorScreen();
}
};
callNewsAPI(pageNum, categoryId, searchString, articleCallback);
}
private List<Article> cleanResponse(List<Article> uncleanedResponse) {
int MIN_CHAR_COUNT_FOR_ARTICLE = 1200; // Articles with char count < this val likely only have a video or audio link which our app doesn't handle.
//TODO: This also means however that we aren't loading things like comics or single images.
//Ultimately we want to be able to load videos or audio.
for (int i = uncleanedResponse.size() - 1; i >= 0; i--) {
Article article = uncleanedResponse.get(i);
if (MacWeeklyUtils.isTextEmpty(article.excerpt.rendered) || article.content.rendered.length() < MIN_CHAR_COUNT_FOR_ARTICLE) {
uncleanedResponse.remove(i);
}
}
return uncleanedResponse;
}
@Override
public void articleViewClicked(View view, int position) {
showFullArticle(mArticleAdapter.getDataSet().get(position));
}
private void showFullArticle(Article targetArticle) {
// These attributes might be null or missing
String authorBio = "";
String authorName = "";
String authorImgUrl = "";
if(targetArticle.guestAuthor != null) {
GuestAuthor gAuthor = targetArticle.guestAuthor;
if(gAuthor.name != null) {
authorName = targetArticle.guestAuthor.name;
}
if(!MacWeeklyUtils.isTextEmpty(gAuthor.imgUrl)) {
authorImgUrl = gAuthor.imgUrl;
}
if(!MacWeeklyUtils.isTextEmpty(gAuthor.bio)){
authorBio = gAuthor.bio;
}
}
Intent articleIntent = new Intent(this, ArticleActivity.class);
articleIntent.putExtra(ArticleActivity.ARTICLE_AUTHOR_KEY, "Author name here");
articleIntent.putExtra(ArticleActivity.ARTICLE_CONTENT_KEY, targetArticle.content
.rendered);
articleIntent.putExtra(ArticleActivity.ARTICLE_DATE_KEY, targetArticle.date);
articleIntent.putExtra(ArticleActivity.ARTICLE_TITLE_KEY, targetArticle.title.rendered);
articleIntent.putExtra(ArticleActivity.ARTICLE_AUTHOR_KEY, authorName);
articleIntent.putExtra(ArticleActivity.ARTICLE_LINK_KEY, targetArticle.link);
articleIntent.putExtra(ArticleActivity.AUTHOR_IMG_URL_KEY, authorImgUrl);
articleIntent.putExtra(ArticleActivity.AUTHOR_BIO_KEY, authorBio);
startActivity(articleIntent);
}
}
此外,这里有两张图片显示了这个问题。在第一张照片中,最新的文章是09年3月,这是准确的,但在第二张照片中,最新的文章是3月02日,这是不正确的。这篇文章是从第二页开始绘制的(我假设第一页刚刚被跳过。在第三张照片中,我们看到3月02日的文章确实出现在&#34; true&#34;列表的下方,大概是什么是第二页。
Infinite scroll working correctly, grabs most recent page
Infinite scroll working incorrectly, starts with page 2 in pagination
Infinite scroll working correctly, showing the second page starting after the first
很抱歉,如果这是太多的信息,不确定要包括多少。任何正确方向的提示都将非常感谢!
答案 0 :(得分:0)
可能已经发现了这个问题。由于我在25处有可见文章的最小阈值,并且我一次抓25个,程序会自动调用API两次。我不认为这会是一个问题,但可能存在某种并发问题。我做到这一点,当应用程序第一次加载时,它最多只能获取1页数据。
希望这个答案可以帮助其他从事分页工作的人!