0%

第3章:View体系与自定义View《一》

坐标系

Android 系统中有两种坐标系:Android 坐标系和 View 坐标系,了解这两种坐标系能够帮助我们实现View的各种操作。

Android坐标系

Android坐标系中, 将屏幕左上角的顶点作为原点, 这个原点向右是X轴正方向, 向下是Y轴正方向, 如下图所示。

Android坐标系

View坐标系

View坐标系与Android坐标系并不冲突,两者是共同存在的,一起来帮助开发者更好地控制View。对于View坐标系,搞明白下图的信息即可:

View坐标系

MotionEvent提供的方法:假设上图中间的那个圆点就是我们触摸点,无论是View还是ViewGroup,最终的点击事件都会由onTouchEvent(MotionEvent event)方法来处理。MotionEvent提供了获取焦点坐标的各种方法:

  • getX():获取点击事件距离控件左边的距离,即视图坐标。
  • getY():获取点击事件距离控件顶边的距离,即视图坐标。
  • getRawX():获取点击事件距离整个屏幕左边的距离,即绝对坐标。
  • getRawY():获取点击事件距离整个屏幕顶边的距离,即绝对坐标。

View的滑动

View的滑动基本思想:当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。 实现View滑动有很多种方法, 在这里主要讲解6种滑动方法, 分别是layout()、offsetLeftAndRight() 与 offsetTopAndBottom()、LayoutParams、动画、scollTo 与 scollBy ,以及Scroller。

layout方法

View进行绘制的时候会调用onLayout()方法来设置显示的位置, 因此我们同样也可以通过修改View的left、 top、 right、 bottom属性来控制View的坐标。以下是实现一个随手指滑动的自定义view的步骤:

  1. 首先获取触摸点的坐标
1
2
3
4
5
6
7
8
9
10
11
12
public boolean onTouchEvent(MotionEvent event){
//获取手指触摸点的横坐标和纵坐标
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
}
}
  1. 在ACTION_MOVE事件中计算偏移量,再调用layout( ) 方法重新放置这个自定义View的位置即可:
1
2
3
4
5
6
7
8
9
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//调用layout方法来重新确定它的位置
layout(getLeft() + offsetX,getTop()+offsetY,
getRight()+offsetX,getBottom()+offsetY)

break

在每次移动时都会触发layout()方法对屏幕重新布局,从而达到移动View的效果。

offsetLeftAndRight() 与offsetTopAndBottom()

这两种方法和layout()方法的效果以及使用方式都差不多,只需要将上面ACTION_MOVE中的代码替换为以下代码即可:

1
2
3
4
5
6
7
8
9
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//对left 及 right 进行偏移
offsetLeftAndRight(offsetX);
//对top及bottom进行偏移
offsetTopAndBottom(offsetY);
break;

LayoutParams( 改变布局参数)

LayoutParams主要保存了View的布局参数, 因此可以通过改变它来达到改变View位置的效果。 我们只需将 ACTION_MOVE 中的代码替换成如下代码即可(注意是:MarginLayoutParams):

1
2
3
4
5
6
7
8
9
10
case MotionEvent.ACTION_MOVE:
//计算移动距离
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)getLayoutParams();

params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
setLayoutParams(params)
break;

动画

采用动画来移动,在res目录新建anim文件夹并创建如下translate.xml文件:

1
2
3
4
5
6
7
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true">
<translate
android:duration="1000"
android:fromXDelta="0"
android:toXDelta="300"/>
</set>

在Java中调用:

view.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate))

需要注意的是,如果动画文件中没有添加 android:fillAfter=”true” ,则方块向右平移300像素后,又返回原来的位置。并且,View动画不能改变View的位置参数,如果对一个Button加上如上的平移动画,当Button平移300像素停留在当前位置时,我们点击这个Button并不会触发点击事件,但是点击原始位置却触发了点击事件,这是因为对于系统来说,Button并没有改变原来位置。

在Android 3.0出现的属性动画解决了上述问题,它不仅可以执行动画,还能改变View的位置参数,其操作如下:

ObjectAnimator.ofFloat(view,”translationX”,0,300).setDuration(1000).start()

scrollTo 与 scrollBy

scollTo、scollBy移动的是View的内容,如果在ViewGroup中使用, 则是移动其所有的子View。scrollTo(x,y)表示移动到一个具体的坐标点,而scrollBy(dx,dy)则表示移动的增量为dx、dy。 其中, scollBy最终也是要调用scollTo的。二者的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void scrollTo(int x,int y){
if(mScrollX != x || mScrollY != y){
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;

invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if(!awakenScrollBars()){
postInvalidateOnAnimation();
}
}
}

public void scrollBy(int x,int y){
scrollTo(mScrollX + x, mScrollY + y)
}

如果要实现上面view随手指滑动的效果,就需要将ACTION_MOVE中的代码替换成如下代码:

((View)getParent()).scrollBy(-offsetX,-offsetY);

请注意,这里是对view的parent进行scroll,这是因为如果对view本身scroll的话,就是对自己的内容进行移动,而不是整个view。并且注意,这里设置的偏移量值都为负值,以下具体讲解一下。

假设我们正用放大镜来看报纸,放大镜用来显示字的内容。同样我们可以把放大镜看作我们的手机屏幕,它们都是负责显示内容的;而报纸则可以被看作屏幕下的画布,它们都是用来提供内容的。放大镜外的内容,也就是报纸的内容不会随着放大镜的移动而消失,它一直存在。同样,我们的手机屏幕看不到的视图并不代表其不存在。过程的示意图如下:

scrollBy之前:

scrollBy初始状态

调用scrollBy(50,50)之后:

scrollBy之后

Scroller

我们在用scollTo/scollBy方法进行滑动时,这个过程是瞬间完成的,所以用户体验不大好。这里我们可以使用 Scroller 来实现有过渡效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller本身是不能实现View的滑动的,它需要与View的computeScroll() 方法配合才能实现弹性滑动的效果。具体代码如下示意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public CustomView(Context context,AttributeSet attrs){
private Scroller mScroller;

super(context,attrs);
//初始化mScroller
mScroller = new Scroller(conetxt);

@Override
public void computeScroll(){
super.computeScroll();
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrentX(),mScroller.getCurrentY());
invalidate();
}

}


//提供调用的方法
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int scrollY = getScrollY();

int deltaX = destX - scrollX;
int deltaY = destY - scrollY;
mScroller.startScroll(scrollX, scrollY, deltaX, deltaY);
}
}

我们首先初始化Scroller,之后重写computeScroll方法,系统会在绘制View的时候在 draw 方法中调用该方法。在computeScroll方法中, 我们调用父类的scrollTo() 方法并通过Scroller来不断获取当前的滚动值, 每滑动一小段距离我们就调用invalidate() 方法不断地进行重绘,重绘就会调用computeScroll()方法, 这样我们通过不断地移动一个小的距离并连贯起来就实现了平滑移动的效果。这里我们设定CustomView沿着X轴向右平移400像素(至于为什么是负数,上面已经解释过了):

mCustomView.smoothScrollTo(-400,0);

属性动画

在属性动画出现之前,Android系统提供的动画只有帧动画和 View 动画。View 动画我们都了解,它提供了AlphaAnimation、 RotateAnimation、 TranslateAnimation、 ScaleAnimation这4种动画方式,并提供了AnimationSet动画集合来混合使用多种动画。 随着Android 3.0属性动画的推出, View动画不再风光。 相比属性动画, View动画一个非常大的缺陷突显, 其不具有交互性。 当某个元素发生View动画后,其响应事件的位置依然在动画进行前的地方, 所以View动画只能做普通的动画效果, 要避免涉及交互操作。 但是它的优点也非常明显: 效率比较高, 使用也方便。

在属性动画中使用最多的就是AnimatorSet和ObjectAnimator配合: 使用 ObjectAnimator 进行更精细化的控制, 控制一个对象和一个属性值, 而使用多个ObjectAnimator组合到AnimatorSet形成一个动画。 属性动画通
过调用属性get、 set方法来真实地控制一个View的属性值, 因此, 强大的属性动画框架基本可以实现所有的动画效果。

ObjectAnimator

ObjectAnimator 是属性动画最重要的类, 创建一个 ObjectAnimator 只需通过其静态工厂类直接返还一个ObjectAnimator对象。 参数包括一个对象和对象的属性名字, 但这个属性必须有get和set方法, 其内部会通
过Java反射机制来调用set方法修改对象的属性值。 一般使用方式如下:

ObjectAnimator.ofFloat(view,”translationX”,200,0).start()

ObjectAnimator的使用方法就不介绍了,需要注意的是, 在使用ObjectAnimator的时候, 要操作的属性必须要有get和set方法, 不然ObjectAnimator 就无法生效。 如果一个属性没有get、 set方法, 也可以通过自定义一个属性类或包装类来间接地给这个属性增加get和set方法。 如以下示例这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static class MyView{
private View mTarget;
private MyView(View target){
this.mTarget = target;
}

public int getWidth(){
return mTarget.getLayoutParams().widht
}

public void setWidth(int width){
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}

使用时只需要操作包类就可以调用get、 set方法了:

1
2
MyView mMyView = new MyView(mButton);
ObjectAnimator.ofInt(mMyView,"width,500).setDuration(500).start()

ValueAnimator

ValueAnimator不提供任何动画效果, 它更像一个数值发生器, 用来产生有一定规律的数字, 从而让调用者控制动画的实现过程。 通常情况下, 在ValueAnimator的AnimatorUpdateListener中监听数值的变化, 从而完成动画的变换。

AnimatorSet

在XML中使用属性动画

1
2
3
4
5
6
7
8
<objectAnimator 
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:duration="3000"
android:propertyName="scaleX"
android:valueFrom="1.0"
android:valueTo="2.0"
android:valueType="floatType"/>

在代码中引用xml属性动画也很简单:

1
2
3
Animator animator = AnimatorInflater.loadAnimator(this,R.anim.scale);
animator.setTarget(view);
animator.start();

解析Scroller

略,去看源码,看不懂再来添加这块内容

View 事件分发机制

这里了解Activity的构成就好了,如下图:

Activity构成

事件分发机制则看之前写的文章还容易理解一些,这里就略过了。

View的工作流程

这一章太长,作为第二部分内容。

谢谢你的鼓励