仿最美应用-每日最美 钢琴律动效果(二)
原文: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();
}
}
}
到此已经基本仿照了最美应用的界面了,最后运行后的效果如下