1. View的位置信息
通过查看View的源代码,我们知道关于View的位置信息有以下:
1. mLeft、mTop、mRight、mBottom
2. translationX、translationY
3. getX()、getY()
2. demo验证
直接放了两个TextView在布局中
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text1"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center"
android:background="#ff0000"
android:textSize="15sp"
android:textColor="#ffffff"
android:text="text1"
/>
<TextView
android:id="@+id/text2"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center"
android:background="#00ff00"
android:textSize="15sp"
android:textColor="#ffffff"
android:text="text2"
/>
</LinearLayout>
然后设置监听器,点击text2就触动text1的动画,点击text1就打印日志
final TextView text1 = (TextView) findViewById(R.id.text1);
text1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i("cc", "mLeft = " + text1.getLeft() + ",mTop = " + text1.getTop() + ",mRight = " + text1.getRight() + ",mBottom = " + text1.getBottom());
Log.i("cc", "translationX = " + text1.getTranslationX() + ",translationY = " + text1.getTranslationY());
Log.i("cc", "x = " + text1.getX() + ",y = " + text1.getY());
}
});
final TextView text2 = (TextView) findViewById(R.id.text2);
text2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ObjectAnimator anim = ObjectAnimator.ofFloat(text1, "translationY", 0, 300);
anim.setDuration(2000);
anim.start();
}
});
开始运行,此时的界面如下:
text1在text2的上面,嗯,界面没有问题
此时,我直接点击text1,打印日志如下:
可以看出mTop的值为0,translationY的值也为0,y的值也为0,嗯。没有问题。然后我点击text2,界面如下
此时我再次点击text1,打印日志如下:
发现问题没有,mTop依然为0,但是translationY和y都已经变成了300.下面开始分析
3. 分析
我们翻看View的源码,直接搜索“mTop =”,看具体有哪儿对mTop进行了赋值。
搜索结果有两个地方,一个是在setTop()方法,一个叫setFrame的方法中看见了对mTop的赋值。
其中setTop方法是public方法,意味着是暴露给外界调用的,不过一般不会调用,因为会改变控件的高度。原因如下:
@ViewDebug.ExportedProperty(category = "layout")
public final int getHeight() {
return mBottom - mTop;
}
那么我们只需要关心setFrame方法了,因为该方法较长,我只截取了部分代码:
protected boolean setFrame(int left, int top, int right, int bottom) {
…………
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
…………
}
return changed;
}
可以看到这儿对mTop进行了赋值,那么我们继续搜索setFrame方法在哪些地方被调用了。搜索结果有三处地方:setLeftTopRightBottom、setOpticalFrame和layout方法。首先setLeftTopRightBottom方法也是public方法,也是提供给外界调用的,不过一般开发者不会去调用它,原因上面已经阐明。那么我们只需要关心setOpticalFrame和layout方法。
我们查看下setOpticalFrame的源码
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
平平常常,并没有什么什么奇特之处,只是简单得调用setFrame而已,然后我们继续搜索代码中哪儿调用了setOpticalFrame方法,搜索结果只有一个,就是上面提到的layout方法。一下是layout的源码。
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
主要是下面这一句在起作用:
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
也就是说,系统对于mTop的修改,仅仅限于在layout方法内部,而layout方法我们都知道,是View在onMeasure测量之后,对自身以及子控件进行位置布局的。因此我们刚才写的demo代码对translationY开启的属性动画,并不会影响到mTop的值。那么为什么y的值跟随translationY一起改动了呢?我们接着看源码。
public float getY() {
return mTop + getTranslationY();
}
public void setY(float y) {
setTranslationY(y - mTop);
}
是不是一下子就真相大白了,View根本就没有x、y的成员变量,不过是根据translationY和mTop的值计算出来的而已。
那么通过上面的demo,我们是否就只能得出这些信息呢?答案是NO,大家是否注意到了两个现象:
- 当我点击text2时,text1的位置往下移动了,但是text2的位置并没有被挤下去,我们的布局容器是LinearLayout,按道理text1往下移动了,text2也应该跟着往下移动。
- 当我点击text2后,text1移动到了text2的下面,此时我点击text1,发现删除了日志,也就是说text1接受了点击事件。做过动画的朋友都知道,如果我们用View动画来移动View的话,移动的只是View的影响,在动画结束的位置是无法响应点击事件的,而在最开始的位置能响应点击事件。
首先我们解释第一个现象,上面也说了,系统对于mTop的修改仅仅位于layout方法内,而我们开启动画是不能改变mTop的值的。那么mTop的值的意义是什么?是告诉父容器,我的控件的位置在哪儿。那么这个有什么意义呢?意义就是,父容器在onMeasure和onLayout的时候,是以mTop的值作为基准的,这也就是为什么text1往下移动了,text2没有被跟着挤下去。原因就是父容器LinearLayout认为text1的位置依然在mTop的位置。
好,那么如果我们就想改变mTop的值怎么办呢?有两个方法:
1. 直接调用setTop()方法进行设置,不过你要承受住控件变形的风险。
2. 调用setLayoutParam方法进行对控件的margin、width、height、left、top等设置。这个方法是改变的mTop的值。
然后我们解释第二个现象,我们发现text1的mTop等于0,translationY和y都等于300,并且能够在300的地方响应点击事件,这就说明了系统点击事件的范围判断是根据translationY来判断的,而不是通过mTop。
因此我们可以总结一下
- 如果想要改变View对于父容器的影响,那么影响的是mLeft、mTop、width、height等属于父容器的属性(mLeft、mTop、width、height等都是父容器的属性,而不属于本View)。
- 如果想要改变View自身的影响,比如添加监听器,添加动画等等,那么影响的是translationX、translationY等属于自身的属性。