我们先来看看优酷的控件是怎么回事?

仅仅响应最后也就是最顶部的卡片的点击事件,假设点击的不是最顶部的卡片那么就先把它放到最顶部。然后在移动到最前面来。重复如次。

知道了这几条那么我们就非常好做了。

里面的技术细节可能就是child的放置到前面来的动画问题把。

先看看我们实现得效果:

然后细致分析一下我们要实现怎么样的效果:

我也是放置了一个button和两个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;
}
}

这里的代码也非常easy就是在点击的时候推断是不是在动画假设在动画就返回,然后获取到点击的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);
}
}

这里开启下载的传送门:点击这里

android自己定义控件系列教程-----仿新版优酷评论剧集卡片滑动控件的更多相关文章

  1. 强烈推荐android初学者,android进阶者看看这个系列教程

    强烈推荐android初学者,android进阶者看看这个系列教程 转载 2015年05月30日 23:05:44 695 为什么要研究Android,是因为它够庞大,它够复杂,他激起了我作为一个程序 ...

  2. android自己定义控件系列教程----视图

    理解android视图 对于android设备我们所示区域事实上和它在底层的绘制有着非常大的关系,非常多时候我们都仅仅关心我们所示,那么在底层一点它究竟是怎么样的一个东西呢?让我们先来看看这个图. w ...

  3. webpack4 系列教程(六): 处理SCSS

    这节课讲解webpack4中处理scss.只需要在处理css的配置上增加编译scss的 LOADER 即可.了解更多处理css的内容 >>> >>> 本节课源码 & ...

  4. sentinel 集群流控原理

    为什么需要集群流控呢?假设需要将某个API的总qps限制在100,机器数可能为50,这时很自然的想到使用一个专门的server来统计总的调用量,其他实例与该server通信来判断是否可以调用,这就是基 ...

  5. Sentinel 发布里程碑版本,添加集群流控功能

    自去年10月底发布GA版本后,Sentinel在近期发布了另一个里程碑版本v1.4(最新的版本号是v1.4.1),加入了开发者关注的集群流控功能. 集群流控简介 为什么要使用集群流控呢?假设我们希望给 ...

  6. Android自己定义控件系列五:自己定义绚丽水波纹效果

    尊重原创!转载请注明出处:http://blog.csdn.net/cyp331203/article/details/41114551 今天我们来利用Android自己定义控件实现一个比較有趣的效果 ...

  7. Android自己定义控件系列案例【五】

    案例效果: 案例分析: 在开发银行相关client的时候或者开发在线支付相关client的时候常常要求用户绑定银行卡,当中银行卡号一般须要空格分隔显示.最常见的就是每4位数以空格进行分隔.以方便用户实 ...

  8. Android控件系列之CheckBox

    学习目的: 1.掌握在Android中如何建立CheckBox 2.掌握CheckBox的常用属性 3.掌握CheckBox选中状态变换的事件(监听器) CheckBox简介: CheckBox和Bu ...

  9. Android控件GridView之仿支付宝钱包首页带有分割线的GridView九宫格的完美实现

    Android控件GridView之仿支付宝钱包首页带有分割线的GridView九宫格的完美实现 2015-03-10 22:38 28419人阅读 评论(17) 收藏 举报  分类: Android ...

随机推荐

  1. post登录资料备份

    # coding=utf-8 import urllib import hashlib import http.client import http.cookiejar import http.coo ...

  2. Mybatis 在 insert 之后想获取自增的主键 id,但却总是返回1

    记录一次傻逼的问题, 自己把自己蠢哭:Mybatis 在 insert 之后想获取自增的主键 id,但却总是返回1 错误说明: 返回的1是影响的行数,并不是自增的主键id: 想要获取自增主键id,需要 ...

  3. 连接服务器的mysql

    在服务器配置好Mysql 数据库,在客户端连接,报错: 解决方法: 1.在MySQL 数据库中修改user表,将host 中的localhoust 改为 %: 2.配置访问数据库的全选 根据需要配置权 ...

  4. Java中PrintStream(打印输出流)

    Java中PrintStream(打印输出流)   PrintStream 是打印输出流,它继承于FilterOutputStream. PrintStream 是用来装饰其它输出流.它能为其他输出流 ...

  5. 使用WAS寄宿net.tcp WCF服務

    首先添加Windows Features 確保打開以下服務 Net.Tcp Listener Adapter Net.Tcp Port Sharing Service Windows Process ...

  6. WINVER WIN32 WINNT

    WINVER 和 _WIN32_WINNT 请在WINDOWS.H前定义 从 Visual C++ 2008 开始,Visual C++ 不支持面向 Windows 95.Windows 98.Win ...

  7. Vue课程思维导图

  8. CAD动态绘制样条线(网页版)

    在CAD设计时,需要绘制样条线,用户可以设置样条线线重及颜色等属性. 主要用到函数说明: _DMxDrawX::SendStringToExecuteFun 把命令当着函数执行,可以传参数.详细说明如 ...

  9. 洛谷——P1850 换教室

    P1850 换教室 有 2n 节课程安排在 nn 个时间段上.在第 i个时间段上,两节内容相同的课程同时在不同的地点进行,其中,牛牛预先被安排在教室 $c_i$​ 上课,而另一节课程在教室 $d_i$ ...

  10. linux网络编程——域名转换 gethostbyname与gethostbyaddr

    域名转换 #include <netdb.h> struct hostent *gethostbyname(const char *name); 参数: name: 执行主机名的指针 返回 ...