本文介绍了如何自定义View中的后两个步骤:实现交互和优化View。
在上一篇文章中我们介绍了自定义View的前两个步骤:创建一个View类和自定义绘制。接下来我们继续学习自定义View的后两个步骤:实现交互和优化View。
实现交互
处理输入手势
Android中最常见的输入事件就是触摸,重写onTouchEvent方法处理事件:
1 | @Override |
它们自己的触摸事件不是特别有用。现代触摸UI定义了一系列手势,例如:轻敲、拉、推、滑动和缩放。为了将原始触摸事件转换为手势,Android提供了GestureDetector。
使用一个实现 GestureDetector.OnGestureListener类的实例构造GestureDetector。如果你只想处理几个手势,你可以继承 GestureDetector.SimpleOnGestureListener而不是实现GestureDetector.OnGestureListener接口。例如,下面的代码创建了一个继承GestureDetector.SimpleOnGestureListener的类并且重写了 onDown(MotionEvent)。1
2
3
4
5
6
7class mListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
}
mDetector = new GestureDetector(PieChart.this.getContext(), new mListener());
不管你使不使用GestureDetector.SimpleOnGestureListener,你必须实现onDown()方法返回true。这一步是必须的因为所有的手势都从onDown()消息开始。如果你返回false,那么就不会接收到后续事件。除非你想忽略整个手势。当你实现了GestureDetector.OnGestureListener并且创建了GestureDetector实例,你可以使用GestureDetector在onTouchEvent()方法中处理接收到的事件。1
2
3
4
5
6
7
8
9
10
11@Override
public boolean onTouchEvent(MotionEvent event) {
boolean result = mDetector.onTouchEvent(event);
if (!result) {
if (event.getAction() == MotionEvent.ACTION_UP) {
stopScrolling();
result = true;
}
}
return result;
}
当你传递到onTouchEvent()一个触摸事件,如果它没能识别作为手势的一部分,它会返回false。你可以实现自己的自定义手势检测代码。
创建物理动作
手势是一个很强大的控制触摸屏设备的方式,除非符合物理逻辑否则将变得违反直觉并且很难记忆。这有一个很好的例子就是fling手势,用户用手指快速滑动屏幕。这个手势让UI根据快速滑动的方向响应,然后慢下来,就像用户旋转大转盘。
然而,模拟大转盘的旋转并不是简单的。让大转盘正确的旋转需要大量的物理和数学知识。幸运的是,Android提供了帮助类去模拟这个和其它行为。Scroller是一个基础类对于处理大转盘滑动手势。
使用开始速度和滑动的x和y的最大最值调用fling()方法进行滑动。对于速度值,你可以使用GestureDetector计算的值。1
2
3
4
5@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计算出的速度值是精确的物理值,但很多开发者会感觉到使用这个值会让滑动动画太快。常见的x和y速度值是除以4到8。
调用fling()会给滑动手势设置物理模型。之后需要通过固定的时间间隔调用Scroller.computeScrollOffset()更新Scroller。 computeScrollOffset()会通过读取当前时间更新 Scroller对象内部状态和使用物理模型计算那一时刻x和y的位置。调用getCurrX()和getCurrY()获取这些值。
大多数View可以直接传递Scroller对象的x和y的位置到scrollTo()。PieChart示例有一点不同:它使用当前y的位置设置图表的旋转角度。1
2
3
4if (!mScroller.isFinished()) {
mScroller.computeScrollOffset();
setPieRotation(mScroller.getCurrY());
}
Scroller类会帮你计算滚动位置,但它不会自动应用这些位置到你的view上。你应该确保不断获取并应用新的坐标让滚动动画看起来很流畅。这有两种方法:
- 调用fling()之后调用postInvalidate(),为了强制重新绘制。这个技术需要你在onDraw()计算滚动偏移并且每次当滚动偏移变化时调用postInvalidate()。
- 当fling滑动时设置ValueAnimator动画,通过调用addUpdateListener()添加监听处理动画更新。
PieChart示例使用了第二种方法,这个技术对设置来说稍微有点复杂,但它的工作原理和动画系统关联更紧密并且不需要潜在的不必要的view刷新。ValueAnimator的缺点是API level 11(Android 3.0)之前是不可用的,因此它不能运行在Android 3.0之前的设备上。1
2
3
4
5
6
7
8
9
10
11
12
13
14mScroller = 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的外观,不要直接修改属性。而是使用ValueAnimator作修改。在下面的示例中,在PieChart中修改当前选择的饼片将引起整个饼图的旋转,选择指针指向选择的饼片。ValueAnimator会在一段时间内修改旋转角度,而不是直接设置新的旋转角度。1
2
3
4mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0);
mAutoCenterAnimator.setIntValues(targetAngle);
mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
mAutoCenterAnimator.start();
如果你想修改的值是View的基本属性,这个动画更容易实现,因为View内置了一个ViewPropertyAnimator,它是对于同时对多个属性动画最优的。例如:1
animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();
优化View
现在你已经可以设计一个响应手势和状态之间过渡的View,确保View运行很快。为了避免UI卡顿,确保动画每秒运行60帧。
少操作,少频率
为了让View更流畅,从经常被调用的方法删除不必要的代码。先从onDraw()方法开始,将会看到明显的效果。尤其应该删除 onDraw()中的对象分配,因为分配将导致垃圾回收将会引起卡顿。在初始化的时候分配对象,或动画之间。不要在动画运行的时候分配对象。
除了让onDraw()精简,还要确保它尽可能少地被调用。onDraw()最多的调用是invalidate()引起的,所以删除不必要的invalidate()。
其它昂贵的操作是遍历布局。任何时候View调用requestLayout()方法,Android UI系统都需要遍历整个view层级找出每个view需要多大。如果发现测量冲突,它可能需要多次遍历层级。UI设计师有时为了得到想要的效果会创建嵌套深层级ViewGroup对象。这些深层级View会导致性能问题。让你的View层级尽可能的少。
如果你有一个复杂的UI,考虑写一个自定义的ViewGroup执行它的布局。和内置View不同,自定义View可以指定子View的图形和大小,并且因此可以避免遍历子View计算测量。PieChart示例演示了怎样继承ViewGroup作为自定义View的一部分。PieChart有子View,但它从不测量它们。它根据它的自定义布局算法直接设置子View的大小。
查看PieChart示例完整源码:https://github.com/yuweiguocn/AndroidDemo