SwipeRefreshLayout
で新しいRecyclerView-Layout
を使用していますが、奇妙な動作を経験しました。リストを上部にスクロールして戻すと、上部のビューが切り取られることがあります。
今すぐ上にスクロールしようとすると、Pull-To-Refreshトリガーが発生します。
Recycler-Viewの周りのSwipe-Refresh-Layoutを削除しようとすると、問題はなくなります。そして、どの電話でも再現可能です(L-Previewデバイスだけでなく)。
<Android.support.v4.widget.SwipeRefreshLayout
Android:id="@+id/contentView"
Android:layout_width="match_parent"
Android:layout_height="match_parent"
Android:visibility="gone">
<Android.support.v7.widget.RecyclerView
Android:id="@+id/hot_fragment_recycler"
xmlns:Android="http://schemas.Android.com/apk/res/Android"
Android:layout_width="match_parent"
Android:layout_height="match_parent" />
</Android.support.v4.widget.SwipeRefreshLayout>
これが私のレイアウトです-行は、RecyclerViewAdapter(このリストに2つのビュータイプ)によって動的に構築されます。
public class HotRecyclerAdapter extends TikDaggerRecyclerAdapter<GameRow> {
private static final int VIEWTYPE_GAME_TITLE = 0;
private static final int VIEWTYPE_GAME_TEAM = 1;
@Inject
Picasso picasso;
public HotRecyclerAdapter(Injector injector) {
super(injector);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position, int viewType) {
switch (viewType) {
case VIEWTYPE_GAME_TITLE: {
TitleGameRowViewHolder holder = (TitleGameRowViewHolder) viewHolder;
holder.bindGameRow(picasso, getItem(position));
break;
}
case VIEWTYPE_GAME_TEAM: {
TeamGameRowViewHolder holder = (TeamGameRowViewHolder) viewHolder;
holder.bindGameRow(picasso, getItem(position));
break;
}
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
switch (viewType) {
case VIEWTYPE_GAME_TITLE: {
View view = inflater.inflate(R.layout.game_row_title, viewGroup, false);
return new TitleGameRowViewHolder(view);
}
case VIEWTYPE_GAME_TEAM: {
View view = inflater.inflate(R.layout.game_row_team, viewGroup, false);
return new TeamGameRowViewHolder(view);
}
}
return null;
}
@Override
public int getItemViewType(int position) {
GameRow row = getItem(position);
if (row.isTeamGameRow()) {
return VIEWTYPE_GAME_TEAM;
}
return VIEWTYPE_GAME_TITLE;
}
これがアダプターです。
hotAdapter = new HotRecyclerAdapter(this);
recyclerView.setHasFixedSize(false);
recyclerView.setAdapter(hotAdapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
contentView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
loadData();
}
});
TypedArray colorSheme = getResources().obtainTypedArray(R.array.main_refresh_sheme);
contentView.setColorSchemeResources(colorSheme.getResourceId(0, -1), colorSheme.getResourceId(1, -1), colorSheme.getResourceId(2, -1), colorSheme.getResourceId(3, -1));
Fragment
とRecycler
を含むSwipeRefreshLayout
のコード。
他の誰かがこの動作を経験して解決した場合、または少なくともその理由を見つけた場合は?
このソリューションを使用する前に、RecyclerViewはまだ完成していません。私が好きでない限り、本番環境で使用しないでください!
2014年11月に関しては、canScrollVertically
が早期にfalseを返すバグがRecyclerViewに残っています。このソリューションは、すべてのスクロールの問題を解決します。
ソリューションのドロップ:
public class FixedRecyclerView extends RecyclerView {
public FixedRecyclerView(Context context) {
super(context);
}
public FixedRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FixedRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean canScrollVertically(int direction) {
// check if scrolling up
if (direction < 1) {
boolean original = super.canScrollVertically(direction);
return !original && getChildAt(0) != null && getChildAt(0).getTop() < 0 || original;
}
return super.canScrollVertically(direction);
}
}
コード内のRecyclerView
をFixedRecyclerView
に置き換える必要さえありません。XMLタグを置き換えるだけで十分です! (RecyclerViewが完了すると、移行が迅速かつ簡単になります)
説明:
基本的に、canScrollVertically(boolean)
はfalseの返りが早すぎるため、RecyclerView
が最初のビューの最上部(最初の子の最上部が0になるまで)までスクロールされるかどうかを確認してから戻ります。
編集:何らかの理由でRecyclerView
を拡張したくない場合は、SwipeRefreshLayout
を拡張し、canChildScrollUp()
メソッドをオーバーライドして、そこにチェックロジックを配置できます。
EDIT2: RecyclerViewがリリースされており、今のところこの修正を使用する必要はありません。
addOnScrollListener
のRecyclerView
に次のコードを記述します
このような:
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int topRowVerticalPosition =
(recyclerView == null || recyclerView.getChildCount() == 0) ? 0 : recyclerView.getChildAt(0).getTop();
swipeRefreshLayout.setEnabled(topRowVerticalPosition >= 0);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
最近、同じ問題に出会いました。 @Krunal_Patelが提案するアプローチを試しましたが、Nexus 4ではほとんどの場合機能し、samsung galaxy s2ではまったく機能しませんでした。デバッグ中、recyclerView.getChildAt(0).getTop()は、RecyclerViewでは常に正しくありません。そのため、さまざまなメソッドを試した後、LayoutManagerのfindFirstCompletelyVisibleItemPosition()メソッドを使用して、RecyclerViewの最初の項目が表示されるかどうかを予測し、SwipeRefreshLayout.Findを有効にできることがわかりました。同じ問題を修正しようとしている人の助けになることを願っています。乾杯。
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
}
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
swipeRefresh.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0);
}
});
これが私のケースでこの問題を解決した方法です。これに似た解決策を検索するためにここにたどり着く他の誰かにとって有用かもしれません。
recyclerView.addOnScrollListener(new OnScrollListener()
{
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy)
{
// TODO Auto-generated method stub
super.onScrolled(recyclerView, dx, dy);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState)
{
// TODO Auto-generated method stub
//super.onScrollStateChanged(recyclerView, newState);
int firstPos=linearLayoutManager.findFirstCompletelyVisibleItemPosition();
if (firstPos>0)
{
swipeLayout.setEnabled(false);
}
else {
swipeLayout.setEnabled(true);
}
}
});
これが、同様のソリューションを探している人の助けになることを願っています。
ソースコードhttps://drive.google.com/open?id=0BzBKpZ4nzNzURkRGNVFtZXV1RWM
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener(){
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
}
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
swipeRefresh.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0);
}
});
誰かがこの質問を見つけて、答えに満足していない場合:
SwipeRefreshLayoutは、複数のアイテムタイプを持つアダプターと互換性がないようです。
Krunalのソリューションは優れていますが、修正プログラムのように機能し、次のような特定のケースをカバーしません。
RecyclerViewの画面中央にEditTextが含まれているとしましょう。アプリケーションを起動し(topRowVerticalPosition = 0)、EditTextをタップします。その結果、ソフトウェアキーボードが表示され、RecyclerViewのサイズが小さくなり、EditTextが表示されるようにシステムによって自動的にスクロールされ、topRowVerticalPositionが0にならないようにしますが、onScrolledは呼び出されず、topRowVerticalPositionは再計算されません。
したがって、このソリューションをお勧めします。
public class SupportSwipeRefreshLayout extends SwipeRefreshLayout {
private RecyclerView mInternalRecyclerView = null;
public SupportSwipeRefreshLayout(Context context) {
super(context);
}
public SupportSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setInternalRecyclerView(RecyclerView internalRecyclerView) {
mInternalRecyclerView = internalRecyclerView;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mInternalRecyclerView.canScrollVertically(-1)) {
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
内部RecyclerViewをSupportSwipeRefreshLayoutに指定すると、RecyclerViewを上にスクロールできない場合はタッチイベントをSupportSwipeRefreshLayoutに自動的に送信し、それ以外の場合はRecyclerViewに送信します。
これを処理する1つの方法は、ListView/GridViewも処理します。
public class SwipeRefreshLayout extends Android.support.v4.widget.SwipeRefreshLayout
{
public SwipeRefreshLayout(Context context)
{
super(context);
}
public SwipeRefreshLayout(Context context,AttributeSet attrs)
{
super(context,attrs);
}
@Override
public boolean canChildScrollUp()
{
View target=getChildAt(0);
if(target instanceof AbsListView)
{
final AbsListView absListView=(AbsListView)target;
return absListView.getChildCount()>0
&&(absListView.getFirstVisiblePosition()>0||absListView.getChildAt(0)
.getTop()<absListView.getPaddingTop());
}
else
return ViewCompat.canScrollVertically(target,-1);
}
}
残念ながら、これはLinearLayoutManagerの既知のバグです。最初のアイテムが表示されている場合、ScrollOffsetは適切に計算されません。リリース時に修正されます。
私は同じ問題に遭遇します。私の解決策は、onScrolled
のOnScrollListener
メソッドをオーバーライドすることです。
回避策は次のとおりです。
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int offset = dy - ydy;//to adjust scrolling sensitivity of calling OnRefreshListener
ydy = dy;//updated old value
boolean shouldRefresh = (linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0)
&& (recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) && offset > 30;
if (shouldRefresh) {
swipeRefreshLayout.setRefreshing(true);
} else {
swipeRefreshLayout.setRefreshing(false);
}
}
});
単一行ソリューション。
setOnScrollListenerは非推奨です。
次のような同じ目的でsetOnScrollChangeListenerを使用できます。
recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));
同じ問題が発生しました。予想される最初の表示項目がRecyclerView
に描画されるまで待機するスクロールリスナーを追加することで解決しました。このリスナーに沿って、他のスクロールリスナーもバインドできます。ヘッダービューホルダーを使用する場合にSwipeRefreshLayout
を有効にする必要がある場合、期待される最初の可視値がしきい値位置として使用するために追加されます。
public class SwipeRefreshLayoutToggleScrollListener extends RecyclerView.OnScrollListener {
private List<RecyclerView.OnScrollListener> mScrollListeners = new ArrayList<RecyclerView.OnScrollListener>();
private int mExpectedVisiblePosition = 0;
public SwipeRefreshLayoutToggleScrollListener(SwipeRefreshLayout mSwipeLayout) {
this.mSwipeLayout = mSwipeLayout;
}
private SwipeRefreshLayout mSwipeLayout;
public void addScrollListener(RecyclerView.OnScrollListener listener){
mScrollListeners.add(listener);
}
public boolean removeScrollListener(RecyclerView.OnScrollListener listener){
return mScrollListeners.remove(listener);
}
public void setExpectedFirstVisiblePosition(int position){
mExpectedVisiblePosition = position;
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
notifyScrollStateChanged(recyclerView,newState);
LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = llm.findFirstCompletelyVisibleItemPosition();
if(firstVisible != RecyclerView.NO_POSITION)
mSwipeLayout.setEnabled(firstVisible == mExpectedVisiblePosition);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
notifyOnScrolled(recyclerView, dx, dy);
}
private void notifyOnScrolled(RecyclerView recyclerView, int dx, int dy){
for(RecyclerView.OnScrollListener listener : mScrollListeners){
listener.onScrolled(recyclerView, dx, dy);
}
}
private void notifyScrollStateChanged(RecyclerView recyclerView, int newState){
for(RecyclerView.OnScrollListener listener : mScrollListeners){
listener.onScrollStateChanged(recyclerView, newState);
}
}
}
使用法:
SwipeRefreshLayoutToggleScrollListener listener = new SwipeRefreshLayoutToggleScrollListener(mSwiperRefreshLayout);
listener.addScrollListener(this); //optional
listener.addScrollListener(mScrollListener1); //optional
mRecyclerView.setOnScrollLIstener(listener);