Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> android自定義控件系列教程-----仿新版優酷評論劇集卡片滑動控件

android自定義控件系列教程-----仿新版優酷評論劇集卡片滑動控件

編輯:關於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);
	}
}
 

 

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved