android自定义控件系列教程-----仿新版优酷评论剧集卡片滑动控件

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

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

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

里面的技术细节可能就是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);
	}
}

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

时间: 2024-12-13 11:16:34

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

Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一)

转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45027641 自定义view/viewgroup要重写的几个方法:onMeasure(),onLayout(),onDraw().(不熟悉的话可以查看专栏的前几篇文章:Android自定义控件系列二:自定义开关按钮(一)). 今天的任务就是详细研究一下protected void onMeasure(int widthMeasureSpec, int heightMeasureSpe

android自定义控件系列教程----视图的测量和布局

前面说点什么 当我们的一个视图界面绘制在android屏幕上面的时候其实都必须经过这几步measure. layout.draw这几个阶段,我们可以在view类里面看到这几个函数,然后里面有几个函数是onmeasure.onlayout.ondraw这几个函数是我们重写控件需要注意的这几个函数,下面我们就来讲讲这几个函数的功能和作用. onMeasure 正如这个函数的名子一样就是测量,所有的图示其实系统在绘制之前都不知道它到底有多大的,所以在很多时候我们在初始化界面oncreate的时候直接去

android自定义控件系列教程-----touch事件的传递

前沿: 很久没有写过博客了,因为工作的原因很少有时间写东西了,最近想写一个UI系列的博客,因为我发现这一系列的都很少,而且没有那么系统,这里我想以我自己的观点来阐述一下如何自定义android 控件系列. 自定义控件阐述: 在我的理解里面自定义控件,需要了解到touch事件的传递.分发.拦截机制,Scroller类的运用,andorid 视图的理解,ViewGroup的熟悉,因为我们绝大多的控件都是继承自ViewGroup,还有就是要学会布局测量等. Touch事件的传递 首先我们要了解在and

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

理解android视图 对于android设备我们所看到的区域其实和它在底层的绘制有着很大的关系,很多时候我们都只关心我们所看到的,那么在底层一点它到底是怎么样的一个东西呢?让我们先来看看这个图. 对于整个设备的可见区域而言其实就是我们中间的那个屏幕,从上面的拿个图可以清晰的看到,除了我们的可见区域在它的上下左右都应该有内容,那么在android系统中是怎么控制显示它的位置呢?下面我们来解答这个问题. android如何控制视图的显示位置 我们可以打开view类的源码找到这两个函数 /** *

android自定义控件系列教程----继承ViewGroup实现带阻力效果的可回弹的SrollView

前沿分析: 我为什么要想实现一个这样的回弹呢?因为android都没有支持回弹效果,只有个oversroll的回弹效果,其他的时候都是edgeeffect效果,当我们在哪个地方需要这样的回弹效果我们就直接把我们的控件往这个SrollVIew里面一扔就可以了.其他的都不用管. 主要用到的类讲解: Scroller,主要来辅助我们记录动画和滑动的类,VelocityTracker用来计算滑动阀值就是快速滑动的辅助类,用到的辅助类就这两个,其他的就是测量和布局还有事件的编写了. 效果图 里面的按钮是我

android自定义控件系列教程----真正的圆角button来了

前沿: 现在网上随便输入一句圆角button就会出现很多博客和文章提示做这样的一个效果,但是那多半都是xml文件来做的,这样做有个很大的弊端,因为每一次都需要重写xml文件(就连简简单单的修改个按钮的颜色也需要修改).~~为什么呢?因为不修改臣妾做不到啊!!!今天就带大家做一个真正的圆角button,我们还是来看效果吧. 正文干货开始: 很明显我们的按钮的背景就是我们要实现的圆角部分,那么我们情不自禁的想到了setBackground这个方法,看看里面的参数,需要的是一个Drawable,而我们

Android自定义控件系列八:详解onMeasure()(二)--利用onMeasure测量来实现图片拉伸永不变形,解决屏幕适配问题

上一篇文章详细讲解了一下onMeasure/measure方法在Android自定义控件时的原理和作用,参看博文:Android自定义控件系列七:详解onMeasure()方法中如何测量一个控件尺寸(一),今天就来真正实践一下,让这两个方法大显神威来帮我们搞定图片的屏幕适配问题. 请尊重原创劳动成果,转载请注明出处:http://blog.csdn.net/cyp331203/article/details/45038329,非允许请勿用于商业或盈利用途,违者必究. 使用ImageView会遇到

史上最详细的Android Studio系列教程一--下载和安装

链接地址:http://segmentfault.com/a/1190000002401964#articleHeader4 原文链接:http://stormzhang.com/devtools/2014/11/25/android-studio-tutorial1/ 背景 相信大家对Android Studio已经不陌生了,Android Studio是Google于2013 I/O大会针对Android开发推出的新的开发工具,目前很多开源项目都已经在采用,Google的更新速度也很快,明显

Android Studio系列教程一--下载与安装

Android Studio系列教程一--下载与安装 背景 相信大家对Android Studio已经不陌生了,Android Studio是Google于2013 I/O大会针对Android开发推出的新的开发工具,目前很多开源项目都已经在采用,Google的更新速度也很快,明显能感觉到这是Android开发的未来,那么我们还有什么理由不去拥抱未来呢? 虽然推出了很久,但是国内貌似普及的程度并不高,鉴于很多朋友求studio的详细教程,那么今天我就手把手教大家下载.安装.使用,Studio之路