Android 高仿豆瓣客户端动画特效

前一段时间水寒安装了一个豆瓣客户端,第一次打开就被这种界面风格吸引了,今天早上起来在打开豆瓣听音乐的时候,突然产生一个念头,来试着实现一下这种效果,打开客户端分析了一下发现其实这种效果的实现并不是想象中的那么难,下面我先分析一下这种效果的实现思路,然后一步步解释实现的过程,希望大家能提出意见和建议,一起交流学习。

先给大家展示一下我的成果吧:

高仿豆瓣客户端动画特效演示 高仿豆瓣客户端动画特效演示

其实豆瓣客户端的界面上还有其他的文字和菜单,但是这两个的实现效果和其他几个类似,可以作为代表,所以就不绘制那么多组件了。

分析界面的组成结构

界面组成结构原理示意 界面组成结构原理示意

有两个和我们手机屏幕尺寸大小相等的 View (分别是灰色透明度变化的背景和主界面),假设屏幕的宽和高是 w 和 h, 屏幕的坐标原点在左上角,这两个 View 相对于屏幕的坐标是:

刚开始的坐标:

灰色背景: left 0 right w top -h bottom 0

主界面:left 0 right w top 0 bottom h

滑动到最底部(d为滑动到最底部的高度)

灰色背景: left 0 right w top -d bottom h-d

主界面: left 0 right w top h-d bottom 2h-d

接下来我们就要分析两个大问题:

1、滑动的具体实现

从上面的图上可以很容易的看出来,此时我们就要考虑如何实现界面上下滑动,观察豆瓣客户端的滑动手势发现支持滑动加速度检测(速度大于某值时直接从一端滑向另一端)、支持屏幕跟随手指滑动、屏幕的Y轴方向的中间是一个恢复位置的临界点。

从上面的分析我们基本上可以知道用到的技术有如下几个:

  1. 监听Event_Move事件,通过scorllBy实现实时移动(跟随手指)。
  2. 判断Event_Up时的手指位置,来判断是否恢复到原来位置。
  3. 滑动的时候判断手指滑动的速度,来确定是否直接滑动到另一端(上端和下端)。

2、界面上元素的缩放和透明度的变化

界面上透明度变化的地方大致有这几处,滑动时上面的灰色背景透明度渐变(从上向下滑动变透明,从下向上滑动变灰),主界面上的文字透明度变化。

缩放的控件有主界面上的圆形图片和底部菜单(底部菜单是类似的实现,这里不做讨论),而且随着滑动位置从水平居中向左边移动并且变小。

透明度变化的实现其实很简单,只需要知道当前位置相对整个屏幕坐标的比例计算出来即可,现在比较难的是如何实现中间圆形图片的缩放和位置的移动。

图标缩放位置也移动示意图 图标缩放位置也移动示意图

可以简单的从上图中看出大概的变化规律,我们要参考的时屏幕的 TOP 和 LEFT 来确定圆形的位置。

实现过程详解

首先我们添加两个View(灰色界面和主界面)

1
2
3
4
private void addChild(){
    addTopView();
    addCenterView();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private void addTopView(){
    View view = new View(context);
    view.setBackgroundColor(Color.GRAY);
    mTopView = view;	
    addView(mTopView);
}

private void addCenterView(){
    View view = new CustomCenterVIew(context);
    view.setBackgroundColor(Color.WHITE);
    mCenterView = view;
    addView(mCenterView);
}

重写 ViewGroup,然后再 onLayout 中对两个 View 进行布局(初始布局)

1
2
3
4
5
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    mTopView.layout(0, -mViewHeight, mViewWidth, 0);
    mCenterView.layout(0, 0, mViewWidth, mViewHeight);
}

我们重点来看一下对屏幕事件的处理,下面是重写 onTouchEvent 方法:

 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private float mOldY;
private VelocityTracker vTracker;
@Override
public boolean onTouchEvent(MotionEvent event) {
    int disY;
    int vTrackY;
    float eventY = event.getY();
    obtainVelocityTracker(event);
    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        mOldY = eventY;
        break;
    case MotionEvent.ACTION_MOVE:	
        disY = (int)(eventY - mOldY);
        if(disY > 0 && Util.getCenterViewPointY(mCenterView) 
                >= mViewHeight){
            return true;
        }
        if(disY < 0 && Util.getCenterViewPointY(mCenterView) <= mViewPointY){
            return true;
        }
        mOldY = eventY;
        scrollBy(0, -disY);
        break;
    case MotionEvent.ACTION_UP:
        vTracker.computeCurrentVelocity(1000);
        vTrackY = (int) vTracker.getYVelocity();
        Log.e(TAG, "vTrack = " + vTrackY);
        if(Math.abs(vTrackY) > 6000){
            if(vTrackY > 0){
                moveToBottom();
            }else{
                moveToTop();
            }
        }else{
            int disPointY = Util.getCenterViewPointY(mCenterView) - mViewPointY;
            if(disPointY > mViewHeight / 2){
                moveToBottom();
            }else{
                moveToTop();
            }
        }
        break;
    }
    return true;
}

在 ACTION_MOVE 事件中我们做了两个条件判断是为了防止滑动到最上边后或滑动到最下端还可以继续滑动(限定了一个滑动的范围),然后使用 scrollBy 滑动响应(手机滑动)的距离。

在 ACTION_UP 中主要做了两件事,一个是判断用户的手指滑动速度是否超出了一个临界值,如果超出则表明用户是想滑动到底的。另一个是判断是否手指滑动到了屏幕的中界线以外来处理滑动到底还是恢复到原来的位置。

为了实现平滑的滑动我们在 moveToBottom 和 moveToTop 中使用了 computerScroll 方法来实现平滑滑动效果。关于详细用法请参考我的另一篇博文:http://blog.csdn.net/dawanganban/article/details/23998781

接下来我们来看一下主界面 View 的实现,这个 View 其实是一个自定义 View,我重写了 onDraw 方法来实现透明度和大小的变化(使用系统动画实现不了随着手指移动实时变化的效果)具体的实现如下:

 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
29
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int currentNum = Util.getCenterViewPointY(this);
    Log.e(TAG, "currentNum = " + currentNum);
    int alpha = 255 - (int)((float)500 * 
            (currentNum - CustomView.BOTTOM_MENU_HEIGHT) / maxNum);
    if(alpha > 0){
        titleTextPaint.setAlpha(alpha);
        float titleWidth = titleTextPaint.measureText(title);
        canvas.drawText(title, (mViewWidth - titleWidth) / 2, TITLE_TOP_MARGIN, titleTextPaint);
    }
    
    int beginX = (int)((mViewWidth - mCenterIconBitmap.getWidth()) / 2 
            - (float)CENTER_ICON_LEFT_GAP * (currentNum - CustomView.BOTTOM_MENU_HEIGHT) / maxNum);
    int beginY = (int)(CENTER_ICON_MARGIN - 
            (float)CENTER_ICON_MARGIN * currentNum / maxNum);
    
    canvas.save();
    float scale = 1.6f - (float)currentNum / maxNum;
    canvas.scale(scale, scale, 
            beginX + mCenterIconBitmap.getWidth() / 2, 
            beginY + mCenterIconBitmap.getHeight() / 2);

    rectf.set(beginX, beginY, beginX + mCenterIconBitmap.getWidth(), 
            beginY + mCenterIconBitmap.getHeight());
    canvas.drawBitmap(mCenterIconBitmap, null, rectf, bitmapPaint);
    canvas.restore();
}

在上面的绘制中主要是计算大小和透明度来实现绘制中间圆形和文字的效果,现在我们运行的时候可以发现并不是我们所想的可以实现我们想要的效果,这个 onDraw 方法根本不会随着滑动调用,那么怎么办呢?我们可以让滑动的时候来通知自定义 View 的重绘(这里可以做一些优化)。

1
2
3
4
5
6
7
8
9
@Override
public void computeScroll() {
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
    centerViewUpdate();
}

看以看到在 computeScroll 的最后一行我们调用了一个 centerViewUpdate 方法来实现让自定义 View 和上面灰色界面透明度变化的重绘,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private void centerViewUpdate(){
    mCenterView.postInvalidate();
    float alpha = 1 - (float)(Util.getCenterViewPointY(mCenterView) 
            - mViewPointY) / mViewHeight;
    if(alpha < 0){
        mTopView.setVisibility(View.GONE);
    }else{
        mTopView.setVisibility(View.VISIBLE);
        mTopView.setAlpha(alpha);
        mTopView.postInvalidate();
    }
}