创建view的交互性
绘制UI只是创建自定义view的一部分。你也需要让你的view对用户的输入做出反馈。对象需要像真实的事物一样作出反应。举个例子,图片不能再某个地方突然就弹出来,因为在真实的世界中,对象不是那么做的。相对地,图片需要从一个地方移动到另一个地方。
UI应该能够感知到细微地行为方式,并且模仿出真实世界中的最佳反馈方式。举个例子,当用户快速滑动UI对象的时候,他们在最开始的时候应该感知到有一种摩擦力使动作延迟了,最后感知到有一股惯性动量将这个动作滑动起来。
这篇教程将会展示如何使用Android的框架特性去添加一个真实世界的行为去到你的自定义View当中。
处理输入手势
和很多其他的UI框架一样,Android提供了一个输入事件模型。用户的动作都将会被转化为一个事件触发回调,并且你就可以覆写这些回调来自定义你的应用如何来反馈对象。在Android系统中最常见的输入事件就是touch,这将会触发onTouchEvent(android.view.MotionEvent)
。覆写这个方法来处理事件:
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
触摸事件本身来说不是十分有用的。现代的触摸UI交互称之为手势。比如说:tapping, pulling, pushing, flinging, and zooming,为了将这些未处理过的触摸事件进行转化,使之成为手势,Android提供了GestureDetector
这个类。
在一个实例类中构造一个GestureDetector
,可以实现以下:
GestureDetector.OnGestureListener
假如你只想处理一小部分手势,你可以继承这个
来替代实现 GestureDetector.OnGestureListener
接口。在实例中,这部分代码创建了一个类继承 GestureDetector.SimpleOnGestureListener
并去覆写onDown(MotionEvent)
。
class mListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());
不管你是否使用GestureDetector.SimpleOnGestureListener
,你都必须实现onDown(MotionEvent)
方法,并且在方法中返回true。这一步骤是必须的,所有的手势都是以onDown(MotionEvent)
开始的。假如你返回了false,就如在GestureDetector.OnGestureListener
中那样,那么系统就假定你想要忽略剩下的所有手势,那么GestureDetector.OnGestureListener
中的其他方法都永远都不会调用。只有一种情况你应该返回返回false,那就是你真的是想忽略整个手势的。一旦你实现了GestureDetector.OnGestureListener
,并且创建了GestureDetector
的实例,你就能在onTouchEvent()
中使用GestureDetector
来声明触摸事件。
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mDetector.onTouchEvent(event);
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
stopScrolling();
result = true;
}
}
return result;
}
当一个触摸事件被传递到onTouchEvevnt()
中时,它并没有意识到自己是手势的一部分,所以它返回了false。现在,你能够运行你自己的自定义手势代码了。
创建仿真动作
手势是一个用来控制触摸屏设备的强大方法,但是他们可能会违反直觉感知,并且很难被记住。除非他们产出一个仿真结果。一个很好的例子是fling手势,这是用来描述用户在屏幕上快速移动手指,然后离开屏幕。这个手势意味着UI反馈是否能快速地在fling的方向上移动,然后速度慢慢下降,就好像用户推动一个飞轮,然后使它旋转。
然而,模拟出一种飞轮的感觉并不简单。需要很多的物理以及数学知识来使飞轮模型正确工作。幸运的是,Android提供了辅助类来模拟这个和其他的行为。Scroll
类是处理飞轮类型fling的基类。
开始进行fling,调用fling()
,指定开始的速度,在X, Y方向上的最小以及最大值,对于速度值,可以使用GestureDetector
为你计算出来的值。
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
mScroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
postInvalidate();
}
值得注意的是:即使说用GestureDetector
计算出来的是很精确的,很多开发者会感觉到使用这个值会使fling动画过快了。一般而言,我们都将X,Y方向的速度分开,速度为4-8。
调用fling()
方法给fling手势设置了物理模型,在这之后,你需要更新Scroller
,这时候在一个常规间隔内调用方法:Scroller.computeScrollOffset()
。Scroller.computeScrollOffset()
更新了Scroller
对部状态,更新的原理为读取当前时间,并且使用物理模型来计算出x,y方向上的位置。调用getCurrX()
以及 getCurrY()
来检索这些值。
大多数的View直接在X,Y位置上执行Scroller
的scrollTo()
方法。然而,PieChart 这个例子有些不同,它使用了当前滚动Y位置来设定旋转角。
if (!mScroller.isFinished()) {
mScroller.computeScrollOffset();
setPieRotation(mScroller.getCurrY());
}
Scroller
这个类为你计算滚动的位置,但是它并不自动地将其这些位置应用你的views中去。确保你能得到并且应用这些新坐标,然后让滑动看起来尽可能地顺滑,这就是开发者的责任了。以下有两个方法可以做到这样:
- 在调用了
fling()
方法后调用postInvalidate()
方法,这是为了强制重绘。这种技术需要你在onDraw()
中计算出滚动的偏移量,然后在每次偏移量改变的时候调用postInvalidate()
。 - 设置属性动画让fling在一段时间内动画过渡,然后添加监听器来执行动画的更新。在这里使用到了
addUpdateListener()
。
PieChart 这个例子使用了第二种方法。这种技术稍微有些复杂,但是它与动画系统结合得更加紧密,并且不需要可能不是必须的view。缺点是属性动画在API 11以下是不能够使用的,因此这项技术不能在运行Android3.0以下的设备中使用。
需要注意的是:属性动画不能在API 11以下使用,但是你依然可以在低于此版本的应用中使用,你只需要保证检查在当前的API 版本中运行,而忽略掉在版本低于11 的动画系统调用。
mScroller = new Scroller(getContext(), null, true);
mScrollAnimator = ValueAnimator.ofFloat(0,1);
mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
if (!mScroller.isFinished()) {
mScroller.computeScrollOffset();
setPieRotation(mScroller.getCurrY());
} else {
mScrollAnimator.cancel();
onScrollFinished();
}
}
});
让你的转换更加顺滑
用户期待一个现代的UI能够在状态转化期间更加顺滑。UI元素在进和出的时候淡出,而不出突然出现,突然消失。动作顺滑地开始,结束,而不是突然地开始,停止。Android动画框架,包含Android 3.0 ,在这一方面能很简单实现。
在任何性质改变影响到你的view的外观的时候,使用动画系统,不要立即就改变内容,而是使用属性动画来改变。在接下来的例子中,使用了动画:
mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0);
mAutoCenterAnimator.setIntValues(targetAngle);
mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
mAutoCenterAnimator.start();
假如你想改变的是view的某一个基类属性值,使用动画会更加简单,因为这些views都有有内建的属性动画。举个例子:
animate().rotation(targetAngle).setDuration(ANIM_DURATION).start()