View
Android中控件大致被分为两类ViewGroup,View。ViewGroup作为容器管理View。Android视图,是类似于Dom树的架构。父视图负责测量定位绘制等操作。
View的位置参数
View的位置主要是由它的四个顶点来决定的。分别对应View的属性:left、top、right、bottom。需要注意的是这个是相对View的父容器来说的。View的坐标和父容器的关系如下图:在Android中x轴和y轴的正方向分别为右和下。
所以View的宽度和高度分别为:
int width=right-left;
int height=bottom-top;
我们也可以在代码中获取到View的left、top、right、bottom属性:
int left=view.getLeft();
int right=view.getRight();
int top=view.getTop();
int bottom=view.getBottom();
**x:**View左上角横坐标
**Y:**View左上角纵坐标
**translationX:**View左上角相对于父容器的X轴偏移量
**translationY:**View左上角相对于父容器的Y轴偏移量
x=left+translationX
y=top+translationY
MotionEvent和TouchSlop
1.MotionEvent(手指触摸屏幕事件)类型:
case MotionEvent.ACTION_DOWN://手指刚接触到屏幕
break;
case MotionEvent.ACTION_MOVE://手指在屏幕上滑动
break;
case MotionEvent.ACTION_UP://手指离开屏幕
break;
获取到点击位置的坐标:
/**相对于父容器的左上角的x、y坐标*/
int xParent= (int) event.getX();
int yParent= (int) event.getY();
/**相对应手机屏幕左上角的x、y坐标*/
int xScreen= (int) event.getRawX();
int yScreen= (int) event.getRawY();
2、TouchSlop:系统默认最小滑动距离
代码获取:
ViewConfiguration.get(this).getScaledTouchSlop();
VelocityTracker、GestureDetector、Scroller
1、VelocityTracker (手指在屏幕上滑动的速度)如何获取:
@Override
public boolean onTouchEvent(MotionEvent event) {
/**step1:为事件添加速度追踪*/
VelocityTracker velocityTracker=VelocityTracker.obtain();
velocityTracker.addMovement(event);
/**step2:获取到滑动速度*/
velocityTracker.computeCurrentVelocity(1000);//这里是指1000毫秒
int xVelocity= (int) velocityTracker.getXVelocity();//计算的水平方向的速度
int yVelocity= (int) velocityTracker.getYVelocity();//计算竖直方向的速度
/**step3:当我们不需要计算的时候,进行回收内存*/
velocityTracker.clear();
velocityTracker.recycle();
return super.onTouchEvent(event);
}
这里需要注意的是,我们从x轴正方向向反方向滑动时(从右向左),获取到的速度值为负值。
2、GestureDetector(手势检测)
手势检测, 是运动检测的高级形式, 自定义多种形式, 在回调中直接使用, 非常简单. 常见的9种手势, OnGestureListener包含6种, 即onDown(手指轻触屏幕), onShowPress(指轻触屏幕, 尚未松开), onSingleTapUp(单击屏幕), onScroll(手指拖动), onLongPress(长按), onFling(轻滑); OnDoubleTapListener双击包含3种, 即onSingleTapConfirmed(严格的单击行为), onDoubleTap(双击), onDoubleTapEvent(发生双击行为);
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gestureDetector=new GestureDetector(this, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent motionEvent) {
Log.e(TAG, "手指碰到屏幕屏幕");
return false;
}
@Override
public void onShowPress(MotionEvent motionEvent) {
Log.e(TAG, "手指轻触屏幕, 尚未松开");
}
@Override
public boolean onSingleTapUp(MotionEvent motionEvent) {
Log.e(TAG, "单击");
return false;
}
@Override
public boolean onScroll(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
Log.e(TAG, "手指滑动");
return false;
}
@Override
public void onLongPress(MotionEvent motionEvent) {
Log.e(TAG, "长按");
}
@Override
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
Log.e(TAG, "轻轻滑动");
return false;
}
});
gestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener(){
@Override
public boolean onSingleTapConfirmed(MotionEvent motionEvent) {
Log.e(TAG, "严格的单击行为");
return false;
}
@Override
public boolean onDoubleTap(MotionEvent motionEvent) {
Log.e(TAG, "双击");
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent motionEvent) {
Log.e(TAG, "发生双击行为");
return false;
}
});
}
/**需要在ontouchEvent方法中添加手势监听*/
@Override
public boolean onTouchEvent(MotionEvent event) {
gestureDetector.onTouchEvent(event);
return super.onTouchEvent(event);
}
3、Scroller(弹性滑动对象,用于实现View的滑动)
推荐一篇超详细的文章
View的滑动
1、使用scrollTo和scrollB
View提供了专门的方法scrollTo和scrollB来实现滑动的功能。如下:
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;//赋值新的x偏移量
mScrollY = y;//赋值新的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=getScrollX();
mScrollY=getScrollY();
scrollBy和scrollTo只能改变View内容的位置,而不能改变View在布局中的位置。
比如向右移动了100像素,移动的是View的内容,而View本身的位置是不动的。mScrollX和mScrollY的单位为像素。
2、使用动画
使用动画来使我们的View进行平移,平移就是一种滑动。我们可以采用传统的View动画,也可以采用属性动画(注意:属性动画为3.0版本后的API)。
xml文件实现动画:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal"
>
<translate
android:fromXDelta="0"
android:fromYDelta="0"
android:interpolator="@android:anim/linear_interpolator"
android:duration="100"
android:toXDelta="100"
android:toYDelta="100"
/>
</set>
使用属性动画:
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();
注意:View动画是对View的影响做操作,并不能真正改变View的位置参数,包括宽/高;如果需要保存动画后的状态,需要将fillafer属性设置为true,否则动画完成后,恢复符原始状态。使用属性动画则不会存在此问题。
3.改变布局参数(即改变LayoutParams)
比如我们想把一个View向右移动100px,只需要设置LayoutParams里的marginLeft参数的值增加100px即可。
/**this指的是当前的View对象*/
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) this.getLayoutParams();
params.width += 100;//宽度增加100px
params.leftMargin+=100;//同时设置一个距离左边的距离为100px
this.requestLayout();
4.各种滑动方式的对比
- scrollTo/scrollBy:操作简单,适合对View内容的滑动。
- 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果。
- 改变布局参数:操作稍微复杂,适用于有交互的View。
弹性滑动
1、使用Scroller
TouchEvent的ACTION_UP事件中,用户滑动速度很快,但是滑动距离又不足以“翻页”的时候,通过scroller来帮助用户scrollBy掉滑动一页还需要的dx或dy。
2、使用动画
当用户滑动到最上端或最下端时,我们仍然允许用户继续滑动,但是一旦松手,就把页面弹回到最上端和最下端的位置,用IOS的用户都知道IOS几乎所有页面都有这个弹性效果,用户体验非常好,其实我们用scroller也能轻松实现。
3、使用延时策略
第三种场景和第一种类似,大家依然可以参考上面发的那篇仿网易的blog,现在不是翻页,当用户滑动的加速度很大的时候,我们认为用户需要滑动的距离肯定是不只他手从放下到松开的那段距离的,所以这种情况下我们需要通过scroller帮助用户去多滑一段,这个距离具体设置一般需要交互给出,设置的是滑加速度的十分之一,感觉还是太少,各位可以自行设
置。
View的事件分发机制
首先要了解下与事件分发相关的方法:
onTouchEvent
事件处理,
dispatchTouchEvent
事件分发
onInterceptTouchEvent
事件拦截(针对ViewGroup来说)
我们来写一个小例子:假设你的公司,有个总经理,级别下面又一个部长,级别次之,最底层就是干活的你,没有级别。现在呢,董事会交给总经理一项任务,总经理又将这个任务布置给了部长,部长又把任务安排给了你。当你好不容易干完了活,就把任务成果交给了部长,部长觉得任务完成的不错,于是就签了自己的名字交给了总经理,总经理看了也觉得不错,就签了名字交给了董事会,这样一个任务就顺利的完成了。
一个总经理—MyViewGroupA,最外层的ViewGroup.
一个部长—MyViewGroupB,中间层的ViewGroup。
一个干活的你—MyView,底层劳苦大众。
我们新建MyViewGroupA类,代码如下:
public class MyViewGroupA extends LinearLayout{
public static final String TAG="MyViewGroupA";
public MyViewGroupA(Context context) {
super(context);
}
public MyViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Lg.e(TAG+"----onTouchEvent---");
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Lg.e(TAG+"----dispatchTouchEvent---");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Lg.e(TAG+"----onInterceptTouchEvent---");
return super.onInterceptTouchEvent(ev);
}
}
然后再新建一个同样的MyViewGroupB类,与MyViewGroupA基本相同,然后新建一个MyView类,这里需要注意一下,对于View来说,是没有onInterceptTouchEvent(事件拦截)的方法的。代码如下:
public class MyView extends View{
public static final String TAG="MyView";
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Lg.e(TAG+"----onTouchEvent---");
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Lg.e(TAG+"----dispatchTouchEvent---");
return super.dispatchTouchEvent(event);
}
}
布局界面如下:
<?xml version="1.0" encoding="utf-8"?>
<com.example.administrator.tansuo3_view.view.MyViewGroupA xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"
android:gravity="center"
android:id="@+id/tc_a"
android:orientation="vertical">
<com.example.administrator.tansuo3_view.view.MyViewGroupB
android:layout_width="300dp"
android:layout_height="350dp"
android:background="#ffff00"
android:id="@+id/tc_b"
android:gravity="center">
<com.example.administrator.tansuo3_view.view.MyView
android:layout_width="200dp"
android:layout_height="220dp"
android:id="@+id/tc_childs"
android:background="#ffffff"/>
</com.example.administrator.tansuo3_view.view.MyViewGroupB>
</com.example.administrator.tansuo3_view.view.MyViewGroupA>
显示界面如图:
我们对中间的白色块(MyView)进行点击,显示结果如下:
我们来分析一下:这里我们的三个View都没有对事件进行拦截和处理。
当我们点击了MyView,首先是由最外层的MyViewGroupA进行事件分发,
MyViewGroupA(总经理)–>MyViewGroupB(部长)–>MyView(你)当你(MyView)接收到事件以后,需要对事件作出处理
MyView(你)–>MyViewGroupB(部长)–>MyViewGroupA(总经理)事件拦截:onInterceptTouchEvent,我们看到MyViewGroupA、MyViewGroupB都没有对事件进行拦截,所以我们能将事件一路下发的MyView
事件传递的返回值:True,拦截,不继续;false,不拦截,继续流程。
事件处理的返回值也是类似:True,处理了,不用审核。False,没处理,给上级进行处理。
假设:总经理发现这个任务太简单了,自己就可以完成!于是就通过onInterceptTouchEvent方法拦截了事件,即让onInterceptTouchEvent()的返回值为True;
总经理把活都干了,其他人就不用干活了。
View的滑动冲突
冲突的三种情况:
1、外部滑动方向和内部滑动方向不一致;
一般来说出现这种情况的主要是Viewpager+Fragment组合使用的页面滑动效果(左右滑动)。而Fragment中又存在一个ListView或者ScrollView(上下滑动);
ListView与Viewpager+Fragment组合:不会造成滑动冲突,Viewpager内部已经做好了处理。
ScrollView与Viewpager+Fragment组合:会造成冲突,需要手动处理。
2、外部滑动方向和内部滑动方向一致;
当父控件和子控件在同一个方向上进行滑动的时候,造成冲突。比如:ScrollView嵌套ListView
3、上面两种情况混合嵌套。
例如:主界面组成:Activity中有:ViewPager+Fragment(左右滑动)、SlideMenu(左右滑动效果)、Fragment中含有ScrollView(上下滑动效果)
滑动冲突的处理规则:
场景一处理:
上下滑动:内部View拦截处理。
左右滑动:外部View拦截处理。
判断左右滑动或者上下滑动:判断滑动初始位置坐标点和滑动结束位置坐标点,通过判断水平方向移动距离大小和竖直方向移动距离大小的对比进行判断。水平位移大,则为水平方向滑动;竖直位移大,则为竖直方向滑动。
事实上,第一种情况使用的Viewpager已经为我们解决了嵌套ScrollView和ListView所产生的滑动冲突问题。
我们来参考源码做一下:
(1)外部拦截法:重写父控件onInterceptTouchEvent()方法对相应事件作出拦截。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept=false;//是否进行拦截
int x= (int) ev.getX();
int y= (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
isIntercept=false;
break;
case MotionEvent.ACTION_MOVE:
int delX=Math.abs(x-lastX);
int delY=Math.abs(y-lastY);
int min= ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (delX>min&&delX>delY){//如果在x轴方向上移动的距离,大于Y方向移动的距离
isIntercept=true;
}else {
isIntercept=false;
}
break;
case MotionEvent.ACTION_UP:
isIntercept=false;
break;
}
lastX=x;
lastY=y;
return isIntercept;
}
我们重写了Viewpager的onInterceptTouchEvent方法,判断起始位置和目标位置所产生的水平位移差和垂直位移差进行比较。水平位移差大,则进行拦截,否则交给子View进行处理。
(2)内部拦截法:重写子View的dispatchTouchEvent()消耗掉事件。
内部拦截法是指父容器不拦截任何事件,所有事件均传递给子元素,如果子元素需要此事件就直接消耗掉。这个需要配合requestDisallowInterceptTouchEvent方法才能正常工作,比起外部拦截法较为复杂。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x= (int) ev.getX();
int y= (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int delX=Math.abs(x-lastX);
int delY=Math.abs(y-lastY);
if (如果父容器需要此类事件){
getParent().requestDisallowInterceptTouchEvent(false);
}else {
}
break;
case MotionEvent.ACTION_UP:
break;
}
lastX=x;
lastY=y;
return super.dispatchTouchEvent(ev);
}