View是Android所有控件的基类;View是一种界面层的控件的一种抽象;ViewGroup是控件组,继承自View。
View的位置主要由它的四个定点来决定,分别对应View的四个属性:top、left、right、bottom,这下坐标都是相对父容器而言的。从3.0开始View增加了x、y、translationX、translationY;x和y是View左上角的坐标,translationX和translationY是View左上方相对父容器的偏移量。
x = left + translationX;
y = top + translationY;
View平移的过程中,top和left表示的是原始左上角的位置信息;其值并不会改变,此时发生改变的是x、y、translationX和translationY。
x和left不同体现在:left是View的初始坐标,在绘制完毕后就不会再改变;而x是View偏移后的实时坐标,是实际坐标。y和top的区别同理。
MotionEvent是指用户手指触摸屏幕产生的一系列事件。
4 点击屏幕后松开,事件序列 DOWN->UP点击屏幕滑动一会再松开,事件序列为 DOWN->MOVE->…->MOVE->UP。
getX/getY获取相对当前View左上角的x和y坐标;getRawX/getRawY获取相对手机屏幕左上角的x和y坐标。
TouchSlop是系统能识别滑动的最小距离,是系统常量,当手指在屏幕上滑动,小于这个距离,系统不认为你在进行滑动操作;可通过ViewConfiguration.get(getContext()).getScaledTouchSlop()方法来获取;
VelocityTracker用于追踪手指在滑动过程中的速度。使用前在View的onTouchEvent方法中追踪当前单击事件的速度。
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//想知道滑动速度时
//获取速度前需计算速度 参数 时间间隔 单位ms
velocityTracker.computeCurrentVelocity(1000);
//获取速度
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
速度 = (终点位置 - 起点位置)/ 时间段
scrollTo和scrollBy只能改变View内容而不能改变View本身的位置。scrollBy内部也是调用了scrollTo,它是基于当前位置的相对滑动,scrollTo是基于所传递参数的绝对滑动,因此如果利用相同输入参数多次调用scrollTo()方法则只会出现一次View滑动的效果而不是多次。
mScrollX:View的左边缘减去View的内容的左边缘;mScrollY:View的上边缘减去View的内容的上边缘。从右向左滑动,mScrollX为正值,反之为负值;从下往上滑动,mScrollY为正值,反之为负值。
使用动画来移动View,主要是操作View的translationX和translationY属性。需要注意的是,View动画只是对View的影像做操作,它并不能真正改变View的位置参数,如果这个View设置了点击事件,点击动画后的新位置无法触发点击事件的,使用属性动画没有此问题,但3.0之前系统无属性动画。
改变布局参数实现滑动,即改变LayoutParams,如想将一个View右平移100px,只需要将该View的LayoutParams里的marginLeft增加100px即可
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mBtn.getLayoutParams();
params.leftMargin += 100;
mBtn.requestLayout();
//或者mBtn.setLayoutParams(params);
总结:
Scroller不能直接完成View滑动,需要配合View的computeScroll方法才可以完成弹性滑动,它让View不断重绘,每一次重绘有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前滑动的位置,知道了滑动位置就通过scrollTo来完成滑动。每一次滑动都会导致View小幅度滑动,多次小幅度滑动组成了弹性滑动,这就是Scroller的工作机制。
startScroll()保存了我们传递的几个参数 ——> invalidate()会导致View重绘 ——> draw() ——> computeScroll()该方法为空实现,我们内部调用scrollTo(x,y)实现滑动 和 postInvalidate() 继续重绘,反复下去完成弹性滑动。
Scroller则有过渡滑动的效果,能实现弹性滑动;而scrollTo/scrollBy过程是瞬间完成的,是非平滑的滑动
通过动画可以直接实现弹性滑动
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(1000).start();//在1000ms让targetView向右平移100像素
使用延时策略完成滑动,核心思想 就是通过发送一系列延时消息从而达到一种渐进式的效果。用Handler或View的postDelayed方法,postDelayed发送延时消息,然后消息中进行View滑动,接连不断的发送这种延时消息,达到弹性滑动的效果。也可以使用线程的sleep方法来实现。
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
对于一个根ViewGroup,点击事件产生后,首先会传递给它,这时他的dispatchTouchEvent会调用,
如果它的onInterceptTouchEvent返回true表示要拦截当前事件,接下来事件会交给这个ViewGroup处理,它的onTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent返回false,则事件会继续传递给子元素,子元素的dispatchTouchEvent会调用,如此反复直到事件被处理。
当一个View需要处理事件时,如果设置了OnTouchListener,那么OnTouchListener的onTouch方法会回调,如果onTouch返回false,则当前View的onTouchEvent方法会被调用;如果返回true,那么onTouchEvent方法将不会调用。由此可见,OnTouchListener优先级高于onTouchEvent。OnClickListener优先级处在事件传递的尾端。
一个点击事件产生后,传递顺序:Activity->Window->View;如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent会被调用,以此类推,所有元素都不处理该事件,最终将传递给Activity处理,即Activity的onTouchEvent会被调用。
同一个事件序列是指从手指触摸屏幕那一刻开始,到手指离开屏幕那一刻(down->move…move->up)
一个事件序列只能被一个View拦截且消耗,同一个事件序列所有事件都会直接交给它处理,并且它的onInterceptTouchEvent不会再被调用。
某个View一旦开始处理事件,如果它不消耗ACTION_DOWN(onTouchEvent返回了false),那么同一事件序列中其他事件都不会再交给它来处理,事件将重新交给他的父元素处理,即父元素的onTouchEvent会被调用。
如果某个View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以收到后续事件,最终这些消失的点击事件会传递给Activity处理
ViewGroup默认不拦截任何事件,ViewGroup的onInterceptTouchEvent方法默认返回false。
View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
View的onTouchEvent方法默认消耗事件(返回true),除非他是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认都为false,clickable属性分情况,Button默认为true,TextView默认为false。
onClick发生的前提是View可点击,并且它收到了down和up事件。
事件传递过程是由内而外,事件总是先传递给父元素,然后在由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素干预父元素的事件分发过程,但ACTION_DOWN事件除外。
1.常见滑动冲突场景
场景1 —— 外部滑动方向与内部滑动方向不一致,比如ViewPager中包含ListView;
场景2 —— 外部滑动方向与内部滑动方向一致,比如ScrollView中包含ListView;
场景3 —— 上面两种情况的嵌套
2.滑动冲突处理规则
通过判断是水平滑动还是竖直滑动来判断到底应该谁来拦截事件;可以根据水平和竖直两个方向的距离差或速度差来做判断
3.滑动冲突解决方式
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.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;
}
mLastXIntercept = x;
mLastYIntercept = x;
return intercepted;
子View的代码:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = x - mLastY;
if("父容器的点击事件"){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
注意:除了子元素需要处理之外,父元素默认也要拦截除ACTION_DOWN之外的其他事件,这样当子元素调用getParent().requestDisallowInterceptTouchEvent(true)方法时,父元素才能继续拦截所需要的事件。
父View的修改:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if(action == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}
一个例子:我们来实现一个类似于ViewPgaer中嵌套ListView的效果,为了制造滑动冲突,我们写一个类似ViewPager的控件即可,名字叫做HorizontalScrollViewEx,创建三个ListView并添加进HorizontalScrollViewEx,开始解决滑动冲突。
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
private HorizontalScrollViewEx mListContainer;
private int w,h;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i(TAG,"onCreate");
initView();
}
private void initView() {
LayoutInflater inflater = getLayoutInflater();
mListContainer = findViewById(R.id.container);
//屏幕宽高
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
w = wm.getDefaultDisplay().getWidth();
h = wm.getDefaultDisplay().getHeight();
for (int i = 0; i < 3; i++) {
ViewGroup layout = inflater.inflate(R.layout.content_layout,mListContainer,false);
layout.getLayoutParams().width = w;
TextView textview = (TextView) layout.findViewById(R.id.title);
textview.setText("page" + (i+1));
layout.setBackgroundColor(Color.rgb(255/(i+1),255/(i+1),0));
createList(layout);
mListContainer.addView(layout);
}
}
private void createList(ViewGroup layout) {
ListView listview = (ListView) layout.findViewById(R.id.list);
ArrayList<String>datas= new ArrayList<>();
for (int i = 0; i < 50; i++) {
datas.add("names" + i);
}
ArrayAdapter<String>adapter = new ArrayAdapter<String>(this,R.layout.content_list_item,R.id.name,datas);
listview.setAdapter(adapter);
}
}
外部拦截法:把父容器的拦截条件换成了实际的逻辑,在滑动过程中,当水平方向的距离大就判断水平滑动,为了能够水平滑动所以让父容器拦截事件,而竖直距离大于就不拦截,于是事件传递给了ListView,所以ListView能上下滑动,这就解决了冲突了。考虑到一种情况,如果此时用户正在水平滑动,但是水平滑动停止之前如果用户再迅速的进行竖直滑动,就会导致界面在水平滑动无法滑动到终点,而处于一种中间状态,为了避免这种不友好的体验,我们水平正在滑动的时候,下一个序列的点击仍然交给父容器。
public class HorizontalScrollViewEx extends ViewGroup {
public static final String TAG = "HorizontalScrollViewEx";
private int mChindrensize;
private int mChindrenWidth;
private int mChindrenIndex;
//分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
//分别记录上次滑动的坐标
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
....
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = true;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltax = x - mLastXIntercept;
int deltaY = y = mLastYIntercept;
if (Math.abs(deltax) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
@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 / mChindrenWidth;
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
if (Math.abs(xVelocity) >= 50) {
mChindrenIndex = xVelocity > 0 ? mChindrenIndex - 1 : mChindrenIndex + 1;
} else {
mChindrenIndex = (scrollX + mChindrenWidth / 2) / mChindrenWidth;
}
mChindrenIndex = Math.max(0, Math.min(mChindrenIndex, mChindrensize - 1));
int dx = mChindrenIndex * mChindrenWidth - scrollX;
ssmoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastX = x;
mLastY = y;
return true;
}
private void ssmoothScrollBy(int dx, int i) {
mScroller.startScroll(getScrollX(),0,dx,500);
invalidate();
}
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
}
内部拦截:只需要修改ListView的dispatchTouchEvent方法中的父容器的拦截逻辑,同时让父拦截MOVE和Up事件即可。为了重写Listview的dispatchTouchfvent方法,我们必须自定义一个ListView,称为ListViewEx,然后对内部拦截法的模板代码进行修改。
public class ListViewEx extends ListView {
public static final String TAG = "ListViewEx";
private HorizontalScrollViewEx mHorizontalScrollViewEx;
private int mLastX = 0;
private int mLastY = 0;
public ListViewEx(Context context) {
super(context);
}
...
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int delatX = x - mLastX;
int delatY = y - mLastY;
if (Math.abs(delatX) > Math.abs(delatY)) {
mHorizontalScrollViewEx.requestDisallowInterceptTouchEvent(false);
}
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
}
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
mLastX = x;
mLastY = y;
if(!mScroller.isFinished()){
mScroller.abortAnimation();
return true;
}
return false;
}else {
return true;
}
}
提供一个可以上下滑动的父容器,这里就叫 StickyLayout,它看起来就像是可以上下滑动的竖直的LinearLayout,然后在它的内部中的滑动冲突了。当然这个StickyLayout是有滑动规则的:当Header显示时或者ListView 滑动到顶部时,由StickyLayout拦截事件:当Header隐藏时,这要分情况,如果Listview已经滑动到顶部并且当前手势是向下滑动的话,这个时候还是StickyLayout拦截事件,其他情况则由ListView拦截事件。
外部拦截法:当事件落在Header上面时,就不会拦截该事件;接着,如果竖直距离差小于水平距离差,那么父容器也不会拦截该事件;当Header是展开状态并且是向上滑动时,父容器拦截事件;当ListView滑动到顶部并且向下滑动的时候,父容器也会拦截事件。
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (mDisallowInterceptTouchEventOnHeader && y <= getHeaderHeight()) {
intercepted = 0;
} else if (Math.abs(deltaY) <= Math.abs(deltaX)) {
intercepted = 0;
} else if (mStatus == STATUS_EXPANDED && deltaY <= -mTouchSlop) {
intercepted = 1;
} else if (mGiveUpTouchEventListener != null) {
if (mGiveUpTouchEventListener.giveUpTouchEvent(event) && deltaY >= mTouchSlop) {
intercepted = 1;
}
}