初探起因
一开始接触到坐标系相关的东西是使用ScrollView,因为ScrollView布局限制高度应该为wrap_content,所以项目中实际展示出来的布局效果和理想中的布局效果发了偏差。如果给布局内部的控件设置固定DP的话,又会有适配的问题的出现,所以就在java代码使用LayoutParams中对布局高度进行动态赋值,达到了自己想要的效果。
后来看书看到View这部分的时候,又看到了坐标系这一部分的知识,加上身边大佬的Github里也有研究,就打算简单先总结一下,毕竟这个东西搞懂了,自定义View的坐标相关以及自己想实现一些更灵活的效果的话,就更容易了。
流程步骤
这里大致分为以下几步:
1. 了解位置参数
2. 在程序里让控件可以随着我们的手指动起来
3. 对控件的移动范围给予边界的限制
4.让控件“听话”
1. 了解布局参数
首先我们需要了解一下坐标系的概念。这里我们选取了两个参照物,是为了更好的区别后面的参数。
这里有两个参照坐标系,相对图片控件来说。一个是根布局,使用黄色标注的布局的坐标系,左上角为坐标原点(0,0),横向为X轴,纵向为Y轴。另一个是父布局,使用了白色标注。同上。
我们在这里暂且称图片控件为子View,我们子View的位置是由四个顶点决定,分别对于四个属性:top,left,right,bottom。其中,top是左上角纵坐标,left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标。当然这些是相对于我们上面提到的父布局来说的。它是一种相对坐标。
通俗点来说的话,
getLeft()
是子View的左边相对于父布局的左边的距离,getTop()
是子View的上边相对于父布局的上边的距离,getRight()
是子View的右边相对于父布局的左边的距离,getBottom()
是子布局的下边相对于父布局的上边的距离。
上面已经说到了子View的四个参数属性,因为我们要让子View随我们的手指移动,所以在手指接触屏幕后,我们可以通过MotionEvent得到点击当时的x和y坐标,系统为我们提供了getX()
,getY()
,getRawX()
,getRawY()
这四个方法,前两个方法是触摸点相对于子View左上角原点的x和y坐标,后两个方法是相对于手机屏幕的x和y坐标。
要想我们的控件动起来,我们还需要了解两个View的位置参数,他们在View移动的时候会用到。这两个方法就是
getTranslationX()
和getTranslationY()
,他们是View相对自身move后的X和Y轴的偏移量。这两个值是相对来说的,相对你初始化布局时候View的位置,X轴方向向左为负值,向右为正值,Y轴方向向上为负值,向下为正值。好了,到这里,我们了解了十个参数属性:
getLeft()
getTop()
getRight()
getBottom()
getX()
getY()
getRawX()
getRawY()
getTranslationX()
getTranslationY()
那么接下来我们就在代码中使用它们来让我们的控件可以随手指移动。
2. 在程序里让控件可以随着我们的手指动起来
布局很简单,上面我有提到为了区分参数所以中间使用了一个父布局嵌套,当然大家也可以不用父布局,直接在根布局下写一个ImageView控件。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
tools:context=".MainActivity">
<RelativeLayout
android:id="@+id/rl_parent"
android:layout_width="300dp"
android:layout_height="500dp"
android:layout_centerInParent="true"
android:background="@color/text_bg">
<ImageView
android:id="@+id/iv_test_icon"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/head_icon" />
</RelativeLayout>
</RelativeLayout>
写完布局后,我们在AS中可以预览到是这个样子
然后我们在MainActivity中就可以开始写我们想要的效果啦。
因为我们想要控件随我们的手指触摸移动,所以在这里我们用到了setOnTouchListener
这个监听。上面我们也提到了getX()
和getY()
这两个方法是MotionEvent
调用得到的值。而MotionEvent
这个对象我们正好可以在setOnTouchListener
下的onTouch
方法里拿到这个对象。
说到onTouch
,我们还需要知道,关于触摸,我们会发生的状态有三种,按下,移动,抬起。相应的,在onTouch
方法里的MotionEvent
对象我们可以通过判断ACTION_DOWN
,ACTION_MOVE
,ACTION_UP
来决定我们的代码在哪里执行。
接下来,我们在ACTION_DOWN
的时候去获取到x和y的值,然后在ACTION_MOVE
的时候先计算我们的坐标移动的x和y的位移距离,然后再通过setTranslationX()
/setTranslationY()
去让控件跟着改变距离就可以让我们的控件动起来了!
ivTestIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
final int x = (int) motionEvent.getX();
final int y = (int) motionEvent.getY();
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int xOffset = x - mLastX;//得到X的位移距离
int yOffset = y - mLastY;//得到Y的位移距离
ivTestIcon.setTranslationX(ivTestIcon.getTranslationX() + xOffset);
ivTestIcon.setTranslationY(ivTestIcon.getTranslationY() + yOffset);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
});
这里需要注意一下的是onTouch
的返回值要改为true,因为false的话会拦截之后的动作,执行ACTION_DOWN
之后就不会再执行之后的动作了。
然后我们就可以看到下面的效果了
那么现在手指触摸控件是可以开始移动了,但是我们会发现一个问题,当我们的图片超出父布局的范围的部分就消失了,这样的体验很不好,所以,接下来我们给我们图片的规定一个移动范围,让它只能在我们的父布局内部移动。
3. 对控件的移动范围给予边界的限制
首先我们得获取我们子View的宽高和父布局的宽高(这里以我的布局为例子),目的是为了计算我们子View的活动范围来判断我们的边界限制条件。然后,我们用父布局的宽度减去子View的宽度就得到了它可以移动的X轴的范围,同理,我们得到它可以移动的Y轴的范围。然后因为我们相对父布局为坐标范围,所以我们设定最小范围就是0。
ivTestIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
final int x = (int) motionEvent.getX();
final int y = (int) motionEvent.getY();
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int xOffset = x - mLastX;//得到X的位移距离
int yOffset = y - mLastY;//得到Y的位移距离
final int oldTransX = (int) ivTestIcon.getTranslationX();
final int newTransX = oldTransX + xOffset;
final int transXLowerLimit = 0;
if (newTransX < transXLowerLimit) {
xOffset = transXLowerLimit - oldTransX;//X,left,边界判断
}
final int transXUpperLimit = parentW - childW;
if (newTransX > transXUpperLimit) {
xOffset = transXUpperLimit - oldTransX;//X,right,边界判断
}
final int oldTransY = (int) ivTestIcon.getTranslationY();
final int newTransY = oldTransY + yOffset;
final int transYLowerLimit = 0;
if (newTransY < transYLowerLimit) {
yOffset = transYLowerLimit - oldTransY;//Y,top,边界判断
}
final int transYUpperLimit = parentH - childH;
if (newTransY > transYUpperLimit) {
yOffset = transYUpperLimit - oldTransY;//Y,bottom,边界判断
}
ivTestIcon.setTranslationX(ivTestIcon.getTranslationX() + xOffset);
ivTestIcon.setTranslationY(ivTestIcon.getTranslationY() + yOffset);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
});
然后我们就可以看到我们去move我们的子View的时候,就不会有超出父布局的部分了。
4.让控件“听话”
这里实现的效果是,我们给我们的子View去再次设定更小的范围,让子View在我们ACTION_UP
的时候,自己置顶或者到底部。这里的子View自己移动的部分是使用了属性动画ObjectAnimator
,以后会写一个动画的总结博客来总结自己动画的使用。
final int transYLimit = parentH - childH;
final int transYHalfLimit = transYLimit / 2;
final float translationY = ivTestIcon.getTranslationY();
if (translationY == 0 || translationY == transYLimit) {
return;
}
if (translationY < transYHalfLimit) {
mTransAnimator.setDuration((long) (1 * translationY));
mTransAnimator.setFloatValues(translationY, 0);
} else {
mTransAnimator.setDuration((long) (1 * (transYLimit - translationY)));
mTransAnimator.setFloatValues(translationY, transYLimit);
}
mTransAnimator.start();
然后运行,我们的控件就可以听代码的话,自动置顶或者到底部了