本篇文章已授权微信公众号码个蛋
独家发布
前几天在看手机直播的时候,自己就用上了触手app。一进到主页就看上了里面页面切换的效果,自己想这种效果用什么控件可以实现呢。不闲扯了,直接上图更有意思。
触手app新版已经改版了,大家请下载2.2.3.7424
触手app主页效果图:
看到这个效果图后,第一想到的就是RecyclerView
貌似可以实现这种效果,但是用RecyclerView
自己的api还是有很多问题的,先不说如何实现的吧,看下实现出来的效果图吧:
图片式:
流式布局效果:
多种样式效果:
设置Manager:
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来处理了,大家可以着重看看ChuShouScrollAdapter和ChuShouGridAdapter代码:
ChuShouGridAdapter的onCreateViewHolder方法:
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MyHolder(View.inflate(context, R.layout.scroll_item_layout, null), context) {
@Override
protected RecyclerView.Adapter getAdapter(List list, Context context) {
return new ChuShouGridItemAdapter(list, context);
}
@Override
protected RecyclerView.LayoutManager getLayoutManager(Context context, RecyclerView.Adapter adapter) {
return new ChuShouGridLayoutManager(context, adapter);
}
};
}
ChuShouScrollAdapter的onCreateViewHolder方法:
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MyHolder(View.inflate(context, R.layout.scroll_item_layout, null), context) {
@Override
protected RecyclerView.Adapter getAdapter(List list, Context context) {
return new FlowAdapter(list, context);
}
@Override
protected RecyclerView.LayoutManager getLayoutManager(Context context, RecyclerView.Adapter adapter) {
return new FlowLayoutManager();
}
};
}
R.layout.scroll_item_layout代码:
<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
如何实现上面的效果了:
这里整体就是一个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);
}
}
好了,第一步终于完成啦,下面就是RecyclerView
的touch
相关代码控制了,关于touch
的控制,我们需要接触到android.support.v7.widget.helper.ItemTouchHelper.Callback
该类了:
从源码截图中看到该类是一个静态的抽象类,说明我们要使用的时候,需要去实现该类了。这里定义了一个实现类ChuShouCallBack,CallBack抽象类定义了只是定义了我们的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.
*
* 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
滑动。
滑动控件还会有ListView
、ScrollView
等
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
...
compile 'com.github.1002326270xc:ChuShouView-master:v1.0'
...
}
email: [email protected]
csdn:http://blog.csdn.net/u010429219/article/details/70186730
简书:http://www.jianshu.com/p/cf2169630f5e
github:https://github.com/1002326270xc/ChuShouView-master