仿最美应用-每日最美 钢琴律动效果(一)

原文:http://minxiaoming.com/2015/07/23/NiceApp1/ 

前言

GitHub:https://github.com/minxiaoming/NiceAppDemo

原先我并不知道有最美应用这么一个app,但是这个app却被我们的产品经理和老板所推崇,每次开会时都要拿出来说一说这个应用什么什么效果做的好,终于有一天,我们的老板突然和他们说要我们做着这个效果试试看,当时真的是差点吐血,无奈之下进行了反编译,通过一些残留的思路和线索捣鼓了出来,可能很多人不知道这个应用,首先我们看下大致的效果


一、界面分析

首先我们来大概的分析一下这个界面的组成,看上图可以大致的将这个页面分为了3块,头部就是一个标题头并不在本篇的范围内,第二块也就是中间的那一块是一个可以侧拉刷新的Viewpager,这里最美使用的是Github上的一个开源项目:Android-PullToRefresh

第三快也就是底部的那一块就是我们的重点内容了,这里最美自己写了一个自定义view,这个view是继承自HorizontalScrollView的,在这个view中重写了dispatchTouchEvent()方法,将ScrollView的触摸事件拦截了下来,自己进行处理。并且这个HorizontalScrollView必须有一个LinearLayout子控件,在这LinearLayout下面有很多子控件(我称为钢琴按钮),但是最多显示7个,其实这个控件更像是一个ListView,每一个钢琴按钮都是一个Item,点击每一个item都会触发相应的事件,所以这里我们每个item也和ListView一样使用BaseAdapter来获取view和设置相应的数据。


二、添加钢琴按钮

首先是item也就是钢琴按钮的布局:adapter_rhythm_icon.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="48dp"
    android:layout_height="100dp"
    android:padding="2.0dip">
    <RelativeLayout
        android:layout_width="48.0dip"
        android:layout_height="100.0dip"
        android:background="@drawable/home_icon_bg"
        android:padding="2dp">
        <com.shine.niceapp.widget.RoundedImageView
            android:id="@id/image_icon"
            android:layout_width="44.0dip"
            android:layout_height="44.0dip"
            app:riv_corner_radius="10dp"
            app:riv_mutate_background="true" />
    </RelativeLayout>
</RelativeLayout>

此处套了2层的RelativeLayout是为了保持每个item之间的间隔,第14行的RoundedImageView是一个自定义的ImageView,这是一个可以设置圆角度数的ImageView,是Github上一个开源的项目

Github链接:https://github.com/vinc3m1/RoundedImageView

之后便是适配器RhythmAdapter的代码了

public class RhythmAdapter extends BaseAdapter {
    private LayoutInflater mInflater;
    private Context mContext;
    /**
     * item的宽度
     */
    private float itemWidth;
    /**
     * 数据源
     */
    private List<Card> mCardList;
    public RhythmAdapter(Context context, List<Card> cardList) {
        this.mContext = context;
        this.mCardList = new ArrayList();
        this.mCardList.addAll(cardList);
        if (context != null)
            this.mInflater = LayoutInflater.from(context);
    }
    public int getCount() {
        return this.mCardList.size();
    }
    public Object getItem(int position) {
        return this.mCardList.get(position);
    }
    @Override
    public long getItemId(int position) {
        return (this.mCardList.get(position)).getUid();
    }
    /**
     * 设置每个item的宽度
     */
    public void setItemWidth(float width) {
        this.itemWidth = width;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        RelativeLayout relativeLayout = (RelativeLayout) this.mInflater.inflate(R.layout.adapter_rhythm_icon, null);
        //设置item布局的大小以及Y轴的位置
        relativeLayout.setLayoutParams(new RelativeLayout.LayoutParams((int) itemWidth, mContext.getResources().getDimensionPixelSize(R.dimen.rhythm_item_height)));
        relativeLayout.setTranslationY(itemWidth);
        //设置第二层RelativeLayout布局的宽和高
        RelativeLayout childRelativeLayout = (RelativeLayout) relativeLayout.getChildAt(0);
        int relativeLayoutWidth = (int) itemWidth - 2 * mContext.getResources().getDimensionPixelSize(R.dimen.rhythm_icon_margin);
        childRelativeLayout.setLayoutParams(new RelativeLayout.LayoutParams(relativeLayoutWidth, mContext.getResources().getDimensionPixelSize(R.dimen.rhythm_item_height) - 2 * mContext.getResources().getDimensionPixelSize(R.dimen.rhythm_icon_margin)));
        ImageView imageIcon = (ImageView) relativeLayout.findViewById(R.id.image_icon);
        //计算ImageView的大小
        int iconSize = (relativeLayoutWidth - 2 * mContext.getResources().getDimensionPixelSize(R.dimen.rhythm_icon_margin));
        ViewGroup.LayoutParams iconParams = imageIcon.getLayoutParams();
        iconParams.width = iconSize;
        iconParams.height = iconSize;
        imageIcon.setLayoutParams(iconParams);
        //设置背景图片
        imageIcon.setBackgroundResource(R.drawable.ic_launcher);
        return relativeLayout;
    }
}

这是一个很简单的适配器的写法,主要的代码都是在getView()这个方法里面,在这里,我们首先计算了Item的大小,并且设置了Y轴的位置,然后是计算出了ImageView控件的大小以及设置背景图片,最后将这个View返回.

最后是我们的重头戏,自定义的HorizontalScrollView,上面的适配器就是为了我们这个自定义控件提供View,那么我们就来看看这个自定义控件

public class RhythmLayout extends HorizontalScrollView {
    /**
     * ScrollView的子控件
     */
    private LinearLayout mLinearLayout;
    /**
     * item的宽度,为屏幕的1/7
     */
    private float mItemWidth;
    /**
     * 屏幕宽度
     */
    private int mScreenWidth;
    /**
     * 适配器
     */    
    private RhythmAdapter mAdapter;
    private Context mContext;
    public RhythmLayout(Context context) {
        this(context, null);
    }
    public RhythmLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }
    private void init() {
        //获得屏幕大小
        DisplayMetrics displayMetrics = new DisplayMetrics();
        ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        mScreenWidth = displayMetrics.widthPixels;
        //获取Item的宽度,为屏幕的七分之一
        mItemWidth = mScreenWidth / 7;
    }
    public void setAdapter(RhythmAdapter adapter) {
        this.mAdapter = adapter;
        //如果获取HorizontalScrollView下的LinearLayout控件
        if (mLinearLayout == null) {
            mLinearLayout = (LinearLayout) getChildAt(0);
        }
        //循环获取adapter中的View,设置item的宽度并且add到mLinearLayout中
        mAdapter.setItemWidth(mItemWidth);
        for (int i = 0; i < this.mAdapter.getCount(); i++) {
            mLinearLayout.addView(mAdapter.getView(i, null, null));
        }
    }
}

在控件初始的init()方法中只是计算了下屏幕的宽度以及item的宽度,init()下面还有个setAdapter()方法,我们在这里面使用getChildAt(0)方法去获取我们这个自定义控件下面的第一个子控件,并且将它强制转换成了LinearLayout,也就是说如果想使用这个自定义的HorizontalScrollView,它下面包含的控件必须为LinearLayout,否则将会出错.

在得到子控件也就是LinearLayout之后,我们做了一个循环在这个循环里我们不断的设置item的宽度并且将其获取后调用了addView方法添加到了mLinearLayout中,到此这个自定义控件需要的item都添加完了。接下来我们需要在Activity里调用使用这个自定义控件

Activity的布局:activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:background="#00aac6"
    android:layout_height="match_parent">
    <com.shine.niceapp.RhythmLayout
        android:id="@+id/box_rhythm"
        android:layout_width="match_parent"
        android:layout_height="@dimen/rhythm_layout_height"
        android:layout_alignParentBottom="true"
        android:scrollbars="none">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal" />
    </com.shine.niceapp.RhythmLayout>
</RelativeLayout>

为了能够明显的看出底部的控件我设置了一个背景颜色,之后是Activity的代码:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RhythmLayout rhythmLayout = (RhythmLayout) findViewById(R.id.box_rhythm);
        List<Card> cardList = new ArrayList<Card>();
        for (int i = 0; i < 30; i++) {
            Card card = new Card();
            cardList.add(card);
        }
        RhythmAdapter adapter = new RhythmAdapter(this, cardList);
        rhythmLayout.setAdapter(adapter);
    }
}

因为我们现在不需要任何数据,我们的图标也是直接从drawable文件夹下获取的,所以这里的Card并没有添加任何数据,只是为了让cardList的size能达到一定的数量,所以这里做了一个循环。到此自定义控件RhythmLayout中的元素已经添加完毕,我们看看下面运行出来后的效果图片可以看到底部有7个控件

blob.png


三、阶梯式动画效果

接下来我们就可以根据不同的触摸的位置进行各种效果的展示,以达到最美应用中的效果,所需要做的是阶梯式的动画,从gif中可以看出其他的item会根据和被手指选中的item的距离升起不同的高度,最终出现阶梯式的效果。代码如下:

public class RhythmLayout extends HorizontalScrollView {
    /**
     * ScrollView的子控件
     */
    private LinearLayout mLinearLayout;
    /**
     * item的宽度,为屏幕的1/7
     */
    private float mItemWidth;
    /**
     * 屏幕宽度
     */
    private int mScreenWidth;
    /**
     * 当前被选中的的Item的位置
     */
    private int mCurrentItemPosition;
    /**
     * 适配器
     */
    private RhythmAdapter mAdapter;
    /**
     * item在Y轴位移的单位,以这个值为基础开始阶梯式位移动画
     */
    private int mIntervalHeight;
    /**
     * item在Y轴位移最大的高度
     */
    private int mMaxTranslationHeight;
    private Context mContext;
    public RhythmLayout(Context context) {
        this(context, null);
    }
    public RhythmLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }
    private void init() {
        //获得屏幕大小
        DisplayMetrics displayMetrics = new DisplayMetrics();
        ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        mScreenWidth = displayMetrics.widthPixels;
        //获取Item的宽度,为屏幕的七分之一
        mItemWidth = mScreenWidth / 7;
        //初始化时将手指当前所在的位置置为-1
        mCurrentItemPosition = -1;
        mMaxTranslationHeight = (int) mItemWidth;
        mIntervalHeight = (mMaxTranslationHeight / 6);
    }
    public void setAdapter(RhythmAdapter adapter) {
        this.mAdapter = adapter;
        //如果获取HorizontalScrollView下的LinearLayout控件
        if (mLinearLayout == null) {
            mLinearLayout = (LinearLayout) getChildAt(0);
        }
        //循环获取adapter中的View,设置item的宽度并且add到mLinearLayout中
        mAdapter.setItemWidth(mItemWidth);
        for (int i = 0; i < this.mAdapter.getCount(); i++) {
            mLinearLayout.addView(mAdapter.getView(i, null, null));
        }
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE://移动
                updateItemHeight(ev.getX());
                break;
        }
        return true;
    }
    //更新钢琴按钮的高度
    private void updateItemHeight(float scrollX) {
        //得到屏幕上可见的7个钢琴按钮的视图
        List viewList = getVisibleViews();
        //当前手指所在的item
        int position = (int) (scrollX / mItemWidth);
        //如果手指位置没有发生变化或者大于childCount的则跳出方法不再继续执行
        if (position == mCurrentItemPosition || position >= mLinearLayout.getChildCount())
            return;
        mCurrentItemPosition = position;
        makeItems(position, viewList);
    }
    /**
     * 得到当前可见的7个钢琴按钮
     */
    private List<View> getVisibleViews() {
        ArrayList arrayList = new ArrayList();
        if (mLinearLayout == null)
            return arrayList;
        //当前可见的第一个钢琴按钮的位置
        int firstPosition = getFirstVisibleItemPosition();
        //当前可见的最后一个钢琴按钮的位置
        int lastPosition = firstPosition + 7;
        if (mLinearLayout.getChildCount() < 7) {
            lastPosition = mLinearLayout.getChildCount();
        }
        //取出当前可见的7个钢琴按钮
        for (int i = firstPosition; i < lastPosition; i++)
            arrayList.add(mLinearLayout.getChildAt(i));
        return arrayList;
    }
    /**
     * 得到可见的第一个钢琴按钮的位置
     */
    public int getFirstVisibleItemPosition() {
        if (mLinearLayout == null) {
            return 0;
        }
        //获取钢琴按钮的数量
        int size = mLinearLayout.getChildCount();
        for (int i = 0; i < size; i++) {
            View view = mLinearLayout.getChildAt(i);
            //当出现钢琴按钮的x轴比当前ScrollView的x轴大时,这个钢琴按钮就是当前可见的第一个
            if (getScrollX() < view.getX() + mItemWidth / 2.0F)
                return i;
        }
        return 0;
    }
    /**
     * 计算出个个钢琴按钮需要的高度并开始动画
     */
    private void makeItems(int fingerPosition, List<View> viewList) {
        if (fingerPosition >= viewList.size()) {
            return;
        }
        int size = viewList.size();
        for (int i = 0; i < size; i++) {
            //根据钢琴按钮的位置计算出在Y轴需要位移的大小
            int translationY = Math.min(Math.max(Math.abs(fingerPosition - i) * mIntervalHeight, 10), mMaxTranslationHeight);
            //位移动画
            updateItemHeightAnimator(viewList.get(i), translationY);
        }
    }
    /**
     * 根据给定的值进行Y轴位移的动画
     *
     * @param view
     * @param translationY
     */
    private void updateItemHeightAnimator(View view, int translationY) {
        if (view != null)
            AnimatorUtils.showUpAndDownBounce(view, translationY, 180, true, true);
    }
}

在这里我们重写了dispatchTouchEvent()方法并且做了手势识别,当手指在屏幕上移动时将进入updateItemHeight()方法更新个个钢琴按钮的高度最终使用的动画是在AnimatorUtils类下的ShowUpAndDownBounce里,代码如下:

public class AnimatorUtils {
    /**
     * @param view                需要设置动画的view
     * @param translationY        偏移量
     * @param animatorTime        动画时间
     * @param isStartAnimator     是否开启指示器
     * @param isStartInterpolator 是否开始动画
     * @return 平移动画
     */
    public static Animator showUpAndDownBounce(View view, int translationY, int animatorTime, boolean isStartAnimator, boolean isStartInterpolator) {
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "translationY", translationY);
        if (isStartInterpolator) {
            objectAnimator.setInterpolator(new OvershootInterpolator());
        }
        objectAnimator.setDuration(animatorTime);
        if (isStartAnimator) {
            objectAnimator.start();
        }
        return objectAnimator;
    }
}

四、钢琴按钮回落

手指按下屏幕时,这些钢琴按钮会升起,当手指离开屏幕时,这些钢琴会回落,想要实现这些效果就需要我们在dispatchTouchEvent()中拦截ACTION_DOWN和ACTION_UP,根据不同的Action来实现想要的效果,修改dispatchTouchEvent()如下:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE://移动
            updateItemHeight(ev.getX());
            break;
        case MotionEvent.ACTION_DOWN://按下
            updateItemHeight(ev.getX());
            break;
        case MotionEvent.ACTION_UP://抬起
            actionUp();
            break;
    }
    return true;
}

当ev.getAction为ACTION.DOWN时,所做的事情是和ACTION_MOVE一样的都是按照不同的位置升起不同的高度,而当手指离开屏幕时,也就是ev.getAction为ACTION_UP时调用了actionUp()方法,代码如下:

/**
* 手指抬起时将其他钢琴按钮落下,重置到初始位置
*/
private void actionUp() {
    if (mCurrentItemPosition < 0) {
        return;
    }
    //得到当前可见第一个钢琴按钮的位置
    int firstPosition = getFirstVisibleItemPosition();
    //得到当前可见第二个钢琴按钮的位置
    int lastPosition = firstPosition + mCurrentItemPosition;
    final List viewList = getVisibleViews();
    int size = viewList.size();
    //将当前钢琴按钮从要落下的ViewList中删除
    if (size > mCurrentItemPosition) {
        viewList.remove(mCurrentItemPosition);
    }
    if (firstPosition - 1 >= 0) {
        viewList.add(mLinearLayout.getChildAt(firstPosition - 1));
    }
    if (lastPosition + 1 <= mLinearLayout.getChildCount()) {
        viewList.add(mLinearLayout.getChildAt(lastPosition + 1));
    }
    //200毫秒后执行动画
    this.mHandler.postDelayed(new Runnable() {
        public void run() {
            for (int i = 0; i < viewList.size(); i++) {
                View downView = (View) viewList.get(i);
                shootDownItem(downView, true);
            }
        }
    }, 200L);
    mCurrentItemPosition = -1;
    //使设备震动
    vibrate(20L);
}

可以看到在第16行删除了当前按下位置的钢琴按钮时其不会落回初始位置保持升起状态,29行的方法是一个动画,让钢琴按钮回到初始位置,在33行,因为手指已经离开屏幕所以将mCurrentItemPosition设为-1,35行的方法是让设备震动。修改后的RhythmLayout的完整代码如下:

public class RhythmLayout extends HorizontalScrollView {
    /**
     * ScrollView的子控件
     */
    private LinearLayout mLinearLayout;
    /**
     * item的宽度,为屏幕的1/7
     */
    private float mItemWidth;
    /**
     * 屏幕宽度
     */
    private int mScreenWidth;
    /**
     * 当前被选中的的Item的位置
     */
    private int mCurrentItemPosition;
    /**
     * 适配器
     */
    private RhythmAdapter mAdapter;
    /**
     * item在Y轴位移的单位,以这个值为基础开始阶梯式位移动画
     */
    private int mIntervalHeight;
    /**
     * item在Y轴位移最大的高度
     */
    private int mMaxTranslationHeight;
    private Context mContext;
    private Handler mHandler;
    public RhythmLayout(Context context) {
        this(context, null);
    }
    public RhythmLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }
    private void init() {
        //获得屏幕大小
        DisplayMetrics displayMetrics = new DisplayMetrics();
        ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        mScreenWidth = displayMetrics.widthPixels;
        //获取Item的宽度,为屏幕的七分之一
        mItemWidth = mScreenWidth / 7;
        //初始化时将手指当前所在的位置置为-1
        mCurrentItemPosition = -1;
        mMaxTranslationHeight = (int) mItemWidth;
        mIntervalHeight = (mMaxTranslationHeight / 6);
        mHandler = new Handler();
    }
    public void setAdapter(RhythmAdapter adapter) {
        this.mAdapter = adapter;
        //如果获取HorizontalScrollView下的LinearLayout控件
        if (mLinearLayout == null) {
            mLinearLayout = (LinearLayout) getChildAt(0);
        }
        //循环获取adapter中的View,设置item的宽度并且add到mLinearLayout中
        mAdapter.setItemWidth(mItemWidth);
        for (int i = 0; i < this.mAdapter.getCount(); i++) {
            mLinearLayout.addView(mAdapter.getView(i, null, null));
        }
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE://移动
                updateItemHeight(ev.getX());
                break;
            case MotionEvent.ACTION_DOWN://按下
                updateItemHeight(ev.getX());
                break;
            case MotionEvent.ACTION_UP://抬起
                actionUp();
                break;
        }
        return true;
    }
    //更新钢琴按钮的高度
    private void updateItemHeight(float scrollX) {
        //得到屏幕上可见的7个钢琴按钮的视图
        List viewList = getVisibleViews();
        //当前手指所在的item
        int position = (int) (scrollX / mItemWidth);
        //如果手指位置没有发生变化或者大于childCount的则跳出方法不再继续执行
        if (position == mCurrentItemPosition || position >= mLinearLayout.getChildCount())
            return;
        mCurrentItemPosition = position;
        makeItems(position, viewList);
    }
    /**
     * 得到当前可见的7个钢琴按钮
     */
    private List<View> getVisibleViews() {
        ArrayList arrayList = new ArrayList();
        if (mLinearLayout == null)
            return arrayList;
        //当前可见的第一个钢琴按钮的位置
        int firstPosition = getFirstVisibleItemPosition();
        //当前可见的最后一个钢琴按钮的位置
        int lastPosition = firstPosition + 7;
        if (mLinearLayout.getChildCount() < 7) {
            lastPosition = mLinearLayout.getChildCount();
        }
        //取出当前可见的7个钢琴按钮
        for (int i = firstPosition; i < lastPosition; i++)
            arrayList.add(mLinearLayout.getChildAt(i));
        return arrayList;
    }
    /**
     * 得到可见的第一个钢琴按钮的位置
     */
    public int getFirstVisibleItemPosition() {
        if (mLinearLayout == null) {
            return 0;
        }
        //获取钢琴按钮的数量
        int size = mLinearLayout.getChildCount();
        for (int i = 0; i < size; i++) {
            View view = mLinearLayout.getChildAt(i);
            //当出现钢琴按钮的x轴比当前ScrollView的x轴大时,这个钢琴按钮就是当前可见的第一个
            if (getScrollX() < view.getX() + mItemWidth / 2.0F)
                return i;
        }
        return 0;
    }
    /**
     * 计算出个个钢琴按钮需要的高度并开始动画
     */
    private void makeItems(int fingerPosition, List<View> viewList) {
        if (fingerPosition >= viewList.size()) {
            return;
        }
        int size = viewList.size();
        for (int i = 0; i < size; i++) {
            //根据钢琴按钮的位置计算出在Y轴需要位移的大小
            int translationY = Math.min(Math.max(Math.abs(fingerPosition - i) * mIntervalHeight, 10), mMaxTranslationHeight);
            //位移动画
            updateItemHeightAnimator(viewList.get(i), translationY);
        }
    }
    /**
     * 根据给定的值进行Y轴位移的动画
     *
     * @param view
     * @param translationY
     */
    private void updateItemHeightAnimator(View view, int translationY) {
        if (view != null)
            AnimatorUtils.showUpAndDownBounce(view, translationY, 180, true, true);
    }
    /**
     * 手指抬起时将其他钢琴按钮落下,重置到初始位置
     */
    private void actionUp() {
        if (mCurrentItemPosition < 0) {
            return;
        }
        int firstPosition = getFirstVisibleItemPosition();
        int lastPosition = firstPosition + mCurrentItemPosition;
        final List viewList = getVisibleViews();
        int size = viewList.size();
        //将当前钢琴按钮从要落下的ViewList中删除
        if (size > mCurrentItemPosition) {
            viewList.remove(mCurrentItemPosition);
        }
        if (firstPosition - 1 >= 0) {
            viewList.add(mLinearLayout.getChildAt(firstPosition - 1));
        }
        if (lastPosition + 1 <= mLinearLayout.getChildCount()) {
            viewList.add(mLinearLayout.getChildAt(lastPosition + 1));
        }
        //200毫秒后执行动画
        this.mHandler.postDelayed(new Runnable() {
            public void run() {
                for (int i = 0; i < viewList.size(); i++) {
                    View downView = (View) viewList.get(i);
                    shootDownItem(downView, true);
                }
            }
        }, 200L);
        mCurrentItemPosition = -1;
        //使设备震动
        vibrate(20L);
    }
    /**
     * 位移到Y轴'最低'的动画
     *
     * @param view    需要执行动画的视图
     * @param isStart 是否开始动画
     * @return
     */
    public Animator shootDownItem(View view, boolean isStart) {
        if (view != null)
            return AnimatorUtils.showUpAndDownBounce(view, mMaxTranslationHeight, 350, isStart, true);
        return null;
    }
    /**
     * 让移动设备震动
     *
     * @param l 震动的时间
     */
    private void vibrate(long l) {
        ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(new long\[\]{0L, l}, -1);
    }
}

运行后的效果如下


五、爬楼梯式动画效果

当长按第一个钢琴按钮或者最后一个钢琴按钮时,整个控件就会出现一个类似爬楼梯一层层的上去的效果,这个效果是使用计时器Timer来实现的,代码如下:

/**
* 计时器,实现爬楼梯效果
*/
class ShiftMonitorTimer extends Timer {
    private TimerTask timerTask;
    
    private boolean canShift = false;
    private float x;
    private float y;
    void monitorTouchPosition(float x, float y) {
        this.x = x;
        this.y = y;
        //当按下位置在第一个后最后一个,或x<0,y<0时,canShift为false,使计时器线程中的代码不能执行
        if ((x < 0.0F) || ((x > mEdgeSizeForShiftRhythm) && (x < mScreenWidth - mEdgeSizeForShiftRhythm)) || (y < 0.0F)) {
            mFingerDownTime = System.currentTimeMillis();
            canShift = false;
        } else {
            canShift = true;
        }
    }
    void startMonitor() {
        if (this.timerTask == null) {
            timerTask = new TimerTask() {
                @Override
                public void run() {
                    long duration = System.currentTimeMillis() - mFingerDownTime;
                    //按下时间大于1秒,且按下的是第一个或者最后一个等式成立
                    if (canShift && duration > 1000) {
                        int firstPosition = getFirstVisibleItemPosition();
                        int toPosition = 0; //要移动到的钢琴按钮的位置
                        boolean isForward = false; //是否获取第firstPosition-1个钢琴按钮
                        boolean isBackward = false;//是否获取第lastPosition+1个钢琴按钮
                        final List<View> localList;
                        if (x <= mEdgeSizeForShiftRhythm && x >= 0.0F) {//第一个
                            if (firstPosition - 1 >= 0) {
                                mCurrentItemPosition = 0;
                                toPosition = firstPosition - 1;
                                isForward = true;
                                isBackward = false;
                            }
                        } else if (x > mScreenWidth - mEdgeSizeForShiftRhythm) {//最后一个
                            if (mLinearLayout.getChildCount() >= 1 + (firstPosition + 7)) {
                                mCurrentItemPosition = 7;
                                toPosition = firstPosition + 1;
                                isForward = false;
                                isBackward = true;
                            }
                        }
                        //当按下的是第一个的时候isForward为true,最后一个时isBackward为true
                        if (isForward || isBackward) {
                            localList = getVisibleViews(isForward, isBackward);
                            final int finalToPosition = toPosition;
                            mHandler.post(new Runnable() {
                                public void run() {
                                    makeItems(mCurrentItemPosition, localList);//设置每个Item的高度
                                    scrollToPosition(finalToPosition, 200, 0, true);//设置ScrollView在x轴的坐标
                                    vibrate(10L);
                                }
                            });
                        }
                    }
                }
            };
        }
        //200毫秒之后开始执行,每隔250毫秒执行一次
        schedule(timerTask, 200L, 250L);
    }
}

之后在控件初始化的时候一起初初始化这个计时器

mTimer = new ShiftMonitorTimer();
mTimer.startMonitor();

当用户长按钢琴按钮时执行计时器,修改dispatchTouchEvent(),如下

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE://移动
            mTimer.monitorTouchPosition(ev.getX(), ev.getY());
            updateItemHeight(ev.getX());
            break;
        case MotionEvent.ACTION_DOWN://按下
            mTimer.monitorTouchPosition(ev.getX(), ev.getY());
            //得到按下时的时间戳
            mFingerDownTime = System.currentTimeMillis();
            updateItemHeight(ev.getX());
            break;
        case MotionEvent.ACTION_UP://抬起
            actionUp();
            break;
    }
    return true;
}

自定义控件RhythmLayout全部代码如下

/**
 * User: shine
 * Date: 2015-01-14
 * Time: 11:50
 * Description:
 */
public class RhythmLayout extends HorizontalScrollView {
    /**
     * ScrollView的子控件
     */
    private LinearLayout mLinearLayout;
    /**
     * item的宽度,为屏幕的1/7
     */
    private float mItemWidth;
    /**
     * 屏幕宽度
     */
    private int mScreenWidth;
    /**
     * 当前被选中的的Item的位置
     */
    private int mCurrentItemPosition;
    /**
     * 适配器
     */
    private RhythmAdapter mAdapter;
    /**
     * item在Y轴位移的单位,以这个值为基础开始阶梯式位移动画
     */
    private int mIntervalHeight;
    /**
     * item在Y轴位移最大的高度
     */
    private int mMaxTranslationHeight;
    /**
     * 每个图标加上左右2边边距的尺寸
     */
    private int mEdgeSizeForShiftRhythm;
    /**
     * 按下屏幕的时间
     */
    private long mFingerDownTime;
    private Context mContext;
    private Handler mHandler;
    private ShiftMonitorTimer mTimer;
    public RhythmLayout(Context context) {
        this(context, null);
    }
    public RhythmLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        init();
    }
    private void init() {
        //获得屏幕大小
        DisplayMetrics displayMetrics = new DisplayMetrics();
        ((Activity) mContext).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
        mScreenWidth = displayMetrics.widthPixels;
        //获取Item的宽度,为屏幕的七分之一
        mItemWidth = mScreenWidth / 7;
        //初始化时将手指当前所在的位置置为-1
        mCurrentItemPosition = -1;
        mMaxTranslationHeight = (int) mItemWidth;
        mIntervalHeight = (mMaxTranslationHeight / 6);
        mEdgeSizeForShiftRhythm = getResources().getDimensionPixelSize(R.dimen.rhythm_edge_size_for_shift);
        mFingerDownTime = 0;
        mHandler = new Handler();
        mTimer = new ShiftMonitorTimer();
        mTimer.startMonitor();
    }
    public void setAdapter(RhythmAdapter adapter) {
        this.mAdapter = adapter;
        //如果获取HorizontalScrollView下的LinearLayout控件
        if (mLinearLayout == null) {
            mLinearLayout = (LinearLayout) getChildAt(0);
        }
        //循环获取adapter中的View,设置item的宽度并且add到mLinearLayout中
        mAdapter.setItemWidth(mItemWidth);
        for (int i = 0; i < this.mAdapter.getCount(); i++) {
            mLinearLayout.addView(mAdapter.getView(i, null, null));
        }
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE://移动
                mTimer.monitorTouchPosition(ev.getX(), ev.getY());
                updateItemHeight(ev.getX());
                break;
            case MotionEvent.ACTION_DOWN://按下
                mTimer.monitorTouchPosition(ev.getX(), ev.getY());
                //得到按下时的时间戳
                mFingerDownTime = System.currentTimeMillis();
                updateItemHeight(ev.getX());
                break;
            case MotionEvent.ACTION_UP://抬起
                actionUp();
                break;
        }
        return true;
    }
    //更新钢琴按钮的高度
    private void updateItemHeight(float scrollX) {
        //得到屏幕上可见的7个钢琴按钮的视图
        List viewList = getVisibleViews();
        //当前手指所在的item
        int position = (int) (scrollX / mItemWidth);
        //如果手指位置没有发生变化或者大于childCount的则跳出方法不再继续执行
        if (position == mCurrentItemPosition || position >= mLinearLayout.getChildCount())
            return;
        mCurrentItemPosition = position;
        makeItems(position, viewList);
    }
    /**
     * 得到当前可见的7个钢琴按钮
     */
    private List<View> getVisibleViews() {
        ArrayList arrayList = new ArrayList();
        if (mLinearLayout == null)
            return arrayList;
        //当前可见的第一个钢琴按钮的位置
        int firstPosition = getFirstVisibleItemPosition();
        //当前可见的最后一个钢琴按钮的位置
        int lastPosition = firstPosition + 7;
        if (mLinearLayout.getChildCount() < 7) {
            lastPosition = mLinearLayout.getChildCount();
        }
        //取出当前可见的7个钢琴按钮
        for (int i = firstPosition; i < lastPosition; i++)
            arrayList.add(mLinearLayout.getChildAt(i));
        return arrayList;
    }
    /**
     * 获得firstPosition-1 和 lastPosition +1 在当前可见的7个总共9个钢琴按钮
     *
     * @param isForward  是否获取firstPosition - 1 位置的钢琴按钮
     * @param isBackward 是否获取lastPosition + 1 位置的钢琴按钮
     * @return
     */
    private List<View> getVisibleViews(boolean isForward, boolean isBackward) {
        ArrayList viewList = new ArrayList();
        if (this.mLinearLayout == null)
            return viewList;
        int firstPosition = getFirstVisibleItemPosition();
        int lastPosition = firstPosition + 7;
        if (mLinearLayout.getChildCount() < 7) {
            lastPosition = mLinearLayout.getChildCount();
        }
        if ((isForward) && (firstPosition > 0))
            firstPosition--;
        if ((isBackward) && (lastPosition < mLinearLayout.getChildCount()))
            lastPosition++;
        for (int i = firstPosition; i < lastPosition; i++)
            viewList.add(mLinearLayout.getChildAt(i));
        return viewList;
    }
    /**
     * 得到可见的第一个钢琴按钮的位置
     */
    public int getFirstVisibleItemPosition() {
        if (mLinearLayout == null) {
            return 0;
        }
        //获取钢琴按钮的数量
        int size = mLinearLayout.getChildCount();
        for (int i = 0; i < size; i++) {
            View view = mLinearLayout.getChildAt(i);
            //当出现钢琴按钮的x轴比当前ScrollView的x轴大时,这个钢琴按钮就是当前可见的第一个
            if (getScrollX() < view.getX() + mItemWidth / 2.0F)
                return i;
        }
        return 0;
    }
    /**
     * 计算出个个钢琴按钮需要的高度并开始动画
     */
    private void makeItems(int fingerPosition, List<View> viewList) {
        if (fingerPosition >= viewList.size()) {
            return;
        }
        int size = viewList.size();
        for (int i = 0; i < size; i++) {
            //根据钢琴按钮的位置计算出在Y轴需要位移的大小
            int translationY = Math.min(Math.max(Math.abs(fingerPosition - i) * mIntervalHeight, 10), mMaxTranslationHeight);
            //位移动画
            updateItemHeightAnimator(viewList.get(i), translationY);
        }
    }
    /**
     * 根据给定的值进行Y轴位移的动画
     *
     * @param view
     * @param translationY
     */
    private void updateItemHeightAnimator(View view, int translationY) {
        if (view != null)
            AnimatorUtils.showUpAndDownBounce(view, translationY, 180, true, true);
    }
    /**
     * 手指抬起时将其他钢琴按钮落下,重置到初始位置
     */
    private void actionUp() {
        mTimer.monitorTouchPosition(-1.0F, -1.0F);
        if (mCurrentItemPosition < 0) {
            return;
        }
        int firstPosition = getFirstVisibleItemPosition();
        int lastPosition = firstPosition + mCurrentItemPosition;
        final List viewList = getVisibleViews();
        int size = viewList.size();
        //将当前钢琴按钮从要落下的ViewList中删除
        if (size > mCurrentItemPosition) {
            viewList.remove(mCurrentItemPosition);
        }
        if (firstPosition - 1 >= 0) {
            viewList.add(mLinearLayout.getChildAt(firstPosition - 1));
        }
        if (lastPosition + 1 <= mLinearLayout.getChildCount()) {
            viewList.add(mLinearLayout.getChildAt(lastPosition + 1));
        }
        //200毫秒后执行动画
        this.mHandler.postDelayed(new Runnable() {
            public void run() {
                for (int i = 0; i < viewList.size(); i++) {
                    View downView = (View) viewList.get(i);
                    shootDownItem(downView, true);
                }
            }
        }, 200L);
        mCurrentItemPosition = -1;
        //使设备震动
        vibrate(20L);
    }
    /**
     * 位移到Y轴'最低'的动画
     *
     * @param view    需要执行动画的视图
     * @param isStart 是否开始动画
     * @return
     */
    public Animator shootDownItem(View view, boolean isStart) {
        if (view != null)
            return AnimatorUtils.showUpAndDownBounce(view, mMaxTranslationHeight, 350, isStart, true);
        return null;
    }
    /**
     * @param position   要移动到的view的位置
     * @param duration   动画持续时间
     * @param startDelay 延迟动画开始时间
     * @param isStart    动画是否开始
     * @return
     */
    public Animator scrollToPosition(int position, int duration, int startDelay, boolean isStart) {
        int viewX = (int) mLinearLayout.getChildAt(position).getX();
        return smoothScrollX(viewX, duration, startDelay, isStart);
    }
    private Animator smoothScrollX(int position, int duration, int startDelay, boolean isStart) {
        return AnimatorUtils.moveScrollViewToX(this, position, duration, startDelay, isStart);
    }
    /**
     * 让移动设备震动
     *
     * @param l 震动的时间
     */
    private void vibrate(long l) {
        ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE)).vibrate(new long\[\]{0L, l}, -1);
    }
    /**
     * 计时器,实现爬楼梯效果
     */
    class ShiftMonitorTimer extends Timer {
        private TimerTask timerTask;
        /**
         *
         */
        private boolean canShift = false;
        private float x;
        private float y;
        void monitorTouchPosition(float x, float y) {
            this.x = x;
            this.y = y;
            //当按下位置在第一个后最后一个,或x<0,y<0时,canShift为false,使计时器线程中的代码不能执行
            if ((x < 0.0F) || ((x > mEdgeSizeForShiftRhythm) && (x < mScreenWidth - mEdgeSizeForShiftRhythm)) || (y < 0.0F)) {
                mFingerDownTime = System.currentTimeMillis();
                canShift = false;
            } else {
                canShift = true;
            }
        }
        void startMonitor() {
            if (this.timerTask == null) {
                timerTask = new TimerTask() {
                    @Override
                    public void run() {
                        long duration = System.currentTimeMillis() - mFingerDownTime;
                        //按下时间大于1秒,且按下的是第一个或者最后一个等式成立
                        if (canShift && duration > 1000) {
                            int firstPosition = getFirstVisibleItemPosition();
                            int toPosition = 0; //要移动到的钢琴按钮的位置
                            boolean isForward = false; //是否获取第firstPosition-1个钢琴按钮
                            boolean isBackward = false;//是否获取第lastPosition+1个钢琴按钮
                            final List<View> localList;
                            if (x <= mEdgeSizeForShiftRhythm && x >= 0.0F) {//第一个
                                if (firstPosition - 1 >= 0) {
                                    mCurrentItemPosition = 0;
                                    toPosition = firstPosition - 1;
                                    isForward = true;
                                    isBackward = false;
                                }
                            } else if (x > mScreenWidth - mEdgeSizeForShiftRhythm) {//最后一个
                                if (mLinearLayout.getChildCount() >= 1 + (firstPosition + 7)) {
                                    mCurrentItemPosition = 7;
                                    toPosition = firstPosition + 1;
                                    isForward = false;
                                    isBackward = true;
                                }
                            }
                            //当按下的是第一个的时候isForward为true,最后一个时isBackward为true
                            if (isForward || isBackward) {
                                localList = getVisibleViews(isForward, isBackward);
                                final int finalToPosition = toPosition;
                                mHandler.post(new Runnable() {
                                    public void run() {
                                        makeItems(mCurrentItemPosition, localList);//设置每个Item的高度
                                        scrollToPosition(finalToPosition, 200, 0, true);//设置ScrollView在x轴的坐标
                                        vibrate(10L);
                                    }
                                });
                            }
                        }
                    }
                };
            }
            //200毫秒之后开始执行,每隔250毫秒执行一次
            schedule(timerTask, 200L, 250L);
        }
    }
}

最后的效果如下: