一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
1. View与ViewGroup
View是所有控件的基类,ViewGroup是容纳这些控件的容器,它包含很多View以及ViewGroup。需要注意的是ViewGroup也是继承自View的。
2. Android中的坐标系
Android中有两种坐标系,分别为屏幕坐标系和View坐标系。
2.1 屏幕坐标系
在Android中,将左上角的顶点作为Android坐标系的原点,原点向右为X轴的正方向,向下为Y轴的正方向。在触摸事件中,使用getRawX()和getRawY()获得的坐标便是Android坐标系的坐标。
2.2 View坐标系
以view的左上角为原点,水平向右为x轴正方向,竖直向下为y轴正方向。
由上图可以看出
2.2.1. 获取View自身的宽高
int wdith = getRight() - getLeft();
int height = getBottom() - getTop();
Android系统也提供了getWidth()和getHeight()获取View的宽高,其原理也是一致的。部分源码为
//获取View的宽度
public final int getWidth() {
return mRight - mLeft;
}
//获取View的高度
public final int getHeight() {
return mBottom - mTop;
}
2.2.2. 获取View距离父控件(ViewGroup)的距离
- getTop() 获取View自身顶边到父控件顶边的距离。
- getRight() 获取View自身右边到父控件左边的距离。
- getBottom() 获取View自身底边到父控件顶边的距离。
- getLeft() 获取View自身左边到父控件左边的距离。
2.2.3.MotionEvent提供的方法
当View或者ViewGroup获取点击事件的时候,最后都由onTouchEvent(MotionEvent e)方法来处理,MotionEvent提供了获取坐标的方法
- getX() 获取点击位置距离View左边的距离。
- getY() 获取点击位置距离View顶边的距离。
- getRawX() 获取点击位置距离屏幕左边的距离。
- getRawY() 获取点击位置距离屏幕顶边的距离。
3. View的滑动
3.1 layout()方法
View进行绘制的时候会调用onLayout()方法设置显示的位置。因此可以通过修改View的left,right,top,bottom四个属性来控制View的位置。
- 首先在手指触摸的时候记录触摸点的坐标。
- 然后在手指移动的时候重新获取触摸点的坐标,并计算与按下坐标时的偏移量。
- 最后调用layout()方法重新布局,从而达到移动View的效果。
关键代码如下
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取按下的坐标
downX = (int) event.getX();
downY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
//获取移动的偏移量
int offsetX = (int) (event.getX() - downX);
int offsetY = (int) (event.getY() - downY);
//调用layout重新布局
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
}
return true;
}
3.2 offsetLeftAndRight()方法与offsetTopAndBottom()方法。
offsetLeftAndRight()和offsetTopAndBottom()与layout()方法类似,简化了设置方式。 关键代码如下
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取按下的坐标
downX = (int) event.getX();
downY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
//获取移动的偏移量
int offsetX = (int) (event.getX() - downX);
int offsetY = (int) (event.getY() - downY);
//调用offsetxxx()重新布局
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
}
return true;
}
3.3 改变布局参数LayoutParams
LayoutParams保存了View的布局参数,所以可以通过改变LayoutParams去改变View的位置。
- 先通过getLayoutParams()方法获取布局参数。
- 再根据父布局类型强转为需要的类型
- 计算偏移量
- 重新设置LayoutParams参数
关键代码如下
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取按下的坐标
downX = (int) event.getX();
downY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
//获取移动的偏移量
int offsetX = (int) (event.getX() - downX);
int offsetY = (int) (event.getY() - downY);
//此处父布局是LinearLayout,所以强转为LinearLayout.LayoutParams
//LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams();
//params.leftMargin = getLeft() + offsetX;
//params.topMargin = getTop() + offsetY;
//如果不想考虑父布局的类型,还可以使用ViewGroup.MarginLayoutParams来进行设置
ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
marginLayoutParams.leftMargin = getLeft() + offsetX;
marginLayoutParams.topMargin = getTop() + offsetY;
setLayoutParams(marginLayoutParams);
break;
}
return true;
}
3.4 动画
通过平移动画也可以来移动View
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<translate
android:duration="2000"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="400"
android:toYDelta="400" />
</set>
Animation animation = AnimationUtils.loadAnimation(this, R.anim.trans);
offetView.setAnimation(animation);
不过View动画仅仅执行了动画,并没有真正改变View的位置参数。所以当View动画执行完成停留在当前位置时,点击此View,并不会触发点击事件,只有点击其原位置才会触发点击事件。为了解决这个问题,Android在3.0引入了属性动画,他不仅可以执行动画,还可以改变View的位置参数,使用也更加简单。
ObjectAnimator.ofFloat(offetView, "translationX", 0, 300)//X轴向右平移300px
.setDuration(1000)//动画执行时间
.start();
3.5 scrollBy和scrollTo
scrollBy(dx,dy)表示移动的增量是dx,dy;scrollTo(x,y)表示移动到(x,y)这个坐标点。scrollBy也是调用scrollTo实现的。scrollBy源码如下
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy和scrollTo可以移动View和ViewGroup,如果移动的对象是View,则移动的是他的内容,比如TextView中文字的位置,如果移动的对象是ViewGroup,则是移动他的所有子孩子。 自定义一个View,继承TextView,重写onTouchEvent方法,关键代码如下
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//获取按下的坐标
downX = (int) event.getX();
downY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
//获取移动的偏移量
int offsetX = (int) (event.getX() - downX);
int offsetY = (int) (event.getY() - downY);
scrollTo(-offsetX,-offsetY);
break;
}
return true;
}
需要注意的是此时的坐标方向与平常是相反,所以设置成了偏移量的相反数。
3.6 Scroller
scrollBy和scrollTo在滑动时,整个过程是瞬间完成的,而使用Scroller则可以实现有过度效果的滑动。Scroller是一个辅助类,本身不能实现View的滑动,需要和computeScroll()配合使用。
- 初始化Scroller,Scroller构造函数可以指定插值器,此处指定为线性插值器,整个动画更加平滑。
public ScrollerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
scroller = new Scroller(context,new LinearInterpolator());
}
- 重写computeScroll()方法,系统在绘制View的时候在调用draw()的时候会调用此方法,此处通过Scroller不断获取当前的滚动坐标,并通过scrollTo()方法移动的此位置。并且调用invalidate()方法重绘,进而不断调用computeScroll()方法,直至动画完成,即scroller.computeScrollOffset()返回false。
public void computeScroll() {
super.computeScroll();
if(scroller.computeScrollOffset()){//判断scroller的移动动画是否完成(true没有)
((View)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
invalidate();
}
}
- 调用startScroll(int startX, int startY, int dx, int dy, int duration)方法开始滑动。
- startX表示当前视图的x坐标值
- startY表示当前视图的y坐标值
- dx表示在当前视图的x坐标基础上横向移动的距离
- dy表示在当前视图的y坐标基础上纵向移动的距离
- duration表示视图移动的操作在多少时间内执行完场,也就是动画的持续时间(单位:毫秒)
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
scroller.startScroll(0,0,-200,-1000,2000);
invalidate();
break;
}
return true;
}