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

原文:http://minxiaoming.com/2015/07/24/NiceApp2/ 

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

一、可以侧拉刷新加载的ViewPager

首先需要添加ViewPager,这个是一个可以侧拉加载刷新的ViewPager,这里最美使用的是GitHub上的一个开源项目:Android-PullToRefresh

修改我们的activity_main.xml布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ptr="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#00aac6">
    <com.handmark.pulltorefresh.library.extras.viewpager.PullToRefreshViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="50.0dip"
        ptr:ptrAnimationStyle="rotate_and_anim"
        ptr:ptrDrawable="@drawable/loading_1"
        ptr:ptrMode="both"
        ptr:ptrScrollingWhileRefreshingEnabled="false" />
    <com.shine.niceapp.control.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.control.RhythmLayout>
</RelativeLayout>
  • ptrAnimationStyle设置侧拉到底部或顶部时出现的图案将要执行的动画方式,这里我选择的是旋转动画

  • ptrDrawable设置出现的图案,这里我设置成了loading_1这是从最美应用里拿来的图片

  • ptrMode设置所支持的上拉下拉方式,这里我选择上拉下拉都支持

  • ptrScrollingWhileRefreshingEnabled设置刷新时是否允许ViewPager滚动,这里我选择不允许

接下来就是为ViewPager创建一个适配器,这里使用的适配器是FragmentStatePagerAdapter所以需要一个Fragment,先来看下这个Fragment的布局:fragment_card.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="5dp"
        android:background="@drawable/home_card_bg">
        
    </RelativeLayout>
</FrameLayout>

其实就是一个FrameLayout中套了一层RelativeLayout,当然在最美应用的布局中,RelativeLayout里还有很多的控件的,但是这些对于我们来说并不是重点,所以我仅仅拿了最外层的2层布局,接下来看看这个Fragment中的内容

public class CardFragment extends Fragment {
    public static CardFragment getInstance(Card card) {
        CardFragment fragment = new CardFragment();
        Bundle bundle = new Bundle();
        bundle.putSerializable("card", card);
        fragment.setArguments(bundle);
        return fragment;
    }
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_card, null);
    }
}

同上,在此中本来是有很多加载数据的操作的,getInstance中的card就是数据源,但是对于我们来说并不需要,所以我只是简单的写了个getInstance,复写了onCreateView而已,之后便是适配器了

public class CardPagerAdapter extends FragmentStatePagerAdapter {
    private List<Card> mCardList;
    private List<Fragment> mFragments = new ArrayList();
    public CardPagerAdapter(FragmentManager fragmentManager, List<Card> cardList) {
        super(fragmentManager);
        //使用迭代器遍历List,
        Iterator iterator = cardList.iterator();
        while (iterator.hasNext()) {
            Card card = (Card) iterator.next();
            //得到相应的Fragment实例并添加到List中
            mFragments.add(CardFragment.getInstance(card));
        }
        mCardList = cardList;
    }
    public int getCount() {
        return mFragments.size();
    }
    @Override
    public Fragment getItem(int position) {
        return mFragments.get(position);
    }
}

这只是一些很简单的代码,我想并不需要多说什么,最后修改MainActivity中的代码如下

public class MainActivity extends FragmentActivity {
    /**
     * 钢琴布局
     */
    private RhythmLayout mRhythmLayout;
    /**
     * 钢琴布局的适配器
     */
    private RhythmAdapter mRhythmAdapter;
    /**
     * 接收PullToRefreshViewPager中的ViewPager控件
     */
    private ViewPager mViewPager;
    /**
     * 可以侧拉刷新的ViewPager,其实是一个LinearLayout控件
     */
    private PullToRefreshViewPager mPullToRefreshViewPager;
    /**
     * ViewPager的适配器
     */
    private CardPagerAdapter mPagerAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }
    private void init() {
        //实例化控件
        mRhythmLayout = (RhythmLayout) findViewById(R.id.box_rhythm);
        mPullToRefreshViewPager = (PullToRefreshViewPager) findViewById(R.id.pager);
        //获取PullToRefreshViewPager中的ViewPager控件
        mViewPager = mPullToRefreshViewPager.getRefreshableView();
        //设置钢琴布局的高度 高度为钢琴布局item的宽度+10dp
        int height = (int) mRhythmLayout.getRhythmItemWidth() + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0F, getResources().getDisplayMetrics());
        mRhythmLayout.getLayoutParams().height = height;
        //设置<span style="font-family: Arial;">mPullToRefreshViewPager距离底部的距离为钢琴控件的高
        ((RelativeLayout.LayoutParams) this.mPullToRefreshViewPager.getLayoutParams()).bottomMargin = height;
        List<Card> cardList = new ArrayList<Card>();
        for (int i = 0; i < 30; i++) {
            Card card = new Card();
            cardList.add(card);
        }
        //设置ViewPager的适配器
        mPagerAdapter = new CardPagerAdapter(getSupportFragmentManager(), cardList);
        mViewPager.setAdapter(mPagerAdapter);
        //设置钢琴布局的适配器
        mRhythmAdapter = new RhythmAdapter(this, cardList);
        mRhythmLayout.setAdapter(mRhythmAdapter);
      
    }
}

运行后的效果如下


二、钢琴按钮的滚动动画

在写这个动画之前我们需要分析一下这个动画都有哪些步骤

观察上面的Gif图,当滑动ViewPager页时,底部的钢琴界面首先会进行一次位移,将ViewPager对应的底部Item移动到中心,之后将此Item升起,将之前的Item降下所以我们总共需要3个动画,一个滚动动画,一个升起动画,一个降下动画,而且在上图中可以看出动画的执行实在ViewPager切换后执行的所以我们需要设置它的OnPageChangeListener。在OnPageSelected方法中执行组合动画

在RhythmLayout中添加方法以供外部调用这个组合动画

/**
* 位移到所选中的item位置,并进行相应的动画
*
* @param position 被选中的item位置
*/
public void showRhythmAtPosition(int position) {
    //如果所要移动的位置和上一次一样则退出方法
    if (this.mLastDisplayItemPosition == position)
        return;
    //ScrollView的滚动条位移动画
    Animator scrollAnimator;
    //item的弹起动画
    Animator bounceUpAnimator;
    //item的降下动画
    Animator shootDownAnimator;
    if ((this.mLastDisplayItemPosition < 0) || (mAdapter.getCount() <= 7) || (position <= 3)) {
        //当前要位移到的位置为前3个时或者总的item数量小于7个
        scrollAnimator = scrollToPosition(0, mScrollStartDelayTime, false);
    } else if (mAdapter.getCount() - position <= 3) {
        //当前要位移到的位置为最后3个
        scrollAnimator = scrollToPosition(mAdapter.getCount() - 7, mScrollStartDelayTime, false);
    } else {
        //当前位移到的位置既不是前3个也不是后3个
        scrollAnimator = scrollToPosition(position - 3, mScrollStartDelayTime, false);
    }
    //获取对应item升起动画
    bounceUpAnimator = bounceUpItem(position, false);
    //获取对应item降下动画
    shootDownAnimator = shootDownItem(mLastDisplayItemPosition, false);
    //动画合集 弹起动画和降下动画的组合
    AnimatorSet animatorSet1 = new AnimatorSet();
    if (bounceUpAnimator != null) {
        animatorSet1.playTogether(bounceUpAnimator);
    }
    if (shootDownAnimator != null) {
        animatorSet1.playTogether(shootDownAnimator);
    }
    //3个动画的组合
    AnimatorSet animatorSet2 = new AnimatorSet();
    animatorSet2.playSequentially(new Animator\[\]{scrollAnimator, animatorSet1});
    animatorSet2.start();
    mLastDisplayItemPosition = position;
}

mLastDisplayItemPosition为上次选中的item的位置,mScrollStartDelayTime为动画延迟执行的时间,其他都有详细的注释,并不难理解scrollToPosition()方法中调用的是AnimatorUtils中的moveScrollViewToX()方法它将会移动ScrollView的x轴到指定的位置

修改后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;
    /**
     * 每个图标加上左右2边边距的尺寸
     */
    private int mEdgeSizeForShiftRhythm;
    /**
     * 按下屏幕的时间
     */
    private long mFingerDownTime;
    /**
     * 上一次所选中的item的位置
     */
    private int mLastDisplayItemPosition;
    /**
     * ScrollView滚动动画延迟执行的时间
     */
    private int mScrollStartDelayTime;
    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();
        mLastDisplayItemPosition = -1;
        mScrollStartDelayTime = 0;
    }
    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;
    }
    /**
     * 位移到Y轴'最低'的动画
     *
     * @param viewPosition view的位置
     * @param isStart      是否开始动画
     * @return
     */
    public Animator shootDownItem(int viewPosition, boolean isStart) {
        if ((viewPosition >= 0) && (mLinearLayout != null) && (mLinearLayout.getChildCount() > viewPosition))
            return shootDownItem(mLinearLayout.getChildAt(viewPosition), isStart);
        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);
    }
    /**
     * ScrollView滚动动画X轴位移
     *
     * @param position   view的位置
     * @param startDelay 延迟动画开始时间
     * @param isStart    动画是否开始
     * @return
     */
    public Animator scrollToPosition(int position, int startDelay, boolean isStart) {
        int viewX = (int) mLinearLayout.getChildAt(position).getX();
        return smoothScrollX(viewX, 300, startDelay, isStart);
    }
    private Animator smoothScrollX(int position, int duration, int startDelay, boolean isStart) {
        return AnimatorUtils.moveScrollViewToX(this, position, duration, startDelay, isStart);
    }
    /**
     * 位移到Y轴'最高'的动画
     *
     * @param viewPosition view的位置
     * @param isStart      是否开始动画
     * @return
     */
    public Animator bounceUpItem(int viewPosition, boolean isStart) {
        if (viewPosition >= 0)
            return bounceUpItem(mLinearLayout.getChildAt(viewPosition), isStart);
        return null;
    }
    public Animator bounceUpItem(View view, boolean isStart) {
        if (view != null)
            return AnimatorUtils.showUpAndDownBounce(view, 10, 350, isStart, true);
        return null;
    }
    /**
     * 让移动设备震动
     *
     * @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);
        }
    }
    /**
     * 位移到所选中的item位置,并进行相应的动画
     *
     * @param position 前往的item位置
     */
    public void showRhythmAtPosition(int position) {
        //如果所要移动的位置和上一次一样则退出方法
        if (this.mLastDisplayItemPosition == position)
            return;
        //ScrollView的滚动条位移动画
        Animator scrollAnimator;
        //item的弹起动画
        Animator bounceUpAnimator;
        //item的降下动画
        Animator shootDownAnimator;
        if ((this.mLastDisplayItemPosition < 0) || (mAdapter.getCount() <= 7) || (position <= 3)) {
            //当前要位移到的位置为前3个时或者总的item数量小于7个
            scrollAnimator = scrollToPosition(0, mScrollStartDelayTime, false);
        } else if (mAdapter.getCount() - position <= 3) {
            //当前要位移到的位置为最后3个
            scrollAnimator = scrollToPosition(mAdapter.getCount() - 7, mScrollStartDelayTime, false);
        } else {
            //当前位移到的位置既不是前3个也不是后3个
            scrollAnimator = scrollToPosition(position - 3, mScrollStartDelayTime, false);
        }
        //获取对应item升起动画
        bounceUpAnimator = bounceUpItem(position, false);
        //获取对应item降下动画
        shootDownAnimator = shootDownItem(mLastDisplayItemPosition, false);
        //动画合集 弹起动画和降下动画的组合
        AnimatorSet animatorSet1 = new AnimatorSet();
        if (bounceUpAnimator != null) {
            animatorSet1.playTogether(bounceUpAnimator);
        }
        if (shootDownAnimator != null) {
            animatorSet1.playTogether(shootDownAnimator);
        }
        //3个动画的组合
        AnimatorSet animatorSet2 = new AnimatorSet();
        animatorSet2.playSequentially(new Animator\[\]{scrollAnimator, animatorSet1});
        animatorSet2.start();
        mLastDisplayItemPosition = position;
    }
    /*
     * 得到每个键冒(控件)的宽度
     */
    public float getRhythmItemWidth() {
        return mItemWidth;
    }
    /**
     * 设置滚动动画延迟执行时间
     *
     * @param scrollStartDelayTime 延迟时间毫秒为单位
     */
    public void setScrollRhythmStartDelayTime(int scrollStartDelayTime) {
        this.mScrollStartDelayTime = scrollStartDelayTime;
    }
}

AnimatorUtils的代码如下:

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;
    }
    /**
     * 移动ScrollView的x轴
     * @param view 要移动的ScrollView
     * @param toX  要移动到的X轴坐标
     * @param time 动画持续时间
     * @param delayTime 延迟开始动画的时间
     * @param isStart 是否开始动画
     * @return
     */
    public static Animator moveScrollViewToX(View view, int toX, int time, int delayTime, boolean isStart) {
        ObjectAnimator objectAnimator = ObjectAnimator.ofInt(view, "scrollX", new int\[\]{toX});
        objectAnimator.setDuration(time);
        objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        objectAnimator.setStartDelay(delayTime);
        if (isStart)
            objectAnimator.start();
        return objectAnimator;
    }
}

接下来修改MainActivity中的代码

public class MainActivity extends FragmentActivity {
    /**
     * 钢琴布局
     */
    private RhythmLayout mRhythmLayout;
    /**
     * 钢琴布局的适配器
     */
    private RhythmAdapter mRhythmAdapter;
    /**
     * 接收PullToRefreshViewPager中的ViewPager控件
     */
    private ViewPager mViewPager;
    /**
     * 可以侧拉刷新的ViewPager,其实是一个LinearLayout控件
     */
    private PullToRefreshViewPager mPullToRefreshViewPager;
    /**
     * ViewPager的适配器
     */
    private CardPagerAdapter mPagerAdapter;
    private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }
        @Override
        public void onPageSelected(int position) {
            mRhythmLayout.showRhythmAtPosition(position);
        }
        @Override
        public void onPageScrollStateChanged(int state) {
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }
    private void init() {
        //实例化控件
        mRhythmLayout = (RhythmLayout) findViewById(R.id.box_rhythm);
        mPullToRefreshViewPager = (PullToRefreshViewPager) findViewById(R.id.pager);
        //获取PullToRefreshViewPager中的ViewPager控件
        mViewPager = mPullToRefreshViewPager.getRefreshableView();
        //设置钢琴布局的高度 高度为钢琴布局item的宽度+10dp
        int height = (int) mRhythmLayout.getRhythmItemWidth() + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0F, getResources().getDisplayMetrics());
        mRhythmLayout.getLayoutParams().height = height;
        ((RelativeLayout.LayoutParams) this.mPullToRefreshViewPager.getLayoutParams()).bottomMargin = height;
        List<Card> cardList = new ArrayList<Card>();
        for (int i = 0; i < 30; i++) {
            Card card = new Card();
            cardList.add(card);
        }
        //设置ViewPager的适配器
        mPagerAdapter = new CardPagerAdapter(getSupportFragmentManager(), cardList);
        mViewPager.setAdapter(mPagerAdapter);
        //设置钢琴布局的适配器
        mRhythmAdapter = new RhythmAdapter(this, cardList);
        mRhythmLayout.setAdapter(mRhythmAdapter);
        //设置ViewPager的滚动速度
        setViewPagerScrollSpeed(this.mViewPager, 400);
        //设置控件的监听
        mViewPager.setOnPageChangeListener(onPageChangeListener);
        //设置ScrollView滚动动画延迟执行的时间
        mRhythmLayout.setScrollRhythmStartDelayTime(400);
        //初始化时将第一个键帽弹出
        mRhythmLayout.showRhythmAtPosition(0);
    }
    /**
     * 设置ViewPager的滚动速度,即每个选项卡的切换速度
     * @param viewPager ViewPager控件
     * @param speed     滚动速度,毫秒为单位
     */
    private void setViewPagerScrollSpeed(ViewPager viewPager, int speed) {
        try {
            Field field = ViewPager.class.getDeclaredField("mScroller");
            field.setAccessible(true);
            ViewPagerScroller viewPagerScroller = new ViewPagerScroller(viewPager.getContext(), new OvershootInterpolator(0.6F));
            field.set(viewPager, viewPagerScroller);
            viewPagerScroller.setDuration(speed);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

值得注意的是第78行调用的setViewPagerScrollSpeed方法,这个方法的作用就是放缓ViewPager切换页的速度,否则太快的切换速度会出现不和谐的感觉,这个方法中使用的ViewPagerScroller的代码如下

public class ViewPagerScroller extends Scroller {
    private int mDuration;
    public ViewPagerScroller(Context context) {
        super(context);
    }
    public ViewPagerScroller(Context context, Interpolator interpolator) {
        super(context, interpolator);
    }
    public ViewPagerScroller(Context context, Interpolator interpolator, boolean flywheel) {
        super(context, interpolator, flywheel);
    }
    public void setDuration(int duration) {
        this.mDuration = duration;
    }
    public void startScroll(int startX, int startY, int dx, int dy) {
        super.startScroll(startX, startY, dx, dy, this.mDuration);
    }
}

运行后可以看到切换ViewPager的页卡时底部的钢琴控件就会执行相应的动画,已经将钢琴控件’绑’在了ViewPager上了,但是点击底部的钢琴控件,却并没有执行相应的动画效果,也就是说:可以通过ViewPager控制钢琴控件,但是不能通过钢琴控件控制ViewPager,想要实现这样的相互联系就需要我们在钢琴控件RhythmLayout中监听手指抬起的动作。创建一个抽象接口如下

public abstract interface IRhythmItemListener {
    public abstract void onSelected(int position);
}

在RhythmLayout中添加这个监听的成员变量以及一个set方法,如下代码

/**
* 监听器,监听手指离开屏幕时的位置
*/
private IRhythmItemListener mListener;
/**
* 设置监听器
*/
public void setRhythmListener(IRhythmItemListener listener) {
    mListener = listener;
}

监听手指抬起的动作只要把触发监听放在actionUp()方法中就可以了,修改actionUp()方法

/**
* 手指抬起时将其他钢琴按钮落下,重置到初始位置
*/
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);
    //触发监听
    if (mListener != null)
        mListener.onSelected(lastPosition);
    mCurrentItemPosition = -1;
    //使设备震动
    vibrate(20L);
}

在里仅仅是在33行和34行添加了2段代码,来触发监听,最后我们只要在MainActivity中调用setRhythmListener这个监听器就做好了,修改后的MainActivity如下

public class MainActivity extends FragmentActivity {
    /**
     * 钢琴布局
     */
    private RhythmLayout mRhythmLayout;
    /**
     * 钢琴布局的适配器
     */
    private RhythmAdapter mRhythmAdapter;
    /**
     * 接收PullToRefreshViewPager中的ViewPager控件
     */
    private ViewPager mViewPager;
    /**
     * 可以侧拉刷新的ViewPager,其实是一个LinearLayout控件
     */
    private PullToRefreshViewPager mPullToRefreshViewPager;
    /**
     * ViewPager的适配器
     */
    private CardPagerAdapter mPagerAdapter;
    private IRhythmItemListener iRhythmItemListener = new IRhythmItemListener() {
        @Override
        public void onSelected(final int position) {
            new Handler().postDelayed(new Runnable() {
                public void run() {
                    mViewPager.setCurrentItem(position);
                }
            }, 100L);
        }
    };
    private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }
        @Override
        public void onPageSelected(int position) {
            mRhythmLayout.showRhythmAtPosition(position);
        }
        @Override
        public void onPageScrollStateChanged(int state) {
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }
    private void init() {
        //实例化控件
        mRhythmLayout = (RhythmLayout) findViewById(R.id.box_rhythm);
        mPullToRefreshViewPager = (PullToRefreshViewPager) findViewById(R.id.pager);
        //获取PullToRefreshViewPager中的ViewPager控件
        mViewPager = mPullToRefreshViewPager.getRefreshableView();
        //设置钢琴布局的高度 高度为钢琴布局item的宽度+10dp
        int height = (int) mRhythmLayout.getRhythmItemWidth() + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0F, getResources().getDisplayMetrics());
        mRhythmLayout.getLayoutParams().height = height;
        ((RelativeLayout.LayoutParams) this.mPullToRefreshViewPager.getLayoutParams()).bottomMargin = height;
        List<Card> cardList = new ArrayList<Card>();
        for (int i = 0; i < 30; i++) {
            Card card = new Card();
            cardList.add(card);
        }
        //设置ViewPager的适配器
        mPagerAdapter = new CardPagerAdapter(getSupportFragmentManager(), cardList);
        mViewPager.setAdapter(mPagerAdapter);
        //设置钢琴布局的适配器
        mRhythmAdapter = new RhythmAdapter(this, cardList);
        mRhythmLayout.setAdapter(mRhythmAdapter);
        //设置ViewPager的滚动速度
        setViewPagerScrollSpeed(this.mViewPager, 400);
        //设置控件的监听
        mRhythmLayout.setRhythmListener(iRhythmItemListener);
        mViewPager.setOnPageChangeListener(onPageChangeListener);
        //设置ScrollView滚动动画延迟执行的时间
        mRhythmLayout.setScrollRhythmStartDelayTime(400);
        //初始化时将第一个键帽弹出
        mRhythmLayout.showRhythmAtPosition(0);
    }
    /**
     * 设置ViewPager的滚动速度,即每个选项卡的切换速度
     * @param viewPager ViewPager控件
     * @param speed     滚动速度,毫秒为单位
     */
    private void setViewPagerScrollSpeed(ViewPager viewPager, int speed) {
        try {
            Field field = ViewPager.class.getDeclaredField("mScroller");
            field.setAccessible(true);
            ViewPagerScroller viewPagerScroller = new ViewPagerScroller(viewPager.getContext(), new OvershootInterpolator(0.6F));
            field.set(viewPager, viewPagerScroller);
            viewPagerScroller.setDuration(speed);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

运行后的效果如下

至此RhythmLayou这个自定义控件我们已经完成了,最后的任务就是背景颜色的转换


三、背景颜色的转换

因为每个选项卡的颜色都是不一样的所以我们需要在我们的数据源Card这个类中添加一个背景颜色的属性

public class Card implements Serializable {
    private static final long serialVersionUID = -5376313495678563362L;
    private int backgroundColor;
    public int getBackgroundColor() {
        return backgroundColor;
    }
    public void setBackgroundColor(int backgroundColor) {
        this.backgroundColor = backgroundColor;
    }
}

在MainActivity的init()方法的添加数据的for循环中设置不同的颜色

for (int i = 0; i < 30; i++) {
   Card card = new Card();
   //随机生成颜色值
   card.setBackgroundColor((int) -(Math.random() * (16777216 - 1) + 1));
   mCardList.add(card);
}

为了让背景颜色转换的更加和谐我们需要一个过渡动画在AnimationUtils中添加如下方法

/**
* 将View的背景颜色更改,使背景颜色转换更和谐的过渡动画
* @param view   要改变背景颜色的View
* @param preColor  上个颜色值
* @param currColor 当前颜色值
* @param duration  动画持续时间
*/
public static void showBackgroundColorAnimation(View view, int preColor, int currColor, int duration) {
    ObjectAnimator objectAnimator = ObjectAnimator.ofInt(view, "backgroundColor", new int\[\]{preColor, currColor});
    objectAnimator.setDuration(duration);
    objectAnimator.setEvaluator(new ArgbEvaluator());
    objectAnimator.start();
}

之后只要在初始化的时候设置背景颜色,然后在切换ViewPager的页卡时执行动画就ok了,修改后的MainActivity代码如下

public class MainActivity extends FragmentActivity {
    /**
     * 钢琴布局
     */
    private RhythmLayout mRhythmLayout;
    /**
     * 钢琴布局的适配器
     */
    private RhythmAdapter mRhythmAdapter;
    /**
     * 接收PullToRefreshViewPager中的ViewPager控件
     */
    private ViewPager mViewPager;
    /**
     * 可以侧拉刷新的ViewPager,其实是一个LinearLayout控件
     */
    private PullToRefreshViewPager mPullToRefreshViewPager;
    /**
     * ViewPager的适配器
     */
    private CardPagerAdapter mPagerAdapter;
    /**
     * 最外层的View,为了设置背景颜色而使用
     */
    private View mMainView;
    private List<Card> mCardList;
    /**
     * 记录上一个选项卡的颜色值
     */
    private int mPreColor;
    private IRhythmItemListener iRhythmItemListener = new IRhythmItemListener() {
        @Override
        public void onSelected(final int position) {
            new Handler().postDelayed(new Runnable() {
                public void run() {
                    mViewPager.setCurrentItem(position);
                }
            }, 100L);
        }
    };
    private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        }
        @Override
        public void onPageSelected(int position) {
            int currColor = mCardList.get(position).getBackgroundColor();
            AnimatorUtils.showBackgroundColorAnimation(mMainView, mPreColor, currColor, 400);
            mPreColor = currColor;
            mMainView.setBackgroundColor(mCardList.get(position).getBackgroundColor());
            mRhythmLayout.showRhythmAtPosition(position);
        }
        @Override
        public void onPageScrollStateChanged(int state) {
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }
    private void init() {
        //实例化控件
        mMainView = findViewById(R.id.main_view);
        mRhythmLayout = (RhythmLayout) findViewById(R.id.box_rhythm);
        mPullToRefreshViewPager = (PullToRefreshViewPager) findViewById(R.id.pager);
        //获取PullToRefreshViewPager中的ViewPager控件
        mViewPager = mPullToRefreshViewPager.getRefreshableView();
        //设置钢琴布局的高度 高度为钢琴布局item的宽度+10dp
        int height = (int) mRhythmLayout.getRhythmItemWidth() + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0F, getResources().getDisplayMetrics());
        mRhythmLayout.getLayoutParams().height = height;
        ((RelativeLayout.LayoutParams) this.mPullToRefreshViewPager.getLayoutParams()).bottomMargin = height;
        mCardList = new ArrayList<Card>();
        for (int i = 0; i < 30; i++) {
            Card card = new Card();
            //随机生成颜色值
            card.setBackgroundColor((int) -(Math.random() * (16777216 - 1) + 1));
            mCardList.add(card);
        }
        //设置ViewPager的适配器
        mPagerAdapter = new CardPagerAdapter(getSupportFragmentManager(), mCardList);
        mViewPager.setAdapter(mPagerAdapter);
        //设置钢琴布局的适配器
        mRhythmAdapter = new RhythmAdapter(this, mCardList);
        mRhythmLayout.setAdapter(mRhythmAdapter);
        //设置ViewPager的滚动速度
        setViewPagerScrollSpeed(this.mViewPager, 400);
        //设置控件的监听
        mRhythmLayout.setRhythmListener(iRhythmItemListener);
        mViewPager.setOnPageChangeListener(onPageChangeListener);
        //设置ScrollView滚动动画延迟执行的时间
        mRhythmLayout.setScrollRhythmStartDelayTime(400);
        //初始化时将第一个键帽弹出,并设置背景颜色
        mRhythmLayout.showRhythmAtPosition(0);
        mPreColor = mCardList.get(0).getBackgroundColor();
        mMainView.setBackgroundColor(mPreColor);
    }
    /**
     * 设置ViewPager的滚动速度,即每个选项卡的切换速度
     *
     * @param viewPager ViewPager控件
     * @param speed     滚动速度,毫秒为单位
     */
    private void setViewPagerScrollSpeed(ViewPager viewPager, int speed) {
        try {
            Field field = ViewPager.class.getDeclaredField("mScroller");
            field.setAccessible(true);
            ViewPagerScroller viewPagerScroller = new ViewPagerScroller(viewPager.getContext(), new OvershootInterpolator(0.6F));
            field.set(viewPager, viewPagerScroller);
            viewPagerScroller.setDuration(speed);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

到此已经基本仿照了最美应用的界面了,最后运行后的效果如下