前言
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>
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; } }
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)); } } }
<?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>
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个控件
三、阶梯式动画效果
接下来我们就可以根据不同的触摸的位置进行各种效果的展示,以达到最美应用中的效果,所需要做的是阶梯式的动画,从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); } }
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; }
/** * 手指抬起时将其他钢琴按钮落下,重置到初始位置 */ 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); }
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();
@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; }
/** * 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); } } }