前言: 虽然本文标题的有点标题党的感觉,但无论如何,通过这篇文章的学习以及你自己的实践认知,写个简单的滑屏小
Demo还是just so so的。
友情提示:
在继续往下面读之前,希望您对以下知识点有一定程度掌握,否则,继续看下去对您意义也不大。
1、掌握View(视图)的"视图坐标"以及"布局坐标",以及scrollTo()和scrollBy()方法的作用 ----- 必须理解
如果对这方面知识不太清楚的话,建议先看看我的这篇博客
<Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用说明>,
不夸张地说,这篇博客理论上来说是我们这篇博文的基础。
2、知道onInterceptTouchEvent()以及onTouchEvent()对触摸事件的分发流程 ---- 不是必须
3、知道怎么绘制自定义ViewGroup即可 ---- 不是必须
OK。 继续往下看,请一定有所准备 。大家跟着我一步一步来咯。
知识点一: 关于scrollTo()和scrollBy()以及偏移坐标的设置/取值问题
在前面一篇博文中《Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用说明》,我们掌握了scrollTo()和
scrollBy()方法的作用,这两个方法的主要作用是将View/ViewGroup移至指定的坐标中,并且将偏移量保存起来。另外:
mScrollX 代表X轴方向的偏移坐标
mScrollY 代表Y轴方向的偏移坐标
关于偏移量的设置我们可以参看下源码:
- package com.qin.customviewgroup;
- public class View {
- ....
- protected int mScrollX; //该视图内容相当于视图起始坐标的偏移量 , X轴 方向
- protected int mScrollY; //该视图内容相当于视图起始坐标的偏移量 , Y轴方向
- //返回值
- public final int getScrollX() {
- return mScrollX;
- }
- public final int getScrollY() {
- return mScrollY;
- }
- public void scrollTo(int x, int y) {
- //偏移位置发生了改变
- if (mScrollX != x || mScrollY != y) {
- int oldX = mScrollX;
- int oldY = mScrollY;
- mScrollX = x; //赋新值,保存当前便宜量
- mScrollY = y;
- //回调onScrollChanged方法
- onScrollChanged(mScrollX, mScrollY, oldX, oldY);
- if (!awakenScrollBars()) {
- invalidate(); //一般都引起重绘
- }
- }
- }
- // 看出原因了吧 。。 mScrollX 与 mScrollY 代表我们当前偏移的位置 , 在当前位置继续偏移(x ,y)个单位
- public void scrollBy(int x, int y) {
- scrollTo(mScrollX + x, mScrollY + y);
- }
- //...
- }
package com.qin.customviewgroup; public class View { .... protected int mScrollX; //该视图内容相当于视图起始坐标的偏移量 , X轴 方向 protected int mScrollY; //该视图内容相当于视图起始坐标的偏移量 , Y轴方向 //返回值 public final int getScrollX() { return mScrollX; } public final int getScrollY() { return mScrollY; } public void scrollTo(int x, int y) { //偏移位置发生了改变 if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; //赋新值,保存当前便宜量 mScrollY = y; //回调onScrollChanged方法 onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { invalidate(); //一般都引起重绘 } } } // 看出原因了吧 。。 mScrollX 与 mScrollY 代表我们当前偏移的位置 , 在当前位置继续偏移(x ,y)个单位 public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); } //... }
于是,在任何时刻我们都可以获取该View/ViewGroup的偏移位置了,即调用getScrollX()方法和getScrollY()方法
知识点二: Scroller类的介绍
在初次看Launcher滑屏的时候,我就对Scroller类的学习感到非常蛋疼,完全失去了继续研究的欲望。如今,没得办法,
得重新看Launcher模块,基本上将Launcher大部分类以及功能给掌握了。当然,也花了一天时间来学习Launcher里的滑屏实现
,基本上业是拨开云雾见真知了。
我们知道想把一个View偏移至指定坐标(x,y)处,利用scrollTo()方法直接调用就OK了,但我们不能忽视的是,该方法本身
来的的副作用:非常迅速的将View/ViewGroup偏移至目标点,而没有对这个偏移过程有任何控制,对用户而言可能是不太
友好的。于是,基于这种偏移控制,Scroller类被设计出来了,该类的主要作用是为偏移过程制定一定的控制流程(后面我们会
知道的更多),从而使偏移更流畅,更完美。
可能上面说的比较悬乎,道理也没有讲透。下面我就根据特定情景帮助大家分析下:
情景: 从上海如何到武汉?
普通的人可能会想,so easy : 飞机、轮船、11路公交车...
文艺的人可能会想, 小 case : 时空忍术(火影的招数)、翻个筋斗(孙大圣的招数)...
不管怎么样,我们想出来的套路可能有两种:
1、有个时间控制过程才能抵达(缓慢的前进) ----- 对应于Scroller的作用
假设做火车,这个过程可能包括: 火车速率,花费周期等;
2、瞬间抵达(超神太快了,都眩晕了,用户体验不太好) ------ 对应于scrollTo()的作用
模拟Scroller类的实现功能:
假设从上海做动车到武汉需要10个小时,行进距离为1000km ,火车速率200/h 。采用第一种时间控制方法到达武汉的
整个配合过程可能如下:
我们每隔一段时间(例如1小时),计算火车应该行进的距离,然后调用scrollTo()方法,行进至该处。10小时过完后,
我们也就达到了目的地了。
相信大家心里应该有个感觉了。我们就分析下源码里去看看Scroller类的相关方法.
其源代码(部分)如下: 路径位于 \frameworks\base\core\java\android\widget\Scroller.java
- public class Scroller {
- private int mStartX; //起始坐标点 , X轴方向
- private int mStartY; //起始坐标点 , Y轴方向
- private int mCurrX; //当前坐标点 X轴, 即调用startScroll函数后,经过一定时间所达到的值
- private int mCurrY; //当前坐标点 Y轴, 即调用startScroll函数后,经过一定时间所达到的值
- private float mDeltaX; //应该继续滑动的距离, X轴方向
- private float mDeltaY; //应该继续滑动的距离, Y轴方向
- private boolean mFinished; //是否已经完成本次滑动操作, 如果完成则为 true
- //构造函数
- public Scroller(Context context) {
- this(context, null);
- }
- public final boolean isFinished() {
- return mFinished;
- }
- //强制结束本次滑屏操作
- public final void forceFinished(boolean finished) {
- mFinished = finished;
- }
- public final int getCurrX() {
- return mCurrX;
- }
- /* Call this when you want to know the new location. If it returns true,
- * the animation is not yet finished. loc will be altered to provide the
- * new location. */
- //根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中
- public boolean computeScrollOffset() {
- if (mFinished) { //已经完成了本次动画控制,直接返回为false
- return false;
- }
- int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
- if (timePassed < mDuration) {
- switch (mMode) {
- case SCROLL_MODE:
- float x = (float)timePassed * mDurationReciprocal;
- ...
- mCurrX = mStartX + Math.round(x * mDeltaX);
- mCurrY = mStartY + Math.round(x * mDeltaY);
- break;
- ...
- }
- else {
- mCurrX = mFinalX;
- mCurrY = mFinalY;
- mFinished = true;
- }
- return true;
- }
- //开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx , startY+dy)出
- public void startScroll(int startX, int startY, int dx, int dy, int duration) {
- mFinished = false;
- mDuration = duration;
- mStartTime = AnimationUtils.currentAnimationTimeMillis();
- mStartX = startX; mStartY = startY;
- mFinalX = startX + dx; mFinalY = startY + dy;
- mDeltaX = dx; mDeltaY = dy;
- ...
- }
- }
public class Scroller { private int mStartX; //起始坐标点 , X轴方向 private int mStartY; //起始坐标点 , Y轴方向 private int mCurrX; //当前坐标点 X轴, 即调用startScroll函数后,经过一定时间所达到的值 private int mCurrY; //当前坐标点 Y轴, 即调用startScroll函数后,经过一定时间所达到的值 private float mDeltaX; //应该继续滑动的距离, X轴方向 private float mDeltaY; //应该继续滑动的距离, Y轴方向 private boolean mFinished; //是否已经完成本次滑动操作, 如果完成则为 true //构造函数 public Scroller(Context context) { this(context, null); } public final boolean isFinished() { return mFinished; } //强制结束本次滑屏操作 public final void forceFinished(boolean finished) { mFinished = finished; } public final int getCurrX() { return mCurrX; } /* Call this when you want to know the new location. If it returns true, * the animation is not yet finished. loc will be altered to provide the * new location. */ //根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中 public boolean computeScrollOffset() { if (mFinished) { //已经完成了本次动画控制,直接返回为false return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: float x = (float)timePassed * mDurationReciprocal; ... mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; ... } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; } //开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx , startY+dy)出 public void startScroll(int startX, int startY, int dx, int dy, int duration) { mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; ... } }
其中比较重要的两个方法为:
public void startScroll(int startX, int startY, int dx, int dy, int duration)
函数功能说明:根据当前已经消逝的时间计算当前的坐标点,保存在mCurrX和mCurrY值中
public void startScroll(int startX, int startY, int dx, int dy, int duration)
函数功能说明:开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,到达坐标为
(startX+dx , startY+dy)处。
PS : 强烈建议大家看看该类的源码,便于后续理解。
知识点二: computeScroll()方法介绍
为了易于控制滑屏控制,Android框架提供了 computeScroll()方法去控制这个流程。在绘制View时,会在draw()过程调用该
方法。因此, 再配合使用Scroller实例,我们就可以获得当前应该的偏移坐标,手动使View/ViewGroup偏移至该处。
computeScroll()方法原型如下,该方法位于ViewGroup.java类中
- /**
- * Called by a parent to request that a child update its values for mScrollX
- * and mScrollY if necessary. This will typically be done if the child is
- * animating a scroll using a {@link android.widget.Scroller Scroller}
- * object.
- */由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制
- public void computeScroll() { //空方法 ,自定义ViewGroup必须实现方法体
- }
/** * Called by a parent to request that a child update its values for mScrollX * and mScrollY if necessary. This will typically be done if the child is * animating a scroll using a {@link android.widget.Scroller Scroller} * object. */由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制 public void computeScroll() { //空方法 ,自定义ViewGroup必须实现方法体 }
为了实现偏移控制,一般自定义View/ViewGroup都需要重载该方法 。
其调用过程位于View绘制流程draw()过程中,如下:
- @Override
- protected void dispatchDraw(Canvas canvas){
- ...
- for (int i = 0; i < count; i++) {
- final View child = children[getChildDrawingOrder(count, i)];
- if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
- more |= drawChild(canvas, child, drawingTime);
- }
- }
- }
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- ...
- child.computeScroll();
- ...
- }
@Override protected void dispatchDraw(Canvas canvas){ ... for (int i = 0; i < count; i++) { final View child = children[getChildDrawingOrder(count, i)]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ... child.computeScroll(); ... }
Demo说明:
我们简单的复用了之前写的一个自定义ViewGroup,与以前一次有区别的是,我们没有调用scrollTo()方法去进行瞬间
偏移。 本次做法如下:
第一、调用Scroller实例去产生一个偏移控制(对应于startScroll()方法)
第二、手动调用invalid()方法去重新绘制,剩下的就是在 computeScroll()里根据当前已经逝去的时间,获取当前
应该偏移的坐标(由Scroller实例对应的computeScrollOffset()计算而得),
第三、当前应该偏移的坐标,调用scrollBy()方法去缓慢移动至该坐标处。
截图如下:
原始界面 点击按钮或者触摸屏之后的显示界面
附:由于滑动截屏很难,只是简单的截取了两个个静态图片,触摸的话可以实现左右滑动切屏了。
更多知识点,请看代码注释。。
- //自定义ViewGroup , 包含了三个LinearLayout控件,存放在不同的布局位置,通过scrollBy或者scrollTo方法切换
- public class MultiViewGroup extends ViewGroup {
- ...
- //startScroll开始移至下一屏
- public void startMove(){
- curScreen ++ ;
- Log.i(TAG, "----startMove---- curScreen " + curScreen);
- //使用动画控制偏移过程 , 3s内到位
- mScroller.startScroll((curScreen-1) * getWidth(), 0, getWidth(), 0,3000);
- //其实点击按钮的时候,系统会自动重新绘制View,我们还是手动加上吧。
- invalidate();
- //使用scrollTo一步到位
- //scrollTo(curScreen * MultiScreenActivity.screenWidth, 0);
- }
- // 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制
- @Override
- public void computeScroll() {
- // TODO Auto-generated method stub
- Log.e(TAG, "computeScroll");
- // 如果返回true,表示动画还没有结束
- // 因为前面startScroll,所以只有在startScroll完成时 才会为false
- if (mScroller.computeScrollOffset()) {
- Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY());
- // 产生了动画效果,根据当前值 每次滚动一点
- scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
- Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight());
- //此时同样也需要刷新View ,否则效果可能有误差
- postInvalidate();
- }
- else
- Log.i(TAG, "have done the scoller -----");
- }
- //马上停止移动,如果已经超过了下一屏的一半,我们强制滑到下一个屏幕
- public void stopMove(){
- Log.v(TAG, "----stopMove ----");
- if(mScroller != null){
- //如果动画还没结束,我们就按下了结束的按钮,那我们就结束该动画,即马上滑动指定位置
- if(!mScroller.isFinished()){
- int scrollCurX= mScroller.getCurrX() ;
- //判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕
- // 这样的一个简单公式意思是:假设当前滑屏偏移值即 scrollCurX 加上每个屏幕一半的宽度,除以每个屏幕的宽度就是
- // 我们目标屏所在位置了。 假如每个屏幕宽度为320dip, 我们滑到了500dip处,很显然我们应该到达第二屏,索引值为1
- //即(500 + 320/2)/320 = 1
- int descScreen = ( scrollCurX + getWidth() / 2) / getWidth() ;
- Log.i(TAG, "-mScroller.is not finished scrollCurX +" + scrollCurX);
- Log.i(TAG, "-mScroller.is not finished descScreen +" + descScreen);
- mScroller.abortAnimation();
- //停止了动画,我们马上滑倒目标位置
- scrollTo(descScreen *getWidth() , 0);
- curScreen = descScreen ; //纠正目标屏位置
- }
- else
- Log.i(TAG, "----OK mScroller.is finished ---- ");
- }
- }
- ...
- }
//自定义ViewGroup , 包含了三个LinearLayout控件,存放在不同的布局位置,通过scrollBy或者scrollTo方法切换 public class MultiViewGroup extends ViewGroup { ... //startScroll开始移至下一屏 public void startMove(){ curScreen ++ ; Log.i(TAG, "----startMove---- curScreen " + curScreen); //使用动画控制偏移过程 , 3s内到位 mScroller.startScroll((curScreen-1) * getWidth(), 0, getWidth(), 0,3000); //其实点击按钮的时候,系统会自动重新绘制View,我们还是手动加上吧。 invalidate(); //使用scrollTo一步到位 //scrollTo(curScreen * MultiScreenActivity.screenWidth, 0); } // 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制 @Override public void computeScroll() { // TODO Auto-generated method stub Log.e(TAG, "computeScroll"); // 如果返回true,表示动画还没有结束 // 因为前面startScroll,所以只有在startScroll完成时 才会为false if (mScroller.computeScrollOffset()) { Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY()); // 产生了动画效果,根据当前值 每次滚动一点 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight()); //此时同样也需要刷新View ,否则效果可能有误差 postInvalidate(); } else Log.i(TAG, "have done the scoller -----"); } //马上停止移动,如果已经超过了下一屏的一半,我们强制滑到下一个屏幕 public void stopMove(){ Log.v(TAG, "----stopMove ----"); if(mScroller != null){ //如果动画还没结束,我们就按下了结束的按钮,那我们就结束该动画,即马上滑动指定位置 if(!mScroller.isFinished()){ int scrollCurX= mScroller.getCurrX() ; //判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕 // 这样的一个简单公式意思是:假设当前滑屏偏移值即 scrollCurX 加上每个屏幕一半的宽度,除以每个屏幕的宽度就是 // 我们目标屏所在位置了。 假如每个屏幕宽度为320dip, 我们滑到了500dip处,很显然我们应该到达第二屏,索引值为1 //即(500 + 320/2)/320 = 1 int descScreen = ( scrollCurX + getWidth() / 2) / getWidth() ; Log.i(TAG, "-mScroller.is not finished scrollCurX +" + scrollCurX); Log.i(TAG, "-mScroller.is not finished descScreen +" + descScreen); mScroller.abortAnimation(); //停止了动画,我们马上滑倒目标位置 scrollTo(descScreen *getWidth() , 0); curScreen = descScreen ; //纠正目标屏位置 } else Log.i(TAG, "----OK mScroller.is finished ---- "); } } ... }
如何实现触摸滑屏?
其实网上有很多关于Launcher实现滑屏的博文,基本上也把道理阐释的比较明白了 。我这儿也是基于自己的理解,将一些
重要方面的知识点给补充下,希望能帮助大家理解。
想要实现滑屏操作,值得考虑的事情包括如下几个方面:
其中:onInterceptTouchEvent()主要功能是控制触摸事件的分发,例如是子视图的点击事件还是滑动事件。
其他所有处理过程均在onTouchEvent()方法里实现了。
1、屏幕的滑动要根据手指的移动而移动 ---- 主要实现在onTouchEvent()方法中
2、当手指松开时,可能我们并没有完全滑动至某个屏幕上,这是我们需要手动判断当前偏移至去计算目标屏(当前屏或者
前后屏),并且优雅的偏移到目标屏(当然是用Scroller实例咯)。
3、调用computeScroll ()去实现缓慢移动过程。
知识点介绍:
VelocityTracker类
功能: 根据触摸位置计算每像素的移动速率。
常用方法有:
public void addMovement (MotionEvent ev)
public void computeCurrentVelocity (int units)
provides pixels per millisecond, 1000 provides pixels per second, etc.
ViewConfiguration类
功能: 获得一些关于timeouts(时间)、sizes(大小)、distances(距离)的标准常量值 。
常用方法:
public int getScaledEdgeSlop()
说明:获得一个触摸移动的最小像素值。也就是说,只有超过了这个值,才代表我们该滑屏处理了。
public static int getLongPressTimeout()
说明:获得一个执行长按事件监听(onLongClickListener)的值。也就是说,对某个View按下触摸时,只有超过了
这个时间值在,才表示我们该对该View回调长按事件了;否则,小于这个时间点松开手指,只执行onClick监听
我能写下来的也就这么多了,更多的东西参考代码注释吧。 在掌握了上面我罗列的知识后(重点scrollTo、Scroller类),
其他方面的知识都是关于点与点之间的计算了以及触摸事件的分发了。这方面感觉也没啥可写的。
- //自定义ViewGroup , 包含了三个LinearLayout控件,存放在不同的布局位置,通过scrollBy或者scrollTo方法切换
- public class MultiViewGroup extends ViewGroup {
- private static String TAG = "MultiViewGroup";
- private int curScreen = 0 ; //当前屏幕
- private Scroller mScroller = null ; //Scroller对象实例
- public MultiViewGroup(Context context) {
- super(context);
- mContext = context;
- init();
- }
- public MultiViewGroup(Context context, AttributeSet attrs) {
- super(context, attrs);
- mContext = context;
- init();
- }
- //初始化
- private void init() {
- ...
- //初始化Scroller实例
- mScroller = new Scroller(mContext);
- // 初始化3个 LinearLayout控件
- ...
- //初始化一个最小滑动距离
- mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
- }
- // 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制
- @Override
- public void computeScroll() {
- // TODO Auto-generated method stub
- Log.e(TAG, "computeScroll");
- // 如果返回true,表示动画还没有结束
- // 因为前面startScroll,所以只有在startScroll完成时 才会为false
- if (mScroller.computeScrollOffset()) {
- Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY());
- // 产生了动画效果,根据当前值 每次滚动一点
- scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
- Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight());
- //此时同样也需要刷新View ,否则效果可能有误差
- postInvalidate();
- }
- else
- Log.i(TAG, "have done the scoller -----");
- }
- //两种状态: 是否处于滑屏状态
- private static final int TOUCH_STATE_REST = 0; //什么都没做的状态
- private static final int TOUCH_STATE_SCROLLING = 1; //开始滑屏的状态
- private int mTouchState = TOUCH_STATE_REST; //默认是什么都没做的状态
- //--------------------------
- //处理触摸事件 ~
- public static int SNAP_VELOCITY = 600 ; //最小的滑动速率
- private int mTouchSlop = 0 ; //最小滑动距离,超过了,才认为开始滑动
- private float mLastionMotionX = 0 ; //记住上次触摸屏的位置
- //处理触摸的速率
- private VelocityTracker mVelocityTracker = null ;
- // 这个感觉没什么作用 不管true还是false 都是会执行onTouchEvent的 因为子view里面onTouchEvent返回false了
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- // TODO Auto-generated method stub
- Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop);
- final int action = ev.getAction();
- //表示已经开始滑动了,不需要走该Action_MOVE方法了(第一次时可能调用)。
- //该方法主要用于用户快速松开手指,又快速按下的行为。此时认为是出于滑屏状态的。
- if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
- return true;
- }
- final float x = ev.getX();
- final float y = ev.getY();
- switch (action) {
- case MotionEvent.ACTION_MOVE:
- Log.e(TAG, "onInterceptTouchEvent move");
- final int xDiff = (int) Math.abs(mLastionMotionX - x);
- //超过了最小滑动距离,就可以认为开始滑动了
- if (xDiff > mTouchSlop) {
- mTouchState = TOUCH_STATE_SCROLLING;
- }
- break;
- case MotionEvent.ACTION_DOWN:
- Log.e(TAG, "onInterceptTouchEvent down");
- mLastionMotionX = x;
- mLastMotionY = y;
- Log.e(TAG, mScroller.isFinished() + "");
- mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
- break;
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- Log.e(TAG, "onInterceptTouchEvent up or cancel");
- mTouchState = TOUCH_STATE_REST;
- break;
- }
- Log.e(TAG, mTouchState + "====" + TOUCH_STATE_REST);
- return mTouchState != TOUCH_STATE_REST;
- }
- public boolean onTouchEvent(MotionEvent event){
- super.onTouchEvent(event);
- Log.i(TAG, "--- onTouchEvent--> " );
- // TODO Auto-generated method stub
- Log.e(TAG, "onTouchEvent start");
- //获得VelocityTracker对象,并且添加滑动对象
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(event);
- //触摸点
- float x = event.getX();
- float y = event.getY();
- switch(event.getAction()){
- case MotionEvent.ACTION_DOWN:
- //如果屏幕的动画还没结束,你就按下了,我们就结束上一次动画,即开始这次新ACTION_DOWN的动画
- if(mScroller != null){
- if(!mScroller.isFinished()){
- mScroller.abortAnimation();
- }
- }
- mLastionMotionX = x ; //记住开始落下的屏幕点
- break ;
- case MotionEvent.ACTION_MOVE:
- int detaX = (int)(mLastionMotionX - x ); //每次滑动屏幕,屏幕应该移动的距离
- scrollBy(detaX, 0);//开始缓慢滑屏咯。 detaX > 0 向右滑动 , detaX < 0 向左滑动 ,
- Log.e(TAG, "--- MotionEvent.ACTION_MOVE--> detaX is " + detaX );
- mLastionMotionX = x ;
- break ;
- case MotionEvent.ACTION_UP:
- final VelocityTracker velocityTracker = mVelocityTracker ;
- velocityTracker.computeCurrentVelocity(1000);
- //计算速率
- int velocityX = (int) velocityTracker.getXVelocity() ;
- Log.e(TAG , "---velocityX---" + velocityX);
- //滑动速率达到了一个标准(快速向右滑屏,返回上一个屏幕) 马上进行切屏处理
- if (velocityX > SNAP_VELOCITY && curScreen > 0) {
- // Fling enough to move left
- Log.e(TAG, "snap left");
- snapToScreen(curScreen - 1);
- }
- //快速向左滑屏,返回下一个屏幕)
- else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){
- Log.e(TAG, "snap right");
- snapToScreen(curScreen + 1);
- }
- //以上为快速移动的 ,强制切换屏幕
- else{
- //我们是缓慢移动的,因此先判断是保留在本屏幕还是到下一屏幕
- snapToDestination();
- }
- //回收VelocityTracker对象
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- //修正mTouchState值
- mTouchState = TOUCH_STATE_REST ;
- break;
- case MotionEvent.ACTION_CANCEL:
- mTouchState = TOUCH_STATE_REST ;
- break;
- }
- return true ;
- }
- ////我们是缓慢移动的,因此需要根据偏移值判断目标屏是哪个?
- private void snapToDestination(){
- //当前的偏移位置
- int scrollX = getScrollX() ;
- int scrollY = getScrollY() ;
- Log.e(TAG, "### onTouchEvent snapToDestination ### scrollX is " + scrollX);
- //判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕
- //直接使用这个公式判断是哪一个屏幕 前后或者自己
- //判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕
- // 这样的一个简单公式意思是:假设当前滑屏偏移值即 scrollCurX 加上每个屏幕一半的宽度,除以每个屏幕的宽度就是
- // 我们目标屏所在位置了。 假如每个屏幕宽度为320dip, 我们滑到了500dip处,很显然我们应该到达第二屏
- int destScreen = (getScrollX() + MultiScreenActivity.screenWidth / 2 ) / MultiScreenActivity.screenWidth ;
- Log.e(TAG, "### onTouchEvent ACTION_UP### dx destScreen " + destScreen);
- snapToScreen(destScreen);
- }
- //真正的实现跳转屏幕的方法
- private void snapToScreen(int whichScreen){
- //简单的移到目标屏幕,可能是当前屏或者下一屏幕
- //直接跳转过去,不太友好
- //scrollTo(mLastScreen * MultiScreenActivity.screenWidth, 0);
- //为了友好性,我们在增加一个动画效果
- //需要再次滑动的距离 屏或者下一屏幕的继续滑动距离
- curScreen = whichScreen ;
- //防止屏幕越界,即超过屏幕数
- if(curScreen > getChildCount() - 1)
- curScreen = getChildCount() - 1 ;
- //为了达到下一屏幕或者当前屏幕,我们需要继续滑动的距离.根据dx值,可能想左滑动,也可能像又滑动
- int dx = curScreen * getWidth() - getScrollX() ;
- Log.e(TAG, "### onTouchEvent ACTION_UP### dx is " + dx);
- mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2);
- //由于触摸事件不会重新绘制View,所以此时需要手动刷新View 否则没效果
- invalidate();
- }
- //开始滑动至下一屏
- public void startMove(){
- ...
- }
- //理解停止滑动
- public void stopMove(){
- ...
- }
- // measure过程
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- ...
- }
- // layout过程
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- ...
- }
- }
//自定义ViewGroup , 包含了三个LinearLayout控件,存放在不同的布局位置,通过scrollBy或者scrollTo方法切换 public class MultiViewGroup extends ViewGroup { private static String TAG = "MultiViewGroup"; private int curScreen = 0 ; //当前屏幕 private Scroller mScroller = null ; //Scroller对象实例 public MultiViewGroup(Context context) { super(context); mContext = context; init(); } public MultiViewGroup(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; init(); } //初始化 private void init() { ... //初始化Scroller实例 mScroller = new Scroller(mContext); // 初始化3个 LinearLayout控件 ... //初始化一个最小滑动距离 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } // 由父视图调用用来请求子视图根据偏移值 mScrollX,mScrollY重新绘制 @Override public void computeScroll() { // TODO Auto-generated method stub Log.e(TAG, "computeScroll"); // 如果返回true,表示动画还没有结束 // 因为前面startScroll,所以只有在startScroll完成时 才会为false if (mScroller.computeScrollOffset()) { Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY()); // 产生了动画效果,根据当前值 每次滚动一点 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight()); //此时同样也需要刷新View ,否则效果可能有误差 postInvalidate(); } else Log.i(TAG, "have done the scoller -----"); } //两种状态: 是否处于滑屏状态 private static final int TOUCH_STATE_REST = 0; //什么都没做的状态 private static final int TOUCH_STATE_SCROLLING = 1; //开始滑屏的状态 private int mTouchState = TOUCH_STATE_REST; //默认是什么都没做的状态 //-------------------------- //处理触摸事件 ~ public static int SNAP_VELOCITY = 600 ; //最小的滑动速率 private int mTouchSlop = 0 ; //最小滑动距离,超过了,才认为开始滑动 private float mLastionMotionX = 0 ; //记住上次触摸屏的位置 //处理触摸的速率 private VelocityTracker mVelocityTracker = null ; // 这个感觉没什么作用 不管true还是false 都是会执行onTouchEvent的 因为子view里面onTouchEvent返回false了 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // TODO Auto-generated method stub Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop); final int action = ev.getAction(); //表示已经开始滑动了,不需要走该Action_MOVE方法了(第一次时可能调用)。 //该方法主要用于用户快速松开手指,又快速按下的行为。此时认为是出于滑屏状态的。 if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) { return true; } final float x = ev.getX(); final float y = ev.getY(); switch (action) { case MotionEvent.ACTION_MOVE: Log.e(TAG, "onInterceptTouchEvent move"); final int xDiff = (int) Math.abs(mLastionMotionX - x); //超过了最小滑动距离,就可以认为开始滑动了 if (xDiff > mTouchSlop) { mTouchState = TOUCH_STATE_SCROLLING; } break; case MotionEvent.ACTION_DOWN: Log.e(TAG, "onInterceptTouchEvent down"); mLastionMotionX = x; mLastMotionY = y; Log.e(TAG, mScroller.isFinished() + ""); mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: Log.e(TAG, "onInterceptTouchEvent up or cancel"); mTouchState = TOUCH_STATE_REST; break; } Log.e(TAG, mTouchState + "====" + TOUCH_STATE_REST); return mTouchState != TOUCH_STATE_REST; } public boolean onTouchEvent(MotionEvent event){ super.onTouchEvent(event); Log.i(TAG, "--- onTouchEvent--> " ); // TODO Auto-generated method stub Log.e(TAG, "onTouchEvent start"); //获得VelocityTracker对象,并且添加滑动对象 if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); //触摸点 float x = event.getX(); float y = event.getY(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: //如果屏幕的动画还没结束,你就按下了,我们就结束上一次动画,即开始这次新ACTION_DOWN的动画 if(mScroller != null){ if(!mScroller.isFinished()){ mScroller.abortAnimation(); } } mLastionMotionX = x ; //记住开始落下的屏幕点 break ; case MotionEvent.ACTION_MOVE: int detaX = (int)(mLastionMotionX - x ); //每次滑动屏幕,屏幕应该移动的距离 scrollBy(detaX, 0);//开始缓慢滑屏咯。 detaX > 0 向右滑动 , detaX < 0 向左滑动 , Log.e(TAG, "--- MotionEvent.ACTION_MOVE--> detaX is " + detaX ); mLastionMotionX = x ; break ; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker ; velocityTracker.computeCurrentVelocity(1000); //计算速率 int velocityX = (int) velocityTracker.getXVelocity() ; Log.e(TAG , "---velocityX---" + velocityX); //滑动速率达到了一个标准(快速向右滑屏,返回上一个屏幕) 马上进行切屏处理 if (velocityX > SNAP_VELOCITY && curScreen > 0) { // Fling enough to move left Log.e(TAG, "snap left"); snapToScreen(curScreen - 1); } //快速向左滑屏,返回下一个屏幕) else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){ Log.e(TAG, "snap right"); snapToScreen(curScreen + 1); } //以上为快速移动的 ,强制切换屏幕 else{ //我们是缓慢移动的,因此先判断是保留在本屏幕还是到下一屏幕 snapToDestination(); } //回收VelocityTracker对象 if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } //修正mTouchState值 mTouchState = TOUCH_STATE_REST ; break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_REST ; break; } return true ; } ////我们是缓慢移动的,因此需要根据偏移值判断目标屏是哪个? private void snapToDestination(){ //当前的偏移位置 int scrollX = getScrollX() ; int scrollY = getScrollY() ; Log.e(TAG, "### onTouchEvent snapToDestination ### scrollX is " + scrollX); //判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕 //直接使用这个公式判断是哪一个屏幕 前后或者自己 //判断是否超过下一屏的中间位置,如果达到就抵达下一屏,否则保持在原屏幕 // 这样的一个简单公式意思是:假设当前滑屏偏移值即 scrollCurX 加上每个屏幕一半的宽度,除以每个屏幕的宽度就是 // 我们目标屏所在位置了。 假如每个屏幕宽度为320dip, 我们滑到了500dip处,很显然我们应该到达第二屏 int destScreen = (getScrollX() + MultiScreenActivity.screenWidth / 2 ) / MultiScreenActivity.screenWidth ; Log.e(TAG, "### onTouchEvent ACTION_UP### dx destScreen " + destScreen); snapToScreen(destScreen); } //真正的实现跳转屏幕的方法 private void snapToScreen(int whichScreen){ //简单的移到目标屏幕,可能是当前屏或者下一屏幕 //直接跳转过去,不太友好 //scrollTo(mLastScreen * MultiScreenActivity.screenWidth, 0); //为了友好性,我们在增加一个动画效果 //需要再次滑动的距离 屏或者下一屏幕的继续滑动距离 curScreen = whichScreen ; //防止屏幕越界,即超过屏幕数 if(curScreen > getChildCount() - 1) curScreen = getChildCount() - 1 ; //为了达到下一屏幕或者当前屏幕,我们需要继续滑动的距离.根据dx值,可能想左滑动,也可能像又滑动 int dx = curScreen * getWidth() - getScrollX() ; Log.e(TAG, "### onTouchEvent ACTION_UP### dx is " + dx); mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2); //由于触摸事件不会重新绘制View,所以此时需要手动刷新View 否则没效果 invalidate(); } //开始滑动至下一屏 public void startMove(){ ... } //理解停止滑动 public void stopMove(){ ... } // measure过程 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ... } // layout过程 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { ... } }
最后,希望大家能多多实践,最好能写个小Demo出来。那样才正在的掌握了所学所得。
本文代码示例下载地址:
http://download.csdn.net/detail/qinjuning/4194324 (二合一的Demo)
最后, 请一定要自己实践理解。否则,效果也不会很好。
相关推荐
总之,实现Android中的滑屏功能涉及到对触摸事件的处理和`Scroller`类的使用。通过对`Scroller`类的深入理解,我们可以创建出具有自定义动画效果的滑动界面,提升应用的交互体验。在实际开发中,我们还需要考虑性能...
在Android应用开发中,滑屏(Swipe)是一种常见的交互方式,可以用来切换页面、浏览图片或者在列表中滚动。本文将详细介绍三种实现Android滑屏效果的方法:使用ViewPager、GestureDetector和自定义View。 1. **...
在Android中,滑屏通常是通过监听触摸事件(MotionEvent)来实现的。当用户在屏幕上进行滑动操作时,系统会触发一系列的ACTION_DOWN、ACTION_MOVE和ACTION_UP等事件。开发者需要在这些事件中捕获用户的滑动行为,并...
GestureDetector是Android中处理手势识别的类,它可以监听滑动、长按等基本手势。通过覆写GestureDetector.SimpleOnGestureListener的方法,例如onDown()、onFling(),开发者可以实现自定义的滑屏效果。这种方式...
本文将深入探讨触摸滑屏的相关知识点,包括触摸事件的处理机制、手势识别以及如何在应用中实现滑动操作。 1. 触摸事件处理机制 Android系统通过事件分发机制来处理用户的触摸输入。当用户在屏幕上进行滑动操作时,...
本篇将围绕“android 左右滑屏”这一主题,探讨如何在Android中实现这种效果,同时也会提及到“源码”和“工具”的使用。 首先,我们要理解Android中的滑动事件处理。Android系统提供了GestureDetector和Scroller两...
在android学习中,动作交互是软件中重要的一部分,其中的Scroller就是提供了拖动效果的类,在网上,比如说一些Launcher实现滑屏都可以通过这个类去实现。这个就是Scroller类学习的后的实践了,效果很棒哦。你可以去...
首先,Android中的滑屏效果通常是通过ViewPager组件来实现的。ViewPager允许用户通过左右滑动来切换其内部的Fragment或View。在这个Demo中,开发者可能创建了一个自定义的ViewPager子类,增加了循环滚动的功能。通常...
vue-scroller移动端下拉加载组件 vue-scroller移动端下拉加载组件 vue-scroller移动端下拉加载组件
"Android高级应用源码-Android 仿 窗帘效果 和 登录界面拖动效果 (Scroller类的应用)"是一个很好的学习资源,它涉及到Android中的Scroller类以及自定义View的使用,这两个知识点在构建动态和流畅的用户界面时至关...
总之,Vue Virtual Scroller是Vue.js开发中处理大数据列表的理想工具,通过其强大的功能和灵活的配置,你可以构建出高性能且用户体验极佳的滚动列表。通过深入理解并熟练应用这款组件,可以提升你的Vue应用在数据...
在Android开发中,滑屏效果是用户界面设计中不可或缺的一部分,尤其在移动设备上,它提供了流畅、直观的导航体验。本资源集包含了三种不同的滑屏效果实现,这对于开发者来说是一份宝贵的参考资料,有助于提升应用的...
本练习将结合`Scroller`和`ViewPager`,教你如何在没有依赖库的情况下,手动实现一个类似`ViewPager`的滑动效果。 `Scroller`是Android系统中的一个核心类,它并不直接控制View的滚动,而是提供了一个离屏计算滚动...
总结,`Scroller`是Android中实现平滑滚动效果的重要工具,通过合理的使用和定制,我们可以创造出丰富多样的滚动动画。理解其工作原理并结合`Interpolator`,可以提升用户界面的交互体验。在实际项目中,应根据需求...
在Android开发中,Scroller类是一个非常重要的工具类,它主要负责处理滚动动画的效果,比如在启动器(Launcher)中的滑屏操作、ListView的滚动、甚至是自定义View时的平滑滚动等。Scroller并不直接控制View的移动,...
虚拟虚拟卷轴 快速滚动任意数量的数据| |赞助商目录安装npm install --save vue-virtual-scroller :warning: 现在, vue-virtual-scroller使用在显示时自动刷新自身,以防止显示故障。 这意味着您需要包括vue-...
在Android开发中,`Scroller`是一个非常重要的组件,它主要用于实现平滑的滚动效果,尤其是在处理触摸事件和动画时。本示例“android scroller学习demo”将带你深入理解如何利用`Scroller`实现类似QQ ListView的侧滑...
Android 使用Scroller实现绚丽的ListView左右滑动删除Item效果,项目详情http://blog.csdn.net/xiaanming/article/details/17539199