1 View滑动主要类
(1)MotionEvent
当用户触摸屏幕时,将创建一个MontionEvent对象。MotionEvent包含了关于发生触摸的位置和时间的信息,以及触摸事件的其他细节
获取MontionEvent对象的方法有:
1.重载Activity中的onTouchEvent(MotionEvent event)方法;
2.View对象调用View.setOnTouchListener接口实现onTouch(View v, MotionEvent event)方法;
获得MontionEvent对象后,可以通过以下常用方法进一步获取触控事件的具体信息:
event.getAction() //获取触控动作比如ACTION_DOWN
event.getRawx(),event.getRawY(),event.getX(),event.getY()
事件类型
/**
* 手指按下时
*/
public static final int ACTION_DOWN = 0;
/**
* 移动操作时
*/
public static final int ACTION_MOVE = 2;
/**
* 手指放开时
*/
public static final int ACTION_UP = 1;
(2)ViewConfiguration
包含了方法和标准的常量用来设置UI的超时、大小和距离
其中TouchSlop参数是系统能识别的最小滑动距离,在ViewConfiguration有定义。通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获取TouchSlop
public static ViewConfiguration get(Context context) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final int density = (int) (100.0f * metrics.density);
ViewConfiguration configuration = sConfigurations.get(density);
if (configuration == null) {
configuration = new ViewConfiguration(context);
sConfigurations.put(density, configuration);
}
return configuration;
}
static final SparseArray<ViewConfiguration> sConfigurations =
new SparseArray<ViewConfiguration>(2);
public int getScaledTouchSlop() {
return mTouchSlop;
}
ViewConfiguration 实例存储在sConfiguration(SparseArray)中。
private ViewConfiguration(Context context) {
.....
mTouchSlop = res.getDimensionPixelSize(
com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
.....
}
<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
可以看出最小滑动距离是8dp.
(3)VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向。
首先,在View的onTouchEvent(MotionEvent event)方法中追踪当前点击事件的速度
VelocityTracker verTracker = VelocityTracker.obtain();
verTracker .addMovement(event);
static public VelocityTracker obtain() {
VelocityTracker instance = sPool.acquire();
return (instance != null) ? instance : new VelocityTracker(null);
}
这里使用了同步资源池
private static final SynchronizedPool<VelocityTracker> sPool =
new SynchronizedPool<VelocityTracker>(2);
(3.1)SynchronizedPool(Pools内部类)
public static class SynchronizedPool<T> extends SimplePool<T> {
private final Object mLock = new Object();
/**
* Creates a new instance.
*
* @param maxPoolSize The max pool size.
*
* @throws IllegalArgumentException If the max pool size is less than zero.
*/
public SynchronizedPool(int maxPoolSize) {
super(maxPoolSize);
}
@Override
public T acquire() {
synchronized (mLock) {
return super.acquire();
}
}
@Override
public boolean release(T element) {
synchronized (mLock) {
return super.release(element);
}
}
}
这里就是用数组保存了资源,使用acquire和release来完成资源的管理
public static class SimplePool<T> implements Pool<T> {
private final Object[] mPool;
private int mPoolSize;
/**
* Creates a new instance.
*
* @param maxPoolSize The max pool size.
*
* @throws IllegalArgumentException If the max pool size is less than zero.
*/
public SimplePool(int maxPoolSize) {
if (maxPoolSize <= 0) {
throw new IllegalArgumentException("The max pool size must be > 0");
}
mPool = new Object[maxPoolSize];
}
@Override
@SuppressWarnings("unchecked")
public T acquire() {
if (mPoolSize > 0) {
final int lastPooledIndex = mPoolSize - 1;
T instance = (T) mPool[lastPooledIndex];
mPool[lastPooledIndex] = null;
mPoolSize--;
return instance;
}
return null;
}
@Override
public boolean release(T instance) {
if (isInPool(instance)) {
throw new IllegalStateException("Already in the pool!");
}
if (mPoolSize < mPool.length) {
mPool[mPoolSize] = instance;
mPoolSize++;
return true;
}
return false;
}
private boolean isInPool(T instance) {
for (int i = 0; i < mPoolSize; i++) {
if (mPool[i] == instance) {
return true;
}
}
return false;
}
}
回到VelocityTracker中,我们获取了VelocityTracker实例,接着追踪事件,再来看看addMovement(event)
public void addMovement(MotionEvent event) {
if (event == null) {
throw new IllegalArgumentException("event must not be null");
}
nativeAddMovement(mPtr, event);
}
已经到Native层了
接着,我们想知道当前的滑动速度,在获取速度之前,必须先计算速度computeCurrentVelocity
verTracker .computeCurrentVelocity(1000);
现在我们可以获取速度了
int xVelocity=(int)verTracker .getXVelocity();
int yVelocity=(int)verTracker .geYVelocity();
public void computeCurrentVelocity(int units) {
nativeComputeCurrentVelocity(mPtr, units, Float.MAX_VALUE);
}
最后,我们要要清除回收内存
verTracker .clear();
verTracker .recycle();
public void clear() {
nativeClear(mPtr);
}
public void recycle() {
if (mStrategy == null) {
clear();
sPool.release(this);
}
}
(4)GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为
public class ThirdActivity extends Activity implements View.OnClickListener,GestureDetector.OnDoubleTapListener,GestureDetector.OnGestureListener {
private GestureDetector mGesture;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
mGesture=new GestureDetector(this);
mGesture.setIsLongpressEnabled(true);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean consume=mGesture.onTouchEvent(event);
return consume;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
return false;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
(5)Scroller
见3 弹性滑动
2 View滑动的类型
(一)通过View本身的scrollTo / scrollBy
使用方法举例:mButton.scrollTo(0,50);
(1)View
先看View中scrollTo 的源码和scrollBy的源码
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy也是调用了scrollTo方法。scrollTo中有两个重要参数,mScrollX和mScrollY,可以分别由view.getScrollX(), view.getScrollY()获取到
mScrollX的值 = (View的左边缘) - (View内容的左边缘),mScrollY = (View的上边缘) -(View内容上边缘),单位为像素。
(二)使用动画
(1)View动画
View动画是对View的影像在操作,并不能改变View的位置参数,包括宽/高。 所以单击动画的影像,无法触发点击事件,单击原来的位置,却可以。
通过java代码:
scale = (TextView) findViewById(R.id.scale);
ScaleAnimation sAnima = new ScaleAnimation(0, 5, 0, 5);
sAnima.setDuration(2000);
scale.startAnimation(sAnima);
通过XML
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<scale
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromXScale="1.0"
android:toXScale="1.4"
android:fromYScale="1.0"
android:toYScale="0.6"
android:pivotX="50%"
android:pivotY="50%"
android:fillAfter="false"
android:duration="1000" />
<set
android:interpolator="@android:anim/accelerate_interpolator"
android:startOffset="1000">
<scale
android:fromXScale="1.4"
android:toXScale="0.0"
android:fromYScale="0.6"
android:toYScale="0.0"
android:pivotX="50%"
android:pivotY="50%"
android:duration="400" />
<rotate
android:fromDegrees="0"
android:toDegrees="60"
android:toYScale="0.0"
android:pivotX="50%"
android:pivotY="50%"
android:duration="400" />
</set>
</set>
img = (ImageView)findViewById(R.id.xmlAnimImg);
Animation anim = AnimationUtils.loadAnimation(AnimaXmlActivity.this, R.anim.myanim);
img.startAnimation(anim);
(2)属性动画
属性动画可以解决动画属性点击无效的问题
ObjectAnimator
.ofFloat(view, "rotationX", 0.0F, 360.0F)
.setDuration(500)
.start();
(三)改变布局参数
通过改变布局参数让看起来在动
jump = (Button) findViewById(R.id.jump);
LinearLayout.LayoutParams param=(LinearLayout.LayoutParams)jump.getLayoutParams();
param.weight=80;
param.height=80;
jump.setLayoutParams(param);
(四)总结
1.scrollTo/scrollBy只能滑动内容,不能滑动View本身
2.View动画因为改变的是影像,所以用于不需要和用户交互的控件
3.布局适用于具有交互性的View,
3 弹性滑动
(一)使用Scroller
public class HorizontalScrollViewEx extends ViewGroup {
......
public HorizontalScrollViewEx(Context context) {
super(context);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
......
@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX, 0);
break;
}
case MotionEvent.ACTION_UP: {
int scrollX = getScrollX();
int scrollToChildIndex = scrollX / mChildWidth;
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
.......
private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
这里我们调用了mScroller.startScroll(getScrollX(), 0, dx, 0, 500),来看看startScroll的源码
(1)Scroller
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
所以仅仅调用startScroll是无法滑动的,再来看看invalidate,invalidate会导致View重绘,在View的draw方法中又会去调用computeScroll方法,再来看看我们对computeScroll方法的复写
(2)View
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
重点在computeScrollOffset之中
(3)Scroller
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float x = timePassed * mDurationReciprocal;
if (mInterpolator == null)
x = viscousFluid(x);
else
x = mInterpolator.getInterpolation(x);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
......
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
computeScrollOffset这个方法根据时间流逝的百分比算mScrollX和mScrollY,然后我们获取到mScrollX和mScrollY并进行重绘postInvalidate
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
再来看看postInvalidate,postInvalidate最终还是调用view的invalidate进行重绘,所以这里就变成了一个递归,直到(mScroller.computeScrollOffset()返回false的时候结束
(二)通过属性动画
ValueAnimator animator=ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction=animation.getAnimatedFraction();
jump.scrollTo((int)(2*fraction),0);
}
});
通过AnimatorUpdateListener监听动画的每一帧到来时间来获取动画完成的比例,在根据动画完成的比例计算当前View要滑动的距离
(三) 延时策略
@Override
public void onClick(View v) {
if (v == mButton1) {
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
}
private Handler mHandler = new Handler() {
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SCROLL_TO: {
mCount++;
if (mCount <= FRAME_COUNT) {
float fraction = mCount / (float) FRAME_COUNT;
int scrollX = (int) (fraction * 100);
mButton1.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
}
default:
break;
}
};
};
通过延时发送消息,自己造一个百分比出来,然后通过scrollTo完成弹性滑动。