在android应用程序的开发过程中,相信我们很多人都想把应用的交互做的比较绚丽,比如让界面切换平滑的滚动,还有热度灰常高的伪3D等界面效果,通常情况下,系统提供的应用在特效这方面只能为我们提供简单的动画接口,所以要想实现比较酷炫的效果还是要自己去开发布局控件(即所谓的自定义View、ViewGroup)。小弟也经常做一些自定义的控件,最近工作比较清闲,所以便将自己对自定义布局控件的一些心得写出来,权当是自己的学习笔记了,各位高手看到了可以忽略。下面就我最近工作中遇到的一个自定义控件开发做一些简单的介绍,其实那个地方原本可以用ScrollView解决很大一部分问题的,但有一些效果确实需要对控件进行重新定义,在继承ScrollView开发中仍然会遇到一些ScrollView自身的限制,所以就仿照ScrollView自己做了一个控件。在其中遇到了一些问题自然就是像ScrollView中拖动的效果(比如快速拖动在手指离开屏幕时控件依旧会由于惯性继续滑动一段距离后才会停止运动),所以就对这个东东做了一下仔细的研究,虽然以前也做过类似的开发,这次由于时间比较充裕,所以将开发中遇到的一些问题都一一记录了下来。下面开始正题:
自定义布局控件自然是要继承某个View或ViewGroup
由于是根据项目的开发来写的这篇博客,所以我就以自定义布局控件(ViewGroup)来做介绍了。
开发一个自定义的ViewGroup自然是要继承ViewGroup类了,在继承这个类之后必须要重写的方法就是
onLayout(boolean changed, int l, int t, int r, int b)
另外至少要有一个构造方法,我个人习惯重写那个有两个参数的构造方法(XXX(Context context, AttributeSet attrs)),因为有了这个构造方法就可以在xml布局文件里使用这个类了。
如果想要对这个布局控件以及其子控件的尺寸进行精确的控制那就要重写下面这个方法了
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
这个方法从字面理解就是估算控件的尺寸大小了,关于这个方法的详细说明引用一下另一位童鞋的文章,这里就不详细介绍了
下面开始介绍关于如何让自定义的控件进行平滑的移动,并能够根据手势的情况产生惯性滑动的效果
先介绍一下开发这种滑动效果需要用到的各种工具类:
android.view.VelocityTracker
android.view.Scroller
android.view.ViewConfiguration
VelocityTracker从字面意思理解那就是速度追踪器了,在滑动效果的开发中通常都是要使用该类计算出当前手势的初始速度(不知道我这么理解是否正确,对应的方法是velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity))并通过getXVelocity或getYVelocity方法得到对应的速度值initialVelocity,并将获得的速度值传递给Scroller类的fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY)方法进行控件滚动时各种位置坐标数值的计算,API中对fling方法的解释是基于一个fling手势开始滑动动作,滑动的距离将由所获得的初始速度initialVelocity来决定。关于ViewConfiguration的使用主要使用了该类的下面三个方法:
configuration.getScaledTouchSlop()//获得能够进行手势滑动的距离
configuration.getScaledMinimumFlingVelocity()//获得允许执行一个fling手势动作的最小速度值
configuration.getScaledMaximumFlingVelocity()//获得允许执行一个fling手势动作的最大速度值
需要重写的方法至少要包含下面几个方法:
onTouchEvent(MotionEvent event)//有手势操作必然少不了这个方法了
computeScroll()//必要时由父控件调用请求或通知其一个子节点需要更新它的mScrollX和mScrollY的值。典型的例子就是在一个子节点正在使用Scroller进行滑动动画时将会被执行。所以,从该方法的注释来看,继承这个方法的话一般都会有Scroller对象出现。
在往下就是介绍比较具体的开发思路
首先我们要初始化一些变量,其中的多数代码已经在上面做出介绍了
点击(此处)折叠或打开
-
void init(Context context) {
-
-
mScroller = new Scroller(getContext());
-
-
setFocusable(true);
-
-
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
-
-
setWillNotDraw(false);
-
-
final ViewConfiguration configuration = ViewConfiguration.get(context);
-
-
mTouchSlop = configuration.getScaledTouchSlop();
-
-
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
-
-
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
-
-
- }
点击(此处)折叠或打开
-
private void obtainVelocityTracker(MotionEvent event) {
-
-
if (mVelocityTracker == null) {
-
-
mVelocityTracker = VelocityTracker.obtain();
-
-
}
-
-
mVelocityTracker.addMovement(event);
-
-
}
-
-
-
private void releaseVelocityTracker() {
-
-
if (mVelocityTracker != null) {
-
-
mVelocityTracker.recycle();
-
-
mVelocityTracker = null;
-
-
}
-
- }
onTouchEvent(MotionEvent event)方法的重写
点击(此处)折叠或打开
-
public boolean onTouchEvent(MotionEvent event) {
-
-
if (event.getAction() == MotionEvent.ACTION_DOWN
-
-
&& event.getEdgeFlags() != 0) {
-
-
return false;
-
-
}
-
-
-
obtainVelocityTracker(event);
-
-
-
final int action = event.getAction();
-
-
final float x = event.getX();
-
-
final float y = event.getY();
-
-
-
switch (action) {
-
-
case MotionEvent.ACTION_DOWN:
-
-
LogUtil.log(TAG, "ACTION_DOWN#currentScrollY:" + getScrollY()
-
-
+ ", mLastMotionY:" + mLastMotionY,
-
-
LogUtil.LOG_E);
-
-
if (!mScroller.isFinished()) {
-
-
mScroller.abortAnimation();
-
-
}
-
-
mLastMotionY = y;
-
-
break;
-
-
-
case MotionEvent.ACTION_MOVE:
-
-
final int deltaY = (int) (mLastMotionY - y);
-
-
mLastMotionY = y;
-
-
if (deltaY < 0) {
-
-
if (getScrollY() > 0) {
-
-
scrollBy(0, deltaY);
-
-
}
-
} else if (deltaY > 0) {
-
-
mIsInEdge = getScrollY() <= childTotalHeight - height;
-
-
if (mIsInEdge) {
-
-
scrollBy(0, deltaY);
-
-
}
-
-
}
-
-
break;
-
-
-
case MotionEvent.ACTION_UP:
-
-
final VelocityTracker velocityTracker = mVelocityTracker;
-
-
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
-
-
int initialVelocity = (int) velocityTracker.getYVelocity();
-
-
-
if ((Math.abs(initialVelocity) > mMinimumVelocity)
-
-
&& getChildCount() > 0) {
-
-
fling(-initialVelocity);
-
-
}
-
-
-
releaseVelocityTracker();
-
-
break;
-
-
}
-
-
-
return true;
-
- }
在onTouchEvent方法中,当手势执行到ACTION_UP时获得当时手势的速度值然后判断这个速度值是否大于可滑动的最小速度,如果符合条件那么就执行fling(int velocityY)方法,通过fling方法中的日志发现,在执行了invalidate()方法之后,程序便会执行computeScroll()方法,在computeScroll()方法中执行scrollTo方法主要是因为mScrollX、mScrollY这两个变量的修饰符为portected,无法在扩展类里面无法对这两个变量直接进行操作,那么就需要使用scrollTo方法对这两个变量进行操作,以刷新当前的UI控件,下面附上computeScroll()方法的代码
点击(此处)折叠或打开
-
public void computeScroll() {
-
-
if (mScroller.computeScrollOffset()) {
-
-
int scrollX = getScrollX();
-
-
int scrollY = getScrollY();
-
-
int oldX = scrollX;
-
-
int oldY = scrollY;
-
-
int x = mScroller.getCurrX();
-
-
int y = mScroller.getCurrY();
-
-
scrollX = x;
-
-
scrollY = y;
-
-
scrollY = scrollY + 10;
-
-
scrollTo(scrollX, scrollY);
-
-
postInvalidate();
-
-
}
-
- }
其中的mScroller.computeScrollOffset()是用来判断动画是否完成,如果没有完成返回true继续执行界面刷新的操作,各种位置信息将被重新计算用以重新绘制最新状态的界面。关于scrollTo方法,我们需要看一下该方法的代码(来自View中):
点击(此处)折叠或打开
-
public void scrollTo(int x, int y) {
-
-
if (mScrollX != x || mScrollY != y) {
-
-
int oldX = mScrollX;
-
-
int oldY = mScrollY;
-
-
mScrollX = x;
-
-
mScrollY = y;
-
-
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
-
-
if (!awakenScrollBars()) {
-
-
invalidate();
-
-
}
-
-
}
-
- }
我们可以看到,当传递进来的x、y的值与控件当前的mScrollX、mScrollY的值不相同时对界面进行重新计算,根据日志打印的情况来看似乎awakenScrollBars()返回的总是true, 这样的话每执行一次computeScroll()方法,就需要执行一次postInvalidate()方法来刷新界面,而postInvalidate()方法会通过内部线程重新调用invalidate()已达到界面刷新的效果,产生手势离开屏幕之后的惯性滑动效果。