1. 程式人生 > >具有回彈效果的RecyclerView,RecyclerView外層可滾動容器

具有回彈效果的RecyclerView,RecyclerView外層可滾動容器


    一個具有回彈效果的RecyclerView,本文通過實現RecyclerView外層的容器的上下滑動達到了回彈的效果,在整個滑動的事件分發機制中,外層容器的事件攔截機制進行判斷是否攔截事件,判斷標準為RecyclerView是否滾動到了第一個item或者最後一個item,如果下滑滾動到了第一個item還繼續下滑,外層容器的事件攔截機制將此事件進行攔截,交給外層容器的onTouchEvent進行消費;上滑,同理。如果不攔截,將交給子view即RecyclerView進行消費。

    下面是我自己總結的事件分發機制,希望對你能有幫助。不清楚的童鞋可以打印出來進行記憶。每天翻看幾遍,死記住,以後你會慢慢的明白其原理。

dispatchTouchEvent(MotionEvent ev)事件分發
1.返回true,表示這件事由dispatchTouchEvent消費掉了,事件停止向下傳遞。
2.返回false,表示事件不分發,返回給上一層activity或者父控制元件中的onTouchEvent進行消費。
3.返回super.dispatchTouchEvent(ev),事件交由當前view的onlnterceptTouchEvent進行事件攔截。

onlnterceptTouchEvent(MotionEvent ev) 事件攔截
1.返回true,表示事件攔截成功,交由當前view的onTouchEvent消費。
2.返回false,表示事件放行,將事件傳遞給下一個子view進行處理(下一個子view的dispatchTouchEvent進行處理)。
3.返回super.onlnterceptTouchEvent(ev),表示事件攔截,交由當前view的onTouchEvent消費,同true。

onTouchEvent(MotionEvent ev) 事件響應
前提:當前控制元件事件獲得響應
1.返回 false,事件將從當前的view向上傳遞,由父view的onTouchEvent來接收消費事件。
2.返回 true,事件被當前的view接收並消費掉。
3.返回onTouchEvent(ev),同false。

    下面是這個容器的程式碼:

容器類

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Scroller;

/**
 * Created by Sick on 2016/8/8.
 * 自定義可滾動元件的彈性容器,仿IOS回彈效果
 */
public class RVScrollLayout extends LinearLayout {
    private final String TAG = this.getClass().getSimpleName();
    /**
     * 容器中的元件
     */
    private View convertView;
    /**
     * 如果容器中的元件為RecyclerView
     */
    private RecyclerView recyclerView;
    /**
     * 滾動結束
     */
    private int mStart;
    /**
     * 滾動結束
     */
    private int mEnd;
    /**
     * 上一次滑動的座標
     */
    private int mLastY;
    /**
     * 滾動輔助類
     */
    private Scroller mScroller;

    public RVScrollLayout(Context context) {
        this(context, null);
    }

    public RVScrollLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RVScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mScroller = new Scroller(context);

    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if (getChildCount() > 1) {
            throw new RuntimeException(RVScrollLayout.class.getSimpleName() + "只能有一個子控制元件");
        }
        convertView = getChildAt(0);
       //TODO 可以拓展ListView等可滑動的元件
        if (convertView instanceof RecyclerView) {
            recyclerView = (RecyclerView) convertView;
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        if (changed) {
            View view = getChildAt(0);
            view.layout(left, top, right, bottom);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mStart = getScrollY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();  //終止動畫
                }
                scrollTo(0, (int) ((mLastY - y) * 0.4));
                break;
            case MotionEvent.ACTION_UP:
                mEnd = getScrollY();
                int dScrollY = mEnd - mStart;
                /**
                 * 回彈動畫,第一二個引數為開始的x,y
                 * 第三個和第四個引數為滾動的距離(注意方向問題)
                 * 第五個引數是回彈時間
                 */
                mScroller.startScroll(0, mEnd, 0, -dScrollY, 1000);
                break;
        }
        postInvalidate();
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        int y = (int) ev.getY();
        Log.d(TAG, "相對於元件滑過的距離==getY():" + y);
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                /**
                 * 下面兩個判斷來自於 BGARefreshLayout 框架中的判斷,github 上搜索 BGARefreshLayout
                 */
                if (convertView instanceof RecyclerView) {
                    if (y - mLastY > 0) {
                        if (Util.isRecyclerViewToTop(recyclerView)) {
                            Log.d(TAG, "滑倒頂部時時間攔截成功");
                            return true;
                        }
                    }

                    if (y - mLastY < 0) {
                        if (Util.isRecyclerViewToBottom(recyclerView)) {
                            Log.d(TAG, "滑倒底部時時間攔截成功");
                            return true;
                        }
                    }
                }
                break;
        }

        return false;
    }


    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(0, mScroller.getCurrY());
            postInvalidate();
        }
    }
}

區別scrollBy()和scrollTo(),前者是在上一個動作的基礎上移動,後者是從view的最開始移動,移動是相對於括號中的引數而言。

還有最重要的兩點:第一,它們移動的是當前view中的content,當前viewgroup中的子view,在recyclerview中相當於移動的它裡面的子item;第二,他們的移動,為什麼說他們的移動是一個重點呢,因為如果你將移動的x,y直接寫入引數,你會發現檢視是反方向的移動,這裡你可以將子item看成一個很大的畫板,手機(recyclerview)看成一箇中空的擋板,移動中空擋板就相當於子item移動了,所以它們的引數應該是一個負值。  即scrollBy(-offsetX,-offsetY)

getScrollY() 獲取的是view或這viewgroup滾動過的距離。

    再捋一捋這一流程的事件分發,首先滑動的時候事件是從外層向內層傳遞,本文中外層是容器,內層是recyclerview元件,事件分發給容器,容器進行判斷是否攔截,return true表示攔截,在容器自身的onTouchEvent()中消費,如果return false將交給recyclerview進行消費,這裡將不關注recyclerview。

    起初我的思路是重寫recyclerview的事件分發的方法,對外層容器傳遞過來的事件進行不分發返回給上層容器onTouchEvent()進行消費 或者 在它的OnTouchEvent()事件響應中返回return false 交給上層容器onTouchEvent()進行消費來達到容器可以回彈的效果。

父容器的回彈效果可以通過很多種方式進行實現,可以設定父容器的marginTop和marginBottom達到彈性的效果,而本文采用了Scroller輔助類滾動來實現的。

配置類(判斷recyclerview滾動)

import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
import android.view.ViewParent;

import java.lang.reflect.Field;

import cn.bingoogolapple.refreshlayout.BGAStickyNavLayout;

/**
 * Created by Sick on 2016/8/10.
 */
public class Util {
    public static boolean isRecyclerViewToTop(RecyclerView recyclerView) {
        if (recyclerView != null) {
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager == null) {
                return true;
            }
            if (manager.getItemCount() == 0) {
                return true;
            }

            if (manager instanceof LinearLayoutManager) {
                LinearLayoutManager layoutManager = (LinearLayoutManager) manager;

                int firstChildTop = 0;
                if (recyclerView.getChildCount() > 0) {
                    // 處理item高度超過一螢幕時的情況
                    View firstVisibleChild = recyclerView.getChildAt(0);
                    if (firstVisibleChild != null && firstVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) {
                        if (android.os.Build.VERSION.SDK_INT < 14) {
                            return !(ViewCompat.canScrollVertically(recyclerView, -1) || recyclerView.getScrollY() > 0);
                        } else {
                            return !ViewCompat.canScrollVertically(recyclerView, -1);
                        }
                    }

                    // 如果RecyclerView的子控制元件數量不為0,獲取第一個子控制元件的top

                    // 解決item的topMargin不為0時不能觸發下拉重新整理
                    View firstChild = recyclerView.getChildAt(0);
                    RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) firstChild.getLayoutParams();
                    firstChildTop = firstChild.getTop() - layoutParams.topMargin - getRecyclerViewItemTopInset(layoutParams) - recyclerView.getPaddingTop();
                }

                if (layoutManager.findFirstCompletelyVisibleItemPosition() < 1 && firstChildTop == 0) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * 通過反射獲取RecyclerView的item的topInset
     *
     * @param layoutParams
     * @return
     */
    private static int getRecyclerViewItemTopInset(RecyclerView.LayoutParams layoutParams) {
        try {
            Field field = RecyclerView.LayoutParams.class.getDeclaredField("mDecorInsets");
            field.setAccessible(true);
            // 開發者自定義的滾動監聽器
            Rect decorInsets = (Rect) field.get(layoutParams);
            return decorInsets.top;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

    public static boolean isRecyclerViewToBottom(RecyclerView recyclerView) {
        if (recyclerView != null) {
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager == null || manager.getItemCount() == 0) {
                return false;
            }

            if (manager instanceof LinearLayoutManager) {
                // 處理item高度超過一螢幕時的情況
                View lastVisibleChild = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
                if (lastVisibleChild != null && lastVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) {
                    if (android.os.Build.VERSION.SDK_INT < 14) {
                        return !(ViewCompat.canScrollVertically(recyclerView, 1) || recyclerView.getScrollY() < 0);
                    } else {
                        return !ViewCompat.canScrollVertically(recyclerView, 1);
                    }
                }

                LinearLayoutManager layoutManager = (LinearLayoutManager) manager;
                if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) {
                    BGAStickyNavLayout stickyNavLayout = getStickyNavLayout(recyclerView);
                    if (stickyNavLayout != null) {
                        // 處理BGAStickyNavLayout中findLastCompletelyVisibleItemPosition失效問題
                        View lastCompletelyVisibleChild = layoutManager.getChildAt(layoutManager.findLastCompletelyVisibleItemPosition());
                        if (lastCompletelyVisibleChild == null) {
                            return true;
                        } else {
                            // 0表示x,1表示y
                            int[] location = new int[2];
                            lastCompletelyVisibleChild.getLocationOnScreen(location);
                            int lastChildBottomOnScreen = location[1] + lastCompletelyVisibleChild.getMeasuredHeight();
                            stickyNavLayout.getLocationOnScreen(location);
                            int stickyNavLayoutBottomOnScreen = location[1] + stickyNavLayout.getMeasuredHeight();
                            return lastChildBottomOnScreen <= stickyNavLayoutBottomOnScreen;
                        }
                    } else {
                        return true;
                    }
                }
            } else if (manager instanceof StaggeredGridLayoutManager) {
                StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager;

                int[] out = layoutManager.findLastCompletelyVisibleItemPositions(null);
                int lastPosition = layoutManager.getItemCount() - 1;
                for (int position : out) {
                    if (position == lastPosition) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    public static BGAStickyNavLayout getStickyNavLayout(View view) {
        ViewParent viewParent = view.getParent();
        while (viewParent != null) {
            if (viewParent instanceof BGAStickyNavLayout) {
                return (BGAStickyNavLayout) viewParent;
            }
            viewParent = viewParent.getParent();
        }
        return null;
    }
}
Activity類
import android.app.Activity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;

/**
 * Created by Sick on 2016/8/8.
 */
public class TestActivity extends Activity {
    private RecyclerView rvCustomList;
    private ArrayList<String> data;
    private RVAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bbrecyclerveiw);
        initData();
        initView();

    }

    private void initData() {
        data = new ArrayList<String>();
        for (int i = 0; i <20; i++) {
            data.add("測試"+i);
        }
    }

    private void initView() {
        rvCustomList = (RecyclerView) findViewById(R.id.rv_custom_list);
        rvCustomList.setLayoutManager(new LinearLayoutManager(this));
        adapter = new RVAdapter();
        rvCustomList.setAdapter(adapter);
    }


    public class RVAdapter extends RecyclerView.Adapter{

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            MyHolderView holder = new MyHolderView(LayoutInflater.from(TestActivity.this).inflate(R.layout.item_data,parent,false));
            return holder;
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            if (holder instanceof MyHolderView){
                ((MyHolderView) holder).tvData.setText(data.get(position));
            }
        }

        @Override
        public int getItemCount() {
            return data.size();
        }
        private class MyHolderView extends RecyclerView.ViewHolder{
            TextView tvData;
            public MyHolderView(View itemView) {
                super(itemView);
                tvData = (TextView) itemView.findViewById(R.id.tv_data);
            }
        }
    }
}

xml檔案
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#00ff00">

    <com.song.view.RVScrollLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ff0000"
        >

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv_custom_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#0000ff">


        </android.support.v7.widget.RecyclerView>
    </com.song.view.RVScrollLayout>
</LinearLayout>
程式碼直接copy到你的專案裡面就可以使用,大神勿噴,小弟只是一個beginner,你有更好的想法更健壯的程式碼可以聯絡我![email protected]

相關推薦

具有效果RecyclerViewRecyclerView外層滾動容器

    一個具有回彈效果的RecyclerView,本文通過實現RecyclerView外層的容器的上下滑動達到了回彈的效果,在整個滑動的事件分發機制中,外層容器的事件攔截機制進行判斷是否攔截事件,判斷標準為RecyclerView是否滾動到了第一個item或者最後一個i

angual+mui 雙欄上拉加載微信裏面禁用默認事件可用可以防止瀏覽器效果

apply length data mui this reat mobile ng- a10 //html 部分 p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 15.0px Consolas; color: #2eafa9

原來操控介面可以這麼簡單----安卓上下滑動縮放頂部圖片左右滑動結束當前Activity及View柔和效果

先上效果圖: 上傳圖片不能超過2M,費了好大勁。每一張gif動的有點快,將就看。 首先說原理: 為activity的xml檔案根佈局新增setOnTouchListener。上下滑動和左右滑動的所有操作都是在OnTouchListener的onTouch方法中實現的,通過

下拉ScrollView伸縮頭佈局實現ScrollView效果

專案中用到了商品詳情展示效果,所以立馬想到借鑑天貓商品詳情介面,看了天貓的詳情頁面想到了兩套解決方案。1,使用LitView 新增header監聽listView 的滑動然後根據listView 的滑動距離計算 header應該滑動的距離 和改變header的

Android 自定義ScrollView 支援慣性滑動慣性效果。支援上拉載入更多

先講下原理: ScrollView的子View 主要分為3部分:head頭部,滾動內容,fooder底部 我們實現慣性滑動,以及回彈,都是靠超過head或者fooder 就重新滾動到  ,內容的頂部或者底部。 之前看了Pulltorefresh 他是通過不斷改變 head或

-webkit-overflow-scrolling 與滾動效果.

插件 列表 卡住 快的 優雅 css 移動設備 分享 兼容性 參考來源:https://developer.mozilla.org/zh-CN/docs/Web/CSS/-webkit-overflow-scrolling      https://www.w3cways.

自定義ScrollView 實現上拉下拉的效果--並且子控件中有Viewpager的情況

是否 AS abs pri tar utils lda animation ted onInterceptTouchEvent就是對子控件中Viewpager的處理:左右滑動應該讓viewpager消費 1 public class MyScrollView ext

解決蘋果微信瀏覽器下拉效果

.content是需要滑動的部分 var overscroll = function(el) { el.addEventListener('touchstart', function()

移動端阻止瀏覽器中預設元素滑動效果(橡皮筋效果

在js檔案中加如下程式碼: document.addEventListener('touchstart',function(e){ e.preventDefault(); //

JavaScript禁止微信瀏覽器下拉效果

本文例項為大家分享了JavaScript禁止微信瀏覽器下拉回彈的效果 方法1:         <script type="text/javascript">             var overscroll = function(el){     

ScrollView巢狀RecyclerViewRecyclerView總是把它上面的控制元件頂出頁面(頁面出現自己滾動)

ScrollView巢狀RecyclerView,當我離開當前頁面,然後又回來時,RecyclerView就會把它上邊的控制元件都擠出頁面,它顯示在頁面最上邊。 原因應該是RecyclerView搶了焦點,只需要把ScrollView中最上邊的那個控制元件加上幾句程式碼

android ListView 仿IOS 效果

最近看IOS的下拉效果感覺很不錯,當拉倒最上面和最下面的時候繼續拉動會有緩衝,想在android裡面也做一個,到網上到處找,沒有找到好的方法,據說android新的API對ListView有這樣的支援,感覺不是特別好用。 自己利用scroller實現了一下,廢話不多說了直接

實現ViewPager的效果

為了能夠在ViewPager的第一頁和最後一頁左右滑動時候不顯得那麼生硬,通過重寫ViewPager類實現回彈效果。 程式碼很簡單,主要重寫onTouchEvent方法。 程式碼如下: public class BounceBackViewPager

CoordinatorLayout初體驗以及標題欄下方圖片的效果

最近在研究material design ,瞭解到 CoordinatorLayout 這個佈局,所以研究和學習下,寫了個demo.加上拓展仿照了一個標題欄下方圖片的回彈效果,但不是使用CoordinatorLayout 實現的,下面看圖: 第一個效果是使

Android 帶阻尼效果的ScorllView

import android.content.Context; import android.graphics.Color; import android.graphics.Rect; import android.util.AttributeSet; impo

仿IOS效果支援任何控制元件

效果圖: 匯入依賴: dependencies { // ... compile 'me.everything:overscroll-decor-android:1.0.4

Android HorizontalScrollView效果

轉載記錄備份查閱 import android.annotation.SuppressLint; import android.os.Build; import android.util.Log; import android.view.MotionEvent

ViewPager效果

其實在我們很多應用中都看到當ViewPager滑到第一頁或者最後一頁的時候,如果再滑動的時候,就會有一個緩衝的過程,也就是回彈效果。之前在研究回彈效果的時候,也順便實現了ViewPager的回彈效果,其實也很簡單,一下是實現程式碼,註釋比較少: package com.fr

ScrollView實現阻尼效果

今天跟大夥簡紹個ScrollView的阻尼回彈!下拉到一定程度,可以回撥進行重新整理和進行操作等! 直接上程式碼了! package com.***.fb**.widget; import android.content.Context; import

Android滑動效果

原理: addHeaderView裡做的事: 1.測量出header的寬高,呼叫了measureView方法 2.設定LayoutParams,寬:MATCH_PARENT,高:10 3.設定topMargin的值為負的header的高度,即將header隱藏在螢幕最上方