Development Tip

RecyclerView 및 SwipeRefreshLayout

yourdevel 2020. 12. 14. 20:53
반응형

RecyclerView 및 SwipeRefreshLayout


나는 새로운 사용하고 RecyclerView-LayoutA의를 SwipeRefreshLayout하고 이상한 행동을 경험했다. 목록을 다시 맨 위로 스크롤 할 때 가끔 맨 위에있는보기가 잘립니다.

맨 위로 스크롤 된 목록

지금 맨 위로 스크롤하려고하면 Pull-To-Refresh가 트리거됩니다.

cutted 행

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 월 현재 RecyclerView에는 canScrollVertically조기에 false를 반환 하는 버그가 있습니다 . 이 솔루션은 모든 스크롤 문제를 해결합니다.

솔루션 하락 :

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에서 대부분 작동했으며 삼성 갤럭시 s2에서는 전혀 작동하지 않았습니다. 디버깅하는 동안 recyclerView.getChildAt (0) .getTop ()은 RecyclerView에 대해 항상 올바르지 않습니다. 그래서 다양한 방법을 살펴본 결과, LayoutManager의 findFirstCompletelyVisibleItemPosition () 메서드를 사용하여 RecyclerView의 첫 번째 항목이 표시되는지 여부를 예측하고 SwipeRefreshLayout을 활성화 할 수 있다고 생각했습니다. 아래 코드를 찾습니다. 누군가가 같은 문제를 해결하는 데 도움이되기를 바랍니다. 건배.

    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);
    }
});

불행히도 이것은 LinearLayoutManager의 알려진 버그입니다. 첫 번째 항목이 표시 될 때 제대로 계산되지 않습니다. 출시되면 수정됩니다.


나는 같은 문제를 경험했습니다. 예상되는 첫 번째 표시 항목이 .NET에 그려 질 때까지 기다리는 스크롤 리스너를 추가하여 해결했습니다 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);

나는 같은 문제에 봉착했다. 내 솔루션 오버라이드 (override)되는 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);
        }
    }
});

Here's one way to handle this, which also handles 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);
    }
  }

The krunal's solution is good, but it works like hotfix and does not cover some specific cases, for example this one:

Let's say that the RecyclerView contains an EditText at the middle of screen. We start application (topRowVerticalPosition = 0), taps on the EditText. As result, software keyboard shows up, size of the RecyclerView is decreased, it is automatically scrolled by system to keep the EditText visible and topRowVerticalPosition should not be 0, but onScrolled is not called and topRowVerticalPosition is not recalculated.

Therefore, I suggest this solution:

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);
    }
}

After you specify internal RecyclerView to SupportSwipeRefreshLayout, it will automatically send touch event to SupportSwipeRefreshLayout if RecyclerView cannot be scrolled up and to RecyclerView otherwise.


단일 라인 솔루션.

setOnScrollListener 는 더 이상 사용되지 않습니다.

다음과 같이 동일한 목적으로 setOnScrollChangeListener사용할 수 있습니다 .

recylerView.setOnScrollChangeListener((view, i, i1, i2, i3) -> swipeToRefreshLayout.setEnabled(linearLayoutManager.findFirstCompletelyVisibleItemPosition() == 0));

누군가이 질문을 발견하고 답변에 만족하지 않는 경우 :

SwipeRefreshLayout은 항목 유형이 둘 이상인 어댑터와 호환되지 않는 것 같습니다.

참고 URL : https://stackoverflow.com/questions/25178329/recyclerview-and-swiperefreshlayout

반응형