我们先来看看优酷的控件是怎么回事?
只响应最后也就是最顶部的卡片的点击事件,如果点击的不是最顶部的卡片那么就先把它放到最顶部,然后在移动到最前面来,反复如次。
知道了这几条那么我们就很好做了。
里面的技术细节可能就是child的放置到前面来的动画问题把。
先看看我们实现得效果:
然后仔细分析一下我们要实现怎么样的效果:
我也是放置了一个按钮和两个view在控件上面,只有当控件在最前面也就是最里面的时候才会响应事件。
然后我们就动手来实现这个控件。
我们继承一个ViewGroup并且命名为ExchageCarldView,最开始的当然是它的onMeasure和onLayout方法了。这里贴出代码然后一一讲解。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { measureChildren(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); if (mIsExchageAnimation) // 动画路径 { for (int i = 0; i < count; i++) { if (mTouchIndex > i) // 当点击的头部以上的不需要改变layout continue; View view = getChildAt(i); // 缓存第一次view的信息,就是动画刚开始的信息 cacheViewTopAndBottomIfneed(i, view); if (count - 1 == i) // 最上层的布局 { // 计算它到底该走多少高度总高度 int total_dis = view.getHeight() / 2 * (count - 1 - mTouchIndex); // 计算当前的线性距离 int now_dis = (int) (total_dis * (System.currentTimeMillis() - mAnimationStartTime) / Default_animtion_time); // 回归不能超过total_dis这个值 int dis = Math.min(now_dis, total_dis); view.layout(view.getLeft(), mViewsTopCache.get(i) + dis, view.getRight(), mViewsBottomCache.get(i) + dis); } else { // 除去最上层的那个那个布局 // 每个卡片都应该移动view.height的1/2 int total_dis = view.getHeight() / 2; // 计算当前的线性距离 int now_dis = (int) (total_dis * (System.currentTimeMillis() - mAnimationStartTime) / Default_animtion_time); // 回归不能超过total_dis这个值 int dis = Math.min(now_dis, total_dis); // 放置布局的位置 view.layout(view.getLeft(), mViewsTopCache.get(i) - dis, view.getRight(), mViewsBottomCache.get(i) - dis); } // 检测动画是否结束 checkAnimation(); } } else { // 初始化的时候初始化我们的卡片 mTotalHight = 0; for (int i = 0; i < count; i++) { View view = getChildAt(i); view.layout(getPaddingLeft(), mTotalHight, view.getMeasuredWidth(), mTotalHight + view.getMeasuredHeight()); mTotalHight += view.getMeasuredHeight() / 2; // 这里取的是一半的布局 } } }
可以看到在onMeasure方法里面我什么也没做,只是调用了自带的测量方法,最主要的就是在onlayout这个方法里面了,可以看到它有两个分支,一个分支是当他动画的时候调用的分支,一个是静止的时候调用的分支。可以看到,我这里取的是高度的一半来作为遮盖的地方,当然可能还有人问我为什么我这里要用layout来做动画呢?这里我先不解答这个问题,先跟着往下面走。里面有个缓存的函数,我们来还是先贴出来。
/** * 缓存view的顶部和底部信息 * * @param i * @param view */ void cacheViewTopAndBottomIfneed(int i, View view) { int viewtop = mViewsTopCache.get(i, -1); if (viewtop == -1) { mViewsTopCache.put(i, view.getTop()); } int viewbttom = mViewsBottomCache.get(i, -1); if (viewbttom == -1) { mViewsBottomCache.put(i, view.getBottom()); } }
为什么我们需要缓存这个?因为在反复的调用layout的时候我们去调用gettop等方法获取的每次都会变化没有一个对齐的点,所以我们需要缓存一下开始移动的初始化位置。
位置都放置好了那么我们就可以来看看我们的Touch事件是怎么处理的了。贴上我们的代码
@Override public boolean dispatchTouchEvent(MotionEvent event) { if (mIsExchageAnimation) // 当有动画的时候我们吃掉这个事件 return true; if (event.getAction() == MotionEvent.ACTION_DOWN) { mTouchIndex = getTouchChildIndex(event); // 获取点击视图的index if (mTouchIndex != -1 && mTouchIndex != getChildCount() - 1) // 点击的是最后的一个的时候不用开启动画 { startAnimation(); } } // return super.dispatchTouchEvent(event); // // 只响应最后一个卡片的点击的事件 if (mTouchIndex == getChildCount() - 1) { return super.dispatchTouchEvent(event); } else { // 其他的点击事件吃掉 return true; } }这里的代码也很简单就是在点击的时候判断是不是在动画如果在动画就返回,然后获取到点击的child的index调用startAnimation开启动画,后面的判断就是判断只相应最后一个卡片的点击事件。
下面也挨着来看看其他两个函数的代码。
/*** * 根据点击的事件获取child的index * * @param event * @return */ int getTouchChildIndex(MotionEvent event) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); Rect r = new Rect(); view.getGlobalVisibleRect(r); r = new Rect(r.left, r.top, r.right, r.bottom - view.getHeight() / 2); // 需要注意的是这里我们是取的上半部分来做点判断 if (r.contains((int) event.getRawX(), (int) event.getRawY())) { return i; } } return -1; }
这个函数就是根据点击的区域来区分点击到哪个孩子上面的,注意的是取的是上半部分来判定。
然后就是我们的开启动画的代码了。
/** * 开始动画 */ private void startAnimation() { mIsExchageAnimation = true; mViewsBottomCache.clear(); mViewsTopCache.clear(); mAnimationStartTime = System.currentTimeMillis(); View view = getChildAt(mTouchIndex); view.bringToFront(); // 这一句代码是主要的代码 timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { mAnimationHandler.sendEmptyMessage(0); } }, 0, 24); }
这里的方法也很简答,初始化一些变量清空缓存,然后开启一个定时任务去发送消息到handler里面其实这个handler什么事情也没有做,只是不停的在调用requstlayout让他去掉用我们的onLayout方法,最主要的一句代码就是view.bringToFront()这句代码就是会把当前的孩子放在顶层来,其实就是放在孩子数组里面的最后一个来,这里就是为什么我们要用onlayout去做动画。我们只需要不停的改变onlayout的位置不需要去管ondraw里面如果绘制,其实底层也是这样绘制的。先绘制前面的孩子,然后在绘制后面。
总结一下这个demo:
1:卡片显示的多少我是直接取的这个控件的一半
2:通过layout来改变动画
3:最重要的就是理解bringtofront里面孩子的排列
4:缓存view的top和bottom
贴上所有代码,注释都应该很详细了。
/** * * @author edsheng * @filename ExchageCarldView.java * @date 2015/3/12 * @version v1.0 */public class ExchageCarldView extends ViewGroup{ private int mTotalHight = 0; // 总高度 private boolean mIsExchageAnimation = false; // 是否在做交换动画 private SparseIntArray mViewsTopCache = new SparseIntArray(); // 卡片顶部边界的cache private SparseIntArray mViewsBottomCache = new SparseIntArray();// 卡片底部边界的cache private long mAnimationStartTime = 0; // 动画开始的时间 private long Default_animtion_time = 250;// 动画时间 private Timer timer; // 动画定时器 private int mTouchIndex = -1;// touchindex Handler mAnimationHandler = new Handler() { public void dispatchMessage(android.os.Message msg) { requestLayout(); // 更新界面布局动画 }; }; public ExchageCarldView(Context context) { super(context); } /** * 缓存view的顶部和底部信息 * * @param i * @param view */ void cacheViewTopAndBottomIfneed(int i, View view) { int viewtop = mViewsTopCache.get(i, -1); if (viewtop == -1) { mViewsTopCache.put(i, view.getTop()); } int viewbttom = mViewsBottomCache.get(i, -1); if (viewbttom == -1) { mViewsBottomCache.put(i, view.getBottom()); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { measureChildren(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /** * 检测并停止动画 */ private void checkAnimation() { // 当时间到了停止动画 if (Math.abs((System.currentTimeMillis() - mAnimationStartTime)) >= Default_animtion_time) { mAnimationHandler.removeMessages(0); timer.cancel(); mIsExchageAnimation = false; // postDelayed(new Runnable() // { // // @Override // public void run() // { // requestLayout(); // } // }, 50); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); if (mIsExchageAnimation) // 动画路径 { for (int i = 0; i < count; i++) { if (mTouchIndex > i) // 当点击的头部以上的不需要改变layout continue; View view = getChildAt(i); // 缓存第一次view的信息,就是动画刚开始的信息 cacheViewTopAndBottomIfneed(i, view); if (count - 1 == i) // 最上层的布局 { // 计算它到底该走多少高度总高度 int total_dis = view.getHeight() / 2 * (count - 1 - mTouchIndex); // 计算当前的线性距离 int now_dis = (int) (total_dis * (System.currentTimeMillis() - mAnimationStartTime) / Default_animtion_time); // 回归不能超过total_dis这个值 int dis = Math.min(now_dis, total_dis); view.layout(view.getLeft(), mViewsTopCache.get(i) + dis, view.getRight(), mViewsBottomCache.get(i) + dis); } else { // 除去最上层的那个那个布局 // 每个卡片都应该移动view.height的1/2 int total_dis = view.getHeight() / 2; // 计算当前的线性距离 int now_dis = (int) (total_dis * (System.currentTimeMillis() - mAnimationStartTime) / Default_animtion_time); // 回归不能超过total_dis这个值 int dis = Math.min(now_dis, total_dis); // 放置布局的位置 view.layout(view.getLeft(), mViewsTopCache.get(i) - dis, view.getRight(), mViewsBottomCache.get(i) - dis); } // 检测动画是否结束 checkAnimation(); } } else { // 初始化的时候初始化我们的卡片 mTotalHight = 0; for (int i = 0; i < count; i++) { View view = getChildAt(i); view.layout(getPaddingLeft(), mTotalHight, view.getMeasuredWidth(), mTotalHight + view.getMeasuredHeight()); mTotalHight += view.getMeasuredHeight() / 2; // 这里取的是一半的布局 } } } /** * 开始动画 */ private void startAnimation() { mIsExchageAnimation = true; mViewsBottomCache.clear(); mViewsTopCache.clear(); mAnimationStartTime = System.currentTimeMillis(); View view = getChildAt(mTouchIndex); view.bringToFront(); // 这一句代码是主要的代码 timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { mAnimationHandler.sendEmptyMessage(0); } }, 0, 24); } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (mIsExchageAnimation) // 当有动画的时候我们吃掉这个事件 return true; if (event.getAction() == MotionEvent.ACTION_DOWN) { mTouchIndex = getTouchChildIndex(event); // 获取点击视图的index if (mTouchIndex != -1 && mTouchIndex != getChildCount() - 1) // 点击的是最后的一个的时候不用开启动画 { startAnimation(); } } // return super.dispatchTouchEvent(event); // // 只响应最后一个卡片的点击的事件 if (mTouchIndex == getChildCount() - 1) { return super.dispatchTouchEvent(event); } else { // 其他的点击事件吃掉 return true; } } /*** * 根据点击的事件获取child的index * * @param event * @return */ int getTouchChildIndex(MotionEvent event) { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); Rect r = new Rect(); view.getGlobalVisibleRect(r); r = new Rect(r.left, r.top, r.right, r.bottom - view.getHeight() / 2); // 需要注意的是这里我们是取的上半部分来做点判断 if (r.contains((int) event.getRawX(), (int) event.getRawY())) { return i; } } return -1; }}最后是测试代码。
public class CardExchageDemo extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); ExchageCarldView exchageView = new ExchageCarldView(this); View view = new View(this); view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 300)); view.setBackgroundColor(Color.YELLOW); exchageView.addView(view); View view1 = new View(this); view1.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 300)); view1.setBackgroundColor(Color.BLUE); exchageView.addView(view1); Button view2 = new Button(this); view2.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(CardExchageDemo.this, "hello", 0).show(); } }); view2.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 300)); view2.setBackgroundColor(Color.RED); view2.setText("hello"); exchageView.addView(view2); exchageView.setBackgroundColor(Color.GREEN); setContentView(exchageView); }}
这里开启下载的传送门:点击这里