在Android中,什么是View?
这就意味着View可以是单个控件,也可以是由多个控件组成的一组控件
View和位置主要由它的四个顶点来决定,分别对应View的四个属性:top、left、right、bottom,
对应如图所示:
根据上图我们可以得到View的宽高和坐标的关系;
width = right - left;
hight = bottom - top;
如何得到View的这四个参数呢?
left = getLeft();
right = getRight();
top = getTop();
bottom = getBottom();
注:从Android 3.0开始,View增加了几个额外的参数:x,y,translationX和translationY,
translationX
和translationY
是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标;这几个参数的换算关系如下:
x = left + translationX;
y = top + translationY;
此处需要特别说明:事件列,即指从手指接触屏幕至手指离开屏幕这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。
TouchSlop是系统所能识别出的被认为是滑动的最小距离
通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获取这个常量
在源码中还可以找到这个常量的定义
这个常量定义在frameworks/base/core/res/res/values/config.xml文件中,"config_viewConfigurationTouchSlop"对应的就是这个常量的定义。
用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。在View的onTouchEvent
方法中追踪当前单击事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
接着我们就可以来获取速度了,但是获取速度之前必须先计算速度:
velocityTracker.computeCurrentVelocity(1000);//在1000ms中的速度
float xVelocity = velocityTracker.getXVelocity();
float yVelocity = velocityTracker.getYVelocity();
这里的速度是指一段时间内手指滑过的像素数,比如时间间隔设为1000ms时,在1s内,手指在水平方向从左向右滑过100像素,那么水平速度就是100。
速度 = (终点位置 - 起点位置)/ 时间段 ;
当不使用它的时候,需要调用clear方法来重置并回收内存:
velocityTracker.clear();
velocityTracker.recycle();
用于辅助检测用户的单击,滑动,长按,双击等行为。
GestureDetector
的使用:
GestureDetector
对象并继承OnGestureListener
和OnDoubleTapListener
接口GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法手动的问题
mGestureDetector.setIsLongpressEnabled(false);
onTouchEvent
方法,在待监听的View的onTouchEvent
方法中添加如下实现:boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
然后我们就可以有选择的实现这两个接口中的方法了:
在实际开发中,如果只是监听滑动相关的,建议在onTouchEvent中实现,如果是监听双击这种行为,使用GestureDetector。
弹性滑动对象,用于实现View的弹性滑动(实现滑动过程)
View中使用的scrollTo/scrollBy
进行滑动时是瞬间完成的
Scroller
需要与View的computeScroll
方法结合后可以实现滑动过程
Scroller scroller = new Scroller(getContext());
private void smoothScrollerTo(int destX, int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
scroller.startScroll(scrollX,0,delta,1000);
invalidate();//重绘界面
}
@Override
public void computeScroll() {
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}
在Android设备上,滑动几乎是应用的标配,通过三种方法可以实现View的滑动:
ScrollTo/ScrollBy
方法来实现滑动;动画
给View
施加平移效果来实现滑动;LayoutParams
使得View重新布局从而实现滑动。/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
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();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy
实际也是调用了scrollTo
方法,它实现了基于当前位置的相对滑动,而scrollTo
实现了基于所传递参数的绝对滑动。mScrollX的值
总是等于View的左边缘和View内容左边缘在水平方向的距离
,而mScrollY的值
总等于View的上边缘和View内容上边缘在竖直方向的距离
。scrollTo
和scrollBy
只能改变View内容的位置而不能改变View在布局中的位置。参数说明:
mScrollX
和mScrollY
的单位为像素,并且当View左边缘在View内容左边缘的右边时,mScrollX为正值,反之为负值;使用动画,主要就是操作View的translationX
和translationY
属性,既可以采用View动画,也可以采用属性动画。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
//需要设置这个属性为true才会保留动画后的状态,否则会还原
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>
这个操作并不能真正改变View的位置参数,包括宽/高
//按顺序对应括号内的参数(目标view,水平移动,0位置,到100位置,100ms内移动)
ObjectAnimation.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
View动画是对View的内容做操作,它并不能真正改变View的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后其动画结果会消失,View会瞬间恢复到动画前的状态。使用属性动画不会存在上述问题。
即改变LayoutParams
一、比如将一个Button右移100px:将这个Button的L ayoutParams 里的marginLeft参数的值增加100px
二、在Button左边放一个空的View,然后设置这个View的宽度来挤动Button(意思这个改变参数布局的可以灵活使用)
例子:
MarginLayoutParams params = (MarginLayoutParams) mButton 1. getLayoutParams ();
params.width += 100;
params.leftMargin += 100;
//下面是应用这个改动
mButton 1. requestLayout ();
// mButton 1. setLayoutParams (params);
1、scrollTo/scrollBy
:View提供的原生方法,可以比较方便地实现滑动效果并且不影响内部元素的单击事件。缺点:只能滑动View的内容,并不能滑动View本身。
2、动画
:如果是Android3.0以上并采用属性动画,那么这种方式没有明显的缺点;如果是使用View动画或者在Android3.0以下使用属性动画,均不能改变View本身的属性。如果动画元素不需要响应用户的交互,那么可以用动画来做滑动,否则不太适合。一些复杂的效果必须通过动画才能实现。
3、改变布局
:使用起来麻烦些,没有明显的缺点。适用于一些具有交互性的View。
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取当前手指的坐标
//注意不能使用getX/Y,因为这是全屏滑动,所以需要获取当前点击事件在屏幕中的坐标而不是相对于View本身的坐标
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_MOVE: {
//获得位移,这样才可以移动View
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
int translationX = (int)ViewHelper.getTranslationX(this) + deltaX;
int translationY = (int)ViewHelper.getTranslationY(this) + deltaY;
//ViewHelper提供的动画效果
ViewHelper.setTranslationX(this, translationX);
ViewHelper.setTranslationY(this, translationY);
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
Scroller scroller = new Scroller(mContext);
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - srollX;
//1000ms内滑向destX,效果是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
//由下面代码可知startScroll只是单纯的保存了参数
//invalidarte方法会导致View重绘,然后View中的draw方法(后面会提到)会调用computeScroll方法
invalidarte();
}
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;
}
//这个方法原本是空的,以下是为了实现弹性滑动而写的
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
//向Scroller获取当前的scrollX/Y,通过scrollTo实现滑动
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//使用这个方法进行第二次重绘,再调用computeScroll,一直反复到滑动过程结束
postInvalidate();
}
}
再看一下computeScrollOffset的实现
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
//由这个关键词可知这个方法是根据时间流逝来计算当前scrollX/Y的值
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;
}
//让一个view在100ms内向右滑动100像素
ObjectAnimatior.ofFloat(targetView,"translationX",0,100).setDuration(100).start;
同时我们还可以利用动画的特性来实现一些动画不能实现的效果
下面是模仿Scroller来实现View的弹性滑动
final int starX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimation.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimationUpdateListener({
@Override
public void onAnimationUpdate(ValueAnimation animator){
//获取动画的每一帧和之前的Scroller类似
float faction = animator.getAnimatedFraction();
mButton1.scrollTo(startX + (int) (deltaX * fraction),0);
}
});
animator.start();
使用这种方法还可以实现其他动画效果,只需要在onAnimationUpdate方法中加上我们需要的操作
核心思想是通过发送一系列延时消息而达到的渐进式效果
Handler
或者View的postDelayed
方法/线程的sleep方法下面的代码是大约1000ms(因为线程调度不会很精确稳定)内将View的内容向左移动100像素
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELAYED_TIME = 33;
private Button mButton1;
private View mButton2;
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);
mButton1.scrollTo(scrollX, 0);
mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO, DELAYED_TIME);
}
break;
}
default:
break;
}
};
};
Android :安卓学习笔记之 事件分发机制 的简单理解和使用
一文解决Android View滑动冲突
常见滑动冲突场景可以简单分为如下三种:
常见的是ViewPager和Fragment结合实现页面左右滑动效果
然而每个页面中又有一个ListView上下滑动,因为ViewPager内部处理了这种滑动冲突,所以不会出现问题
如果我们使用的事ScrollView而不是ViewPager,那就必须手动处理冲突,否则只有其中一层可以滑动
即父View根据需要对事件进行拦截。逻辑处理放在父View的onInterceptTouchEvent
方法中。我们只需要重写父View的onInterceptTouchEvent方法
,并根据逻辑需要做相应的拦截即可。
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (满足父容器的拦截要求) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
即父View不拦截任何事件,所有事件都传递给子View,子View根据需要决定是自己消费事件还是给父View处理。这需要子View使用requestDisallowInterceptTouchEvent
方法才能正常工作。下面是子View的dispatchTouchEvent
方法的伪代码:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
父View需要重写onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
1、Android开发艺术探索——View的事件体系
2、Android开发艺术探索之View的事件体系