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


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


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

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

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

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


  • 相关阅读:
    论文阅读CRSLab: An Open-Source Toolkit for Building Conversational Recommender System
    论推荐系统的Exploitation和Exploration
    《被讨厌的勇气》
    ERROR 1044 (42000): Access denied for user ''@'localhost' to database 'mysql'
    PyCharm提示ModuleNotFoundError: No module named 'pymysql'
    PyCharm安装
    协同过滤-显式评级和隐式评级
    论文阅读 Navigation-by-Preference: A New Conversational Recommender with Preference-Based Feedback
    java线程的状态
    定时任务
  • 原文地址:https://www.cnblogs.com/jhcelue/p/6898353.html
Copyright © 2020-2023  润新知