本章将介绍Android中十分重要的一个概念View
文章目录:
3.1 View的基础知识
1. 什么是View :
内容 | 含义 |
---|---|
View | 一个单一的控件,如Button、TextView |
ViewGroup | 一个控件组,如RelativeLayout、LinearLayout |
View的视图结构:
View的视图结构
-
View的位置参数:
View的宽高和坐标的关系
View的宽高和坐标的关系:
width = right - left
height = bottom - top
位置获取方式
View的位置是通过view.getxxx()函数进行获取:(以Top为例)
// 获取Top位置
public final int getTop() {
return mTop;
}
// 其余如下:
getLeft(); //获取子View左上角距父View左侧的距离
getBottom(); //获取子View右下角距父View顶部的距离
getRight(); //获取子View右下角距父View左侧的距离
2. MotionEvent和TouchSlop
- MotionEvent
在手指接触屏幕后所产生的事件:
常量 | 含义 |
---|---|
ACTION_DOWN | 手指接触屏幕(按下) |
ACTION_MOVE | 手指在屏幕上移动(滑动) |
ACTION_UP | 手指从屏幕上松开的一瞬间(离开) |
上述三种情况是典型的时间序列,同时通过 MontionEvent 对象我们可以得到点击事件发生的 x 和 y 坐标。系统提供两组方法:getX / getY 和 getRawX / getRawY。
具体代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
//get() :触摸点相对于其所在组件坐标系的坐标
event.getX();
event.getY();
//getRaw() :触摸点相对于屏幕默认坐标系的坐标
event.getRawX();
event.getRawY();
return super.onTouchEvent(event);
}
- TouchSlop
TouchSlop 是系统所能识别出的被认为是滑动的最小距离。换句话说,当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。原因很简单滑动的距离太短,系统不认为是滑动。这是一个常量和设备有关,不同的设备上这个值可能会不同。获取方式:
ViewConfiguration.get(this).getScaledTouchSlop();
4. VelocityTracher、GestureDetector和Scroller
- VelocityTracher
速度追踪,用于追踪手指在滑动过程中的速度,包括水平方向和竖直方向的速度。
@Override
public boolean onTouchEvent(MotionEvent event) {
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);//设置时间间隔为1000
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
Log.e("event", "xVelocity ;" + xVelocity + " yVelocity ;" + yVelocity);
//当我们结束的时候,需要调用 clear 方法来重置并且回收内存。
velocityTracker.clear();
velocityTracker.recycle();
return super.onTouchEvent(event);
}
这里需要注意,第一点,获取速度之前必须先计算速度,即 getXVelocity() 和 getYVelocity() 必须在 computeCurrentVelocity 的后面,第二点,这里的速度是指一段时间内手指所划过的像素数,比如将时间间隔设置有 1000ms 时,在1s 内手指在水平方向划过100像素,那么水平速度就是 100 ,当手指从右向左滑动时,速度为负数,公式:
<div align = center>速度 = (终点位置 - 起始位置)/ 时间段</div>
不要管时间间隔是传统含义,这里只要根据公式来计算即可。
- GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。首先创建一个MianActivity 实现GestureDetector.OnGestureListener ,OnDoubleTapListener接口 ,
public class MainActivity extends AppCompatActivity implements GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener{
private GestureDetector mGestureDetector;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGestureDetector = new GestureDetector(this,this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
}
@Override
public boolean onTouchEvent(MotionEvent event) {return mGestureDetector.onTouchEvent(event);}
//手指轻轻触摸屏幕的一瞬间,由一个 ACTION_DOWN 触发。
@Override
public boolean onDown(MotionEvent e) { return false;}
//手指轻轻触摸屏幕,尚未松开或拖动,由一个 ACTION_DOWN 触发。*注意和 onDown 的区别,它强调的是没有松开或者拖动的状态*
@Override
public void onShowPress(MotionEvent e) {}
//手指(轻轻触摸屏幕后)松开,伴随着一个 MontionEvent ACTION_UP 而触发,这是单击行为。
@Override
public boolean onSingleTapUp(MotionEvent e) {return false; }
//手指按下屏幕并拖动,由 1 个 ACTION_DOWN,多个 ACTION_MOVE 触发,这是拖动行为。
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {return false;}
//用户长久地按着屏幕不放,即长按。
@Override
public void onLongPress(MotionEvent e) { }
//用户按下触摸屏、快速滑动后松开,由 1 个 ACTION_DOWN 、多个 ACTION_MOVE 和 ACTION_UP 触发,这是快速滑动行为
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; }
//双击,由 2 次联系的单击组成,它不可能和 onSingleTapConfirmed 共存。
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {return false; }
//严格的单击行为
//*注意它和 onSingleTapUp的区别,如果触发了onSingleTapConfirmed,那么后面不可能再紧跟着另一个单击行为,即这只可能是单击,而不可能是双击中的一次单击*
@Override
public boolean onDoubleTap(MotionEvent e) {return false;}
//表示发生了双击行为,在双击的期间,ACTION_DOWN 、ACTION_MOVE 、ACTION_UP 都会触发此回调。
@Override
public boolean onDoubleTapEvent(MotionEvent e) {return false;}
}
这点可能会奇怪 setIsLongpressEnabled(false)参数要为false,经过我测试,setIsLongpressEnabled(true)的时候 ,长按屏幕触发 onLongPress 会直接拦截掉其他的触摸
down、move、up 事件,为 false 的时候,onLongPress 则不会触发,其他正常。
方法很多,但是并不是所有的方法都会被时常用到,在日常开发中,比较常用的onSingleTapUp(单击),onFling(快速滑动),onScroll(推动),onLongPress(长按)和onDoubleTap(双击),另外要说明的是,在实际开发中可以不使用 GestureDetector,完全可以自己在view中的onTouchEvent中去实现。
- Scroller
弹性的滑动对象,用于实现View的弹性滑动。
3.2 View 的滑动
常见的的三种 View 滑动实现方式:
- 通过 View 本身提供的 scrollTo / scrollBy 方法来实现滑动
- 通过动画给 View 施加平移想过
- 通过改变 View 的LayoutParams 使得 View 重新布局从而实现滑动。
3.2.1 使用 scrollTo/scrollBy
为了实现 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 智能改变 View 内容的位置而不能改变 View 在布局中的位置。mScrollX 和 mScrollY 单位为像素。
变换规律示意图(单位:像素)完整代码地址
列出部分代码:
public class ScollerView extends View implements View.OnClickListener {
...
public ScollerView(Context context, AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
this.setOnClickListener(this);
}
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
int scrollY = getScrollY();
int deltY = scrollY + destY;
//100ms 内滑向 destX ,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, deltY, 1000);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
...
}
3.2.2 使用动画
使用动画来移动 View ,主要操作 View 的 translatuonX 和 translationY 属性,既可以采用传统的 View 动画,也可以采用属性动画。书中的3.0兼容就不介绍了,现在也基本用不到。
采用 View 动画的代码,如下所示,此动画可以在100ms 内将一个 View 从原来位置像右下角移动 100 个像素。
在 res 目录中创建一个 anim 目录,在新建一个 set :
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true" android:zAdjustment="normal"
>
<translate
android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:toXDelta="100"
android:toYDelta="100"/>
</set>
在 Activity 中使用改方法,就可以移动mButton
ObjectAnimator.ofFloat(mButton,"translationX",0,100).setDuration(2000).start();
View 动画是对 View 的影像做操作,它并不能真正改变 View 的位置参数,包括宽高。并且如果希望动画后的状态得以保存还必须将 fillAfter 设为 true,为 false 时动画结束后 View 会恢复原状。
3.2.3 改变布局参数
第三种实现 View 滑动的方法,那就是改变布局参数,即改变 LayoutParams。改变 View 的位置,将LayoutParams 中的位置关系设置一下即可。实现方法很简单:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)
mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.requestLayout();
//或者mButton.setLayoutParams(params);
3.2.4 各种滑动方式的对比
- scrollTo/scrollBy:操作简单,适合对 View 内容的滑动;
- 动画:操作简单,主要适用于没有交互的 View 和实现复杂的动画效果;
- 改变布局参数:操作稍微复杂,适用于有交互的 View;
GIF.gif
自定义一个 View 继承 Button:
...
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
int translationX = (int) (getTranslationX() + deltaX);
int translationY = (int) (getTranslationY() + deltaY);
setTranslationX(translationX);
setTranslationY(translationY);
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
![GIF.gif](https://img.haomeiwen.com/i5531940/42d068be5205a447.gif?imageMogr2/auto-orient/strip)
mLastX = x;
mLastY = y;
return true;
}
通过上述代码可以看出,这一全屏滑动的效果实现起来相当简单。首先我们通过 getRawX 和 getRawY 方法来获取手指当前的坐标,注意这里不能使用 getX 和 getY 方法,getRawX 是获取全屏坐标,getX 是获取 View 的相对坐标(前面有讲到)。
3.3 弹性滑动
知道了 View 的滑动,还要知道如何实现 View 的弹性滑动。
3.3.1 使用 Scroller
之前使用过Scroller,现在来分析一下它的源码,探究一下为什么它能实现 View 的弹性滑动。
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
int scrollY = getScrollY();
int deltY = scrollY + destY;
//100ms 内滑向 destX ,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, deltY, 1000);
invalidate();
}
@Override
public void computeScroll() {
super.computeScroll();
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
上面是 Scroller 的典型的使用方法,这里先描述它的工作原理:当我们构造一个Scroller 对象并且调用它的 startScroll 方法时,Scroller 内部其实上面也没做,它只是保存了,我们传递的几个参数。这几个参数从 startScroll 的原型上就可以看出来,如下所示。
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;
}
这个方法的参数含义很清楚, startX 和 startY 表示的是滑动的七点,dx 和 dy 表示的是要滑动的距离,而 duration 表示的是滑动时间,即整个滑动过程完成所需要的时间,注意这里的滑动是指 View 滑动内容的滑动而非 View 本身的位置的改变,可以看到仅仅是调用 startScroll 方法是无法让 View 滑动的,因为它内部并没有做滑动相关的事,那么 Scroller 到底是如何让 View 弹性滑动的呢 ?答案就是 startScroll 方法下面的 invalidate 方法,虽然有点不可思议,但是的确是这样的。invalidate 方法会导致 View 重绘, 在 View 的 draw 方法中又会去调用 computeScroll 方法, computeScroll 方法在View 中是一个空实现,因此需要我们自己去实现,方面的代码已经实现了 computeScroll 方法。正是因为这个 computeScroll 方法,View 才能实现弹性滑动。这看起来还是很抽象,其实是这样的:当 View 重绘后在 draw 方法中调用 computeScroll ,而 computeScroll 又回去向 Scroller 获取当前的 scrollX 和 scrollY,然后通过 scrollTo 方法实现滑动;接着又调用 postInvalidate 方法来进行第二次重绘,这一次重绘的过程和第一次重绘一样,还是会导致 computeScroll 方法被调用了;然后继续向 Scroller 获取当前的 scrollX 和 scrollY。并通过 scrollTo 方法滑动到新的位置,如此反复,知道整个滑动的过程结束。
我们再看一下 Scroller 的 computeScrollOffset 方法的实现,如下所示:
public boolean computeScrollOffset() {
...
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
}
return true;
}
是不是突然就明白了?这个方法会根据时间的流逝来计算当前的scrollX和Y的值,计算方法也很简单,大意就是根据时间流逝的百分比来计算scrollX和Y改变的百分比并计算出当前的值,这个过程相当于动画的插值器的概念,这里我们先不去深究这个具体的过程,这个方法的返回值也很重要,他返回true表示滑动还未结束,false表示结束,因此这个方法返回true的时候,我们继续让View滑动
通过上面的分析,我相信大家应该都已经明白了Scroller的滑动原理了,这里做一个概括,他本身并不会滑动,需要配合computeScroll方法才能完成弹性滑动的效果,不断的让View重绘,而每次都有一些时间间隔,通过这个事件间隔就能得到他的滑动位置,这样就可以用ScrollTo方法来完成View的滑动了,就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动形成了弹性滑动,整个过程他对于View没有丝毫的引用,甚至在他内部连计时器都没有。
3.3.2 通过动画
一位大神的 View 系列 传送门:http://blog.csdn.net/harvic880925/article/details/50995268
动画本身就是一种渐进的过程,因此通过它来实现的滑动天然就具有弹性效果。
代码:
ValueAnimator animator = ValueAnimator.ofInt(0, 10, 60, 200).setDuration(2000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
int animatedValue = (int) animation.getAnimatedValue();
mButton.scrollTo(animatedValue, 0);//根据动画本身移动200px
mButton2.scrollTo((int) (fraction * 100), 0);//自定义移动100px
}
});
animator.start();
在上述代码中,mButton 动画移动距离 200,我们的动画本质上没有作用于任何对象上,只是在 2000ms 内完成了整个动画过程。利用这一特性,我们就可以在动画的每一帧到来时获取动画完成的比例 fraction ,然后再根据这个比例计算出当前 View 所要滑动的距离。mButton2 通过改变百分比 fraction 来完成 View 的滑动,采用这种方法除了能够完成弹性滑动以外,还可以实现其他动画效果,我们完全可以在 onAnimationUpdate 方法中加上我们的其他操作。
3.3.3 使用延时策略
另一种实现弹性滑动的方法,延时策略。核心思想是使用 Handler 或 View 的 postDelayed 方法,也可以使用线程的 sleep 方法。
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 30;
private int mCount = 0;
@SuppressLint("HandlerLeak")
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);
mButton.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
}
default:
break;
}
};
};
网友评论