Android :安卓学习笔记之 Android View 的基础知识和冲突事件处理
Posted JMW1407
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android :安卓学习笔记之 Android View 的基础知识和冲突事件处理相关的知识,希望对你有一定的参考价值。
android View的简单理解和使用
Android View
1、View的基础知识
1.1 什么是View
在Android中,什么是View?
- View是Android中所有控件的基类,不管是简单的TextView,Button还是复杂的LinearLayout和ListView,它们的共同基类都是View;
- View是一种界面层的控件的一种抽象,它代表了一个控件,除了View还有ViewGroup,从名字来看ViewGroup可以翻译为控件组,即一组View;
- 在Android中,ViewGroup也继承了View,
这就意味着View可以是单个控件,也可以是由多个控件组成的一组控件
1.2 View的位置参数
View和位置主要由它的四个顶点来决定,分别对应View的四个属性:top、left、right、bottom,
- 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,
- xy是View左上角的坐标
translationX
和translationY
是View左上角相对于父容器的偏移量。
这几个参数也是相对于父容器的坐标;这几个参数的换算关系如下:
x = left + translationX;
y = top + translationY;
1.3 MotionEvent和TouchSlop
1.3.1. MotionEvent
此处需要特别说明:事件列,即指从手指接触屏幕至手指离开屏幕这个过程产生的一系列事件。一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。
1.3.2.TouchSlop
TouchSlop是系统所能识别出的被认为是滑动的最小距离
- 当手指在屏幕上滑动的时候,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。
- 这是一个常量,和设备有关,在不同设备上这个值可能是不同的
通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获取这个常量
在源码中还可以找到这个常量的定义
这个常量定义在frameworks/base/core/res/res/values/config.xml文件中,"config_viewConfigurationTouchSlop"对应的就是这个常量的定义。
1.4 VelocityTracker、GestureDetector和Scroller
1.4.1 VelocityTracker:速度追踪
用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。在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。
- 速度可能为负数,当手指从右向左滑动时,产生的速度就是负数,如果时间间隔是100ms,100ms内从左向右滑过100像素,那么速度就是100/0.1s = 1000像素。
速度 = (终点位置 - 起点位置)/ 时间段 ;
当不使用它的时候,需要调用clear方法来重置并回收内存:
velocityTracker.clear();
velocityTracker.recycle();
1.4.2 GestureDetector:手势检测
用于辅助检测用户的单击,滑动,长按,双击等行为。
GestureDetector
的使用:
- 首先需要创建一个
GestureDetector
对象并继承OnGestureListener
和OnDoubleTapListener
接口
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法手动的问题
mGestureDetector.setIsLongpressEnabled(false);
- 接管View的
onTouchEvent
方法,在待监听的View的onTouchEvent
方法中添加如下实现:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
然后我们就可以有选择的实现这两个接口中的方法了:
在实际开发中,如果只是监听滑动相关的,建议在onTouchEvent中实现,如果是监听双击这种行为,使用GestureDetector。
1.4.3 Scroller:弹性滑动对象
弹性滑动对象,用于实现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();
2、View的滑动
在Android设备上,滑动几乎是应用的标配,通过三种方法可以实现View的滑动:
- 第一种通过View本身提供的
ScrollTo/ScrollBy
方法来实现滑动; - 第二种通过
动画
给View
施加平移效果来实现滑动; - 第三种通过改变View的
LayoutParams
使得View重新布局从而实现滑动。
2.1 使用scrollTo/scrollBy
/**
* 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上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。
- 换句话说,如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。
2.2 使用动画
使用动画,主要就是操作View的translationX
和translationY
属性,既可以采用View动画,也可以采用属性动画。
1.传统的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的位置参数,包括宽/高
2.采用属性动画
//按顺序对应括号内的参数(目标view,水平移动,0位置,到100位置,100ms内移动)
ObjectAnimation.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
View动画是对View的内容做操作,它并不能真正改变View的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后其动画结果会消失,View会瞬间恢复到动画前的状态。使用属性动画不会存在上述问题。
2.3 改变布局参数
即改变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);
2.4 各种滑动方式的对比
1、scrollTo/scrollBy
:View提供的原生方法,可以比较方便地实现滑动效果并且不影响内部元素的单击事件。缺点:只能滑动View的内容,并不能滑动View本身。
2、动画
:如果是Android3.0以上并采用属性动画,那么这种方式没有明显的缺点;如果是使用View动画或者在Android3.0以下使用属性动画,均不能改变View本身的属性。如果动画元素不需要响应用户的交互,那么可以用动画来做滑动,否则不太适合。一些复杂的效果必须通过动画才能实现。
3、改变布局
:使用起来麻烦些,没有明显的缺点。适用于一些具有交互性的View。
2.4 .1 一个全屏滑动的例子
@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;
3 弹性滑动
3.1 使用Scroller
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;
3.2 通过动画
//让一个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方法中加上我们需要的操作
3.3 使用延时策略
核心思想是通过发送一系列延时消息而达到的渐进式效果
- 使用
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;
;
;
4 View的事件分发机制
Android :安卓学习笔记之 事件分发机制 的简单理解和使用
5 View的滑动冲突
5.1 常见的滑动冲突场景
常见滑动冲突场景可以简单分为如下三种:
- 场景1–外部滑动方向和内部滑动方向不一致
常见的是ViewPager和Fragment结合实现页面左右滑动效果
然而每个页面中又有一个ListView上下滑动,因为ViewPager内部处理了这种滑动冲突,所以不会出现问题
如果我们使用的事ScrollView而不是ViewPager,那就必须手动处理冲突,否则只有其中一层可以滑动
- 场景2–外部滑动方向和内部滑动方向一致
- 系统无法知道用户到底想让哪一层滑动,要么只有一层滑动,要么两个都滑动的很缓慢
- 场景3–上面两种情况的嵌套
- 几乎就只是单一的滑动冲突的叠加,因此只需要分别处理内层、中层和外层之间的滑动冲突即可
- 几乎就只是单一的滑动冲突的叠加,因此只需要分别处理内层、中层和外层之间的滑动冲突即可
5.2 滑动冲突的处理规则
- 对于场景1,根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。
- 对于场景2,根据业务规则来决定由谁拦截事件。
- 对于场景3,根据业务规则来决定由谁拦截事件。
5.3 滑动冲突的解决方式
5.3.1.外部拦截法
即父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;
5.3.2.内部拦截法
即父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以上是关于Android :安卓学习笔记之 Android View 的基础知识和冲突事件处理的主要内容,如果未能解决你的问题,请参考以下文章
Android :安卓学习笔记之 Android View 的基础知识和冲突事件处理