一分鐘搞定觸手app主頁酷炫滑動切換效果

本篇文章已受權微信公衆號碼個蛋獨家發佈android

前言:

前幾天在看手機直播的時候,本身就用上了觸手app。一進到主頁就看上了裏面頁面切換的效果,本身想這種效果用什麼控件能夠實現呢。不閒扯了,直接上圖更有意思。git

觸手app新版已經改版了,你們請下載2.2.3.7424:github

效果圖:

觸手app主頁效果圖:api

觸手app主頁效果.gif

看到這個效果圖後,第一想到的就是RecyclerView貌似能夠實現這種效果,可是用RecyclerView本身的api仍是有不少問題的,先不說如何實現的吧,看下實現出來的效果圖吧:bash

圖片式:微信

圖片式效果圖.gif

流式佈局效果:app

流式佈局效果圖.gif

多種樣式效果:maven

多種樣式效果圖.gif

教你一分鐘搞定如何使用:

設置Manager:ide

RecyclerView chuShouView = (RecyclerView) findViewById(R.id.chushou_view);
chuShouView.setLayoutManager(new ChuShouManager());
複製代碼

設置觸摸輔助類ChuShouCallBack:佈局

ItemTouchHelper.Callback callback = new ChuShouCallBack(adapter, maps, ItemTouchHelper.UP | ItemTouchHelper.DOWN);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(chuShouView);
複製代碼

圖片式設置Adapter:

chuShouView.setAdapter(adapter = new ChuShouAdapter(this, maps));
複製代碼

流式佈局式設置Adapter:

chuShouView.setAdapter(adapter = new ChuShouScrollAdapter(this, items));
複製代碼

多種樣式設置Adapter:

chuShouView.setAdapter(chuShouGridAdapter = new ChuShouGridAdapter(this, gridItems));
複製代碼

這裏面的流式佈局的Adapter和多種樣式的Adapter有一個共同點,它們的item都是帶有滑動結構的,所以這裏我把它們的結構當成RecyclerView+RecyclerView來處理了,而上面的圖片式結構就是RecyclerView+ImageView來處理了,你們能夠着重看看ChuShouScrollAdapterChuShouGridAdapter代碼:

ChuShouGridAdapteronCreateViewHolder方法:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    return new MyHolder<ChuShouGridActivity.GridItem>(View.inflate(context, R.layout.scroll_item_layout, null), context) {
        @Override
        protected RecyclerView.Adapter<RecyclerView.ViewHolder> getAdapter(List<ChuShouGridActivity.GridItem> list, Context context) {
            return new ChuShouGridItemAdapter(list, context);
        }
        @Override
        protected RecyclerView.LayoutManager getLayoutManager(Context context, RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
            return new ChuShouGridLayoutManager(context, adapter);
        }
    };
}
複製代碼

ChuShouScrollAdapteronCreateViewHolder方法:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    return new MyHolder<ChuShouScrollActivity.ShowItem>(View.inflate(context, R.layout.scroll_item_layout, null), context) {
        @Override
        protected RecyclerView.Adapter<RecyclerView.ViewHolder> getAdapter(List<ChuShouScrollActivity.ShowItem> list, Context context) {
            return new FlowAdapter(list, context);
        }
        @Override
        protected RecyclerView.LayoutManager getLayoutManager(Context context, RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
            return new FlowLayoutManager();
        }
    };
}
複製代碼

R.layout.scroll_item_layout代碼:

<?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:background="#cccccc"
    android:orientation="vertical">

    <com.library.chushou.view.SlideRecyclerView
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
複製代碼

使用方面就注意這些了,下面就講講是怎麼實現該效果的。。。

講解:

下面就來說講如何用RecyclerView如何實現上面的效果了:

先來張本身畫的思路草圖吧:

草圖.png

這裏總體就是一個RecyclerView了,並且在初始的時候,須要定義咱們本身的Layoutmanager,代碼裏面可見(ChuShouManager)該類,該Layoutmanager的功能就是讓最後一個item在屏幕的上面顯示,第一個item在屏幕中顯示,第二個item到倒數第二個item在屏幕的下面顯示。因此手機上面顯示的永遠是RecyclerView中第一個item了,只不過在手指滑動的時候,去改變數據源。

/**
 * Created by xiangcheng on 17/4/11.
 * 初始化RecyclerView中全部item的位置
 */
public class ChuShouManager extends RecyclerView.LayoutManager {
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        	 *******省略代碼******
            //防止數量沒達到1個以上的要求
            if (getChildCount() >= 1) {
                //第一個item放在屏幕中
                if (i == 0) {
                    layoutDecoratedWithMargins(childAt, 0, 0,
                            getDecoratedMeasuredWidth(childAt),
                            getDecoratedMeasuredHeight(childAt));
                }
            }
            //須要判斷數量
            if (getChildCount() >= 2) {
                //從第二個item到倒數第二個放在屏幕下面
                if (i >= 1 && i < getItemCount() - 1) {
                    layoutDecoratedWithMargins(childAt,
                     0, getHeight(),
                     getDecoratedMeasuredWidth(childAt), 
                     getHeight() +getDecoratedMeasuredHeight(childAt));
                }
            }
            //數量要求
            if (getChildCount() >= 3) {
                //最後一個item放在屏幕上面
                if (i == getItemCount() - 1) {
                    layoutDecoratedWithMargins(childAt,     
                     0, -getDecoratedMeasuredHeight(childAt), 
                     getDecoratedMeasuredWidth(childAt),0);
                }
            }
    }

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        //這裏就直接定義recyclerView裏面item直接佔滿整個屏幕了       
         return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    }
}
複製代碼

好了,第一步終於完成啦,下面就是RecyclerViewtouch相關代碼控制了,關於touch的控制,咱們須要接觸到android.support.v7.widget.helper.ItemTouchHelper.Callback該類了:

屏幕快照 2017-04-17 14.11.28.png

從源碼截圖中看到該類是一個靜態的抽象類,說明咱們要使用的時候,須要去實現該類了。這裏定義了一個實現類ChuShouCallBackCallBack抽象類定義了只是定義了咱們的Drag(拖拽)動做,實際上咱們要用的是SimpleCallback子類,該類實現了咱們的Swipe(滑動)動做。所以這裏須要屏蔽Drag動做,實現Swipe動做。

屏蔽Drag動做,實現Swipe動做:

public class ChuShouCallBack extends ItemTouchHelper.SimpleCallback {

    *****省略代碼*****
    //該構造器屏蔽了swipDirection
    public ChuShouCallBack(RecyclerView.Adapter adapter, List mDatas) {
        this(adapter, mDatas, 0);
    }

	//該構造器接收傳進來的swipDirection
    public ChuShouCallBack(RecyclerView.Adapter adapter, List mDatas, int swipDirection) {
        this(0, swipDirection);
        this.mAdapter = adapter;
        this.mDatas = mDatas;
    }

    public ChuShouCallBack(int dragDirs, int swipeDirs) {
        super(dragDirs, swipeDirs);
    }
	*****省略代碼*****
}
複製代碼

下面看看RecyclerView滑動Item的監聽

@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, final RecyclerView.ViewHolder viewHolder, float dX, float dY, int act
    // super.onChildDraw(c,recyclerView,viewHolder,dX,dY,actionState,isCurrentlyActive);這裏就不要執行父類的該方法了,執行後就會讓當前item隨手勢移動了,這樣就不是咱們想
      Log.d(TAG,"dy")
    if (height == 0) {
        height = recyclerView.getHeight();
    }
    Log.d(TAG, "dy:" + dY + ",actionState:" + actionState + ",isCurrentlyActive:" + isCurrentlyActive);
      Log.d(TAG, "lastDy:" + lastDy);
    //add  2017/4/17,此時換了手指再去按住nextView,若是是下拉時:lastDy > 0 && dY <= 0,若是是上拉時:lastDy < 0 && dY >= 0
    Log.d(TAG, "height * getSwipeThreshold(viewHolder):" + height * getSwipeThreshold(viewHolder));
    if (lastDy > 0 && dY <= 0 || lastDy < 0 && dY >= 0) {
      if (lastDy > 0 && dY <= height * getSwipeThreshold(viewHolder) || lastDy < 0 && dY >= 0) {
        //這個是當鬆手時isCurrentlyActive=false
        if (!isCurrentlyActive) {
            if (valueAnimator == null) {
                //從鬆手一瞬間,從lastDy的位置到0
                valueAnimator = ValueAnimator.ofFloat(lastDy, 0);
                //這裏的下拉或上拉的最大距離是按照swipe的臨界值來算的
                float maxPullHeight = height * getSwipeThreshold(viewHolder);
                //最長的時間是200毫秒
                float duration = 200 * (Math.abs(lastDy) / maxPullHeight);
                valueAnimator.setDuration((long) duration);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        float animatedValue = (float) animation.getAnimatedValue();
                        float percent = Math.abs(animatedValue / height);
                        float scaleAlpha = (float) (1.0 - percent * 1.0);
                        viewHolder.itemView.setAlpha(scaleAlpha);
                        ((ViewGroup) viewHolder.itemView).getChildAt(0).setScaleX(scaleAlpha);
                        ((ViewGroup) viewHolder.itemView).getChildAt(0).setScaleY(scaleAlpha);
                        nextView.setTranslationY(animatedValue);
                    }
                });
                valueAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        lastDy = 0;
                        valueAnimator = null;
                    }
                });
                valueAnimator.start();
            }
        }
    } else {
        //該種狀況就是沒有換手指的狀況
        float percent = Math.abs(dY / height);
        float scaleAlpha = (float) (1.0 - percent * 1.0);
        viewHolder.itemView.setAlpha(scaleAlpha);
        ((ViewGroup) viewHolder.itemView).getChildAt(0).setScaleX(scaleAlpha);
        ((ViewGroup) viewHolder.itemView).getChildAt(0).setScaleY(scaleAlpha);
        //往下拉
        if (dY > 0) {
        	  //獲取屏幕上方的item
            nextView = recyclerView.getChildAt(recyclerView.getChildCount() - 1);
            View childAt = ((ViewGroup) nextView).getChildAt(0);
            if (childAt instanceof SlideRecyclerView) {
                SlideRecyclerView sl = (SlideRecyclerView) childAt;
                if (sl.getScrollY() == 0) {
                    sl.pullNextScroll();
                }
            }
            nextView.setTranslationY(dY);
            pullDown = true;
            lastDy = dY;
        } else if (dY < 0) {
            //往上拉的時候,獲取屏幕下面的item
            nextView = recyclerView.getChildAt(1);
            pullDown = false;
            nextView.setTranslationY(dY);
            lastDy = dY;
        }
    }
}
複製代碼

這裏看似代碼這麼長,實際上是在下拉的時候,獲取到的nextView對應的是屏幕上方的item,也就是RecyclerView的最後一個item,由於最後一個item是放在了屏幕的上面;在上拉的時候,獲取到的nextView對應的是屏幕下方的item,也就是RecyclerView的第二個item。

上面的代碼只是處理咱們的滑動,至於說鬆手的處理還沒說呢。這裏也正好說下onSwipe何時觸發。這裏須要介紹一個方法:

/**
 * Returns the fraction that the user should move the View to be considered as swiped.
 * The fraction is calculated with respect to RecyclerView's bounds. * <p> * Default value is .5f, which means, to swipe a View, user must move the View at least * half of RecyclerView's width or height, depending on the swipe direction.
 *
 * @param viewHolder The ViewHolder that is being dragged.
 * @return A float value that denotes the fraction of the View size. Default value
 * is .5f .
 */
 public float getSwipeThreshold(ViewHolder viewHolder) {
      return .5f;
 }
複製代碼

源碼說是隻要滑動位置超過了RecyclerView的width或height時就會觸發onSwiped方法,咱們這裏不須要去動該值就能夠了,默認就能夠,這裏也正好是RecyclerView高度一半的距離,在鬆手的時候就觸發onSwipe方法

接下來看看onSwiped都作了些什麼:

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
    Log.d(TAG, "onSwiped");
    lastDy = 0;
    refreshData(viewHolder);
    if (onSwipedListener != null) {
        onSwipedListener.onSwiped(pullDown);
    }
}
複製代碼

這裏處理了數據跟接口的回調的監聽,接下來看看refreshData方法作了些什麼:

/**
 * 處理swipe時候view的還原以及數據源的刷新
 *
 * @param viewHolder
 */
private void refreshData(RecyclerView.ViewHolder viewHolder) {
    //將當前的item進行還原
    viewHolder.itemView.setAlpha(1);
    ((ViewGroup) viewHolder.itemView).getChildAt(0).setScaleX(1);
    ((ViewGroup) viewHolder.itemView).getChildAt(0).setScaleY(1);
    if (pullDown) {
        //將上面移動的進行還原
        nextView.setTranslationY(-height);
        //往下拉的時候,將集合總體日後挪一位
        Collections.rotate(mDatas, 1);
    } else {
        //將下面移動的進行還原
        nextView.setTranslationY(height);
        //往上拉的時候,將集合總體往前挪一位
        Collections.rotate(mDatas, -1);
    }
    //刷新item
    mAdapter.notifyDataSetChanged();
}
複製代碼

關於外層的RecyclerView滑動處理就先說這麼多,下面來介紹如何處理內層帶有滑動結構的RecyclerView,兩個都有滑動結構,什麼時候才讓內層的RecyclerView,什麼時候讓外層的RecyclerView滑動呢:

這個時候來看下里面的SlideRecyclerView內部滑動的處理:

public SlideRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            getViewTreeObserver().removeGlobalOnLayoutListener(this);
            initView();
            if (getIsCurrentItem()) {
                addOnScrollListener(new 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);
                        scrollY += dy;
                        Log.d(TAG, "scrollY:" + scrollY);
                        if (scrollY == 0) {
                            //若是父recyclerView已經在頂部而且還往上滑的時候,讓chuShouCallBack沒有swipe動做
                            if (dy < 0) {
                                chuShouCallBack.setDefaultSwipeDirs(0);
                            }
                        }
                        if (isSlideToBottom()) {
                            //若是父recyclerView已經在底部而且還往下拉的時候,讓chuShouCallBack沒有swipe動做
                            if (dy > 0) {
                                chuShouCallBack.setDefaultSwipeDirs(0);
                            }
                        }
                    }
                });
            }
        }
    });
}
複製代碼

監聽SlideRecyclerView滑動位置來動態設置外層的RecyclerView是否有滑動處理。總結下來是兩種狀況:一種是SlideRecyclerView滑動到了頂部,此時若是往上滑的時候,須要禁掉外層的RecyclerView的滑動,直接調用chuShouCallBack.setDefaultSwipeDirs(0)方法就可禁掉外層的滑動。另外一種是SlideRecyclerView滑動到了底部,此時若是往下滑的時候,也須要禁掉外層的RecyclerView滑動。

說到這的時候不少人好奇爲何監聽SlideRecyclerView滑動處理沒有打開外層的RecyclerView代碼呢。這裏打開外層的RecyclerView滑動須要放在ontouch裏面處理。由於在SlideRecyclerView滑動監聽裏面是沒法監聽到若是滑動到頂部時繼續往下滑和滑動到底部時繼續往上滑的操做,所以這裏就經過ontouch的座標該變量來是否打開外層的RecyclerView滑動:

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
    if (getIsCurrentItem()) {
        switch (e.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "MotionEvent.ACTION_DOWN");
                startY = e.getY();
                chuShouCallBack.setDefaultSwipeDirs(0);
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "MotionEvent.ACTION_MOVE");
                dataY = e.getY() - startY;
                //只有滑動到頂部的時候纔會經過判斷兩點之間的距離來切換item
                if (scrollY == 0) {
                    if (dataY > 0) {
                        chuShouCallBack.setDefaultSwipeDirs(ItemTouchHelper.UP | ItemTouchHelper.DOWN);
                        Log.d(TAG, "到了頂部往下拉的時候");
                    }
                }
                if (isSlideToBottom()) {
                    if (dataY < 0) {
                        chuShouCallBack.setDefaultSwipeDirs(ItemTouchHelper.UP | ItemTouchHelper.DOWN);
                        Log.d(TAG, "到了底部往上拉的時候");
                    }
                }
                if (scrollY != 0 && !isSlideToBottom()) {
                    chuShouCallBack.setDefaultSwipeDirs(0);
                    Log.d(TAG, "在中間的位置");
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
    }
    return super.onInterceptTouchEvent(e);
}
複製代碼

這裏涉及到了三種狀況:

(1)SlideRecyclerView滑動到頂部的時候,繼續往下滑的時候,須要打開外層的RecyclerView滑動

(2)SlideRecyclerView滑動到底部的時候,繼續往上滑的時候,須要打開外層的RecyclerView滑動

(3)SlideRecyclerView滑動到中間某一個位置的時候,無論往上滑仍是往下滑須要禁掉外層的RecyclerView滑動

核心代碼就這麼多了,關於使用方面若是還有什麼疑問,能夠直接看demo,也能夠直接跟我探討,歡迎提出issue

總結:

  • 結構分析: 總體外層是一個大的RecyclerView(這裏定義成ScrollRecyclerView,爲了對外提供本身的ChuShouCallBack),裏面的item分兩種狀況,一種是帶有滑動結構,一種是非滑動結構。滑動結構的話又定義了一個RecyclerView(這裏定義成SlideRecyclerView,處理滑動的)。

  • 分析item的排列: 這裏就是ChuShouManager的職責了,它負責把最後一個item放在屏幕上方,第一個item放在屏幕中,從第二個item到倒數第二個item放在屏幕下方。

  • 處理touch的動做: ChuShouCallBack就是扮演該角色了,用來處理上拉和下拉改變item的透明度和平移量。最後在onSwipe時恢復item狀態及改變數據源

  • 處理item自己帶有滑動(SlideRecyclerView)和外層RecyclerView滑動衝突: 這裏就是分析什麼時候去禁掉外層RecyclerView滑動,什麼時候打開滑動。原則是當item滑動到頂部時,若再繼續往上滑禁掉外層RecyclerView滑動,若再繼續往下滑打開外層RecyclerView滑動;當item滑動到中間某一個位置時,此時無論再繼續往上滑仍是往下滑都是禁掉外層RecyclerView滑動;當item滑動到底部時,若再繼續往上滑打開外層RecyclerView滑動,若再繼續往下滑禁掉外層RecyclerView滑動。

後續添加:

滑動控件還會有ListViewScrollView

歡迎客官到本店光臨:184793647(qq羣)

gradle依賴:

allprojects {
	repositories {
		...
		maven { url 'https://jitpack.io' }
	}
}

dependencies {
       ...
       compile 'com.github.1002326270xc:ChuShouView-master:v1.3'
       ...
}

複製代碼

關於我:

email: a1002326270@163.com

csdn: blog.csdn.net/u010429219/…

github: github.com/1002326270x…

更多你喜歡的文章

仿360手機助手下載按鈕
仿蘋果版小黃車(ofo)app主頁菜單效果
設計一個銀行app的最大額度控件
帶你實現ViewGroup規定行數、item居中的流式佈局
定製一個相似地址選擇器的view
3D版翻頁公告效果
一分鐘搞定觸手app主頁酷炫滑動切換效果
快速利用RecyclerView的LayoutManager搭建流式佈局
用貝塞爾曲線本身寫的一個電量顯示的控件
快速搞定一個自定義的日曆
相關文章
相關標籤/搜索