一分钟搞定触手app主页酷炫滑动切换效果
前言:
前几天在看手机直播的时候,自己就用上了触手app。一进到主页就看上了里面页面切换的效果,自己想这种效果用什么控件可以实现呢。不闲扯了,直接上图更有意思。
触手app新版已经改版了,大家请下载。
效果图:
触手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<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);
}
};
}
ChuShouScrollAdapter的onCreateViewHolder方法:
@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
如何实现上面的效果了:
先来张自己画的思路草图吧:
这里整体就是一个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.
* <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
滑动。
后续添加:
滑动控件还会有ListView
、ScrollView
等
欢迎客官到本店光临:184793647
(qq群)
gradle依赖:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
...
compile 'com.github.1002326270xc:ChuShouView-master:v1.3'
...
}
项目文件目录截图:
项目结构图
一分钟搞定触手app主页酷炫滑动切换效果
注:本文著作权归作者,由demo大师代发,拒绝转载,转载需要作者授权
一分钟搞定触手app主页酷炫滑动切换效果的更多相关文章
- JS组件系列——又一款MVVM组件:Vue(一:30分钟搞定前端增删改查)
前言:关于Vue框架,好几个月之前就听说过,了解一项新技术之后,总是处于观望状态,一直在犹豫要不要系统学习下.正好最近有点空,就去官网了解了下,看上去还不错的一个组件,就抽空研究了下.最近园子里vue ...
- 转载-30分钟搞定后台登录界面(103个后台PSD源文件、素材网站)
原文:30分钟搞定后台登录界面(103个后台PSD源文件.素材网站) 目录 一.界面预览 二.PSD源文件预览 三.工具分享 四.资源说明 五.素材下载网站 六.下载 去年八月时要做一个OA系统为 ...
- OpenCV3.4.1快速集成到Android studio中,10分钟搞定
OpenCV3.4.1快速集成到Android studio中,10分钟搞定 转载 https://blog.csdn.net/yu540135101/article/details/8259 ...
- 一分钟搞定AlloyTouch图片轮播
一分钟搞定AlloyTouch图片轮播 轮播图也涉及到触摸和触摸反馈,同时,AlloyTouch可以把惯性运动打开或者关闭,并且设置min和max为运动区域,超出会自动回弹.除了一般的竖向滚动,A ...
- 五分钟搞定Go.js
五分钟搞定Go.js 1.基于html5~因为Go.js是一个依赖于HTML5特性的JavaScript库,所以需要确保您的页面声明它是一个HTML5文档,当然需要加载库 <!DOCTYPE ...
- Spring Boot 返回 XML 数据,一分钟搞定!
Spring Boot 返回 XML 数据,前提必须已经搭建了 Spring Boot 项目,所以这一块代码就不贴了,可以点击查看之前分享的 Spring Boot 返回 JSON 数据,一分钟搞定! ...
- zookeeper-架构设计与角色分工-《每日五分钟搞定大数据》
本篇文章阅读时间5分钟左右 点击看<每日五分钟搞定大数据>完整思维导图 zookeeper作为一个分布式协调系统,很多组件都会依赖它,那么此时它的可用性就非常重要了,那么保证可用性的同 ...
- zookeeper核心-zab协议-《每日五分钟搞定大数据》
上篇文章<paxos与一致性>说到zab是在paxos的基础上做了重要的改造,解决了一系列的问题,这一篇我们就来说下这个zab. zab协议的全称是ZooKeeper Atomic Bro ...
- windows+mysql集群搭建-三分钟搞定集群
注:本文来源: 陈晓婵 < windows+mysql集群搭建-三分钟搞定集群 > 一:mysql集群搭建教程-基础篇 计算机一级考试系统要用集群,目标是把集群搭建起来,保证一 ...
随机推荐
- VMWare虚拟机如何与主机共享文件夹(最容易看懂的讲解)附图~
http://wenku.baidu.com/view/54ab9e19227916888486d776.html 新建好虚拟机并安装好系统后,在编辑虚拟机设置--选项进行以下设置: 点添加 选择你要 ...
- SPI总线介绍
1. 简介 SPI, Serial Peripheral Interface, 串行外设接口, 是一种高速的.全双工.同步的通信总线SPI在芯片的管脚上只占用四根线 SPI接口主要用于MCU与各种外围 ...
- ES6 - Babel编译环境搭建
都看到这里了,我就不写什么node环境安装之类的了. 直接从新建项目文件夹后开始吧! 安装依赖: 命令行cd到项目文件夹之后,执行以下命令:(mac记得前边加sudo) npm init –y // ...
- PHPUnit安装(无需PEAR)
转自:http://www.cnblogs.com/bourneli/articles/2447155.html phpunit源代码下载:http://pear.phpunit.de/ 如何 ...
- POCO库中文编程参考指南(6)Poco::Timestamp
1 类型别名 三个时间戳相关的类型别名,TimeDiff表示两个时间戳的差,第二个是以微秒为单位的时间戳,第三个是以 100 纳秒(0.1 微妙)为单位的时间戳: typedef Int64 Time ...
- 4.创建OpenStack的node环境脚本
创建OpenStack的node环境脚本 使用source admin-openrc.sh来运行脚本 在任意目录下创建admin-openrc.sh文件 vim ~/admin-openrc.sh e ...
- hdu 5062(水题)
Beautiful Palindrome Number Time Limit: 3000/1500 MS (Java/Others) Memory Limit: 32768/32768 K (J ...
- js-限制参与活动的范围(微信H5活动)
近期接到大连某个项目,一个H5的活动,其中有一个需求就是:这个活动的参与者仅限大连地区的用户 所以参考了微信API 得出的操作结果为: wx.ready(function() { wx.getLoca ...
- Extract - <<凤凰牌老熊-现代支付系统设计>>
本文摘录自: http://blog.lixf.cn/essay/2017/04/01/concept-01-overview/ 一.支付概述-- 1. 支付与交易 交易过程: 交易的存在是支付发生的 ...
- 在cnBlogs上使用MarsEdit发blog
工欲善其事,必先利其器.既然决定了要经常使用blog,就要给自己一个好环境! 1.Mac下优秀的发博客工具--MarsEdit 网上有许多有用的文章教你如何使用它. 比如 http://fduo.or ...