PS:自定義View是Android中高手進階的路線.因此我也打算一步一步的學習.看了鴻洋和郭霖這兩位大牛的博客,決定一步一步的學習,循序漸進.
學習內容:
1.自定義View實現ListView的Item左右滑動顯示和隱藏彈窗的效果
自定義View其實是在Android學習路上比較難掌握的一個重要點,但是也是高手的必經之路,自定義View分為很多種,我們可以直接繼承View,或者是繼承他的直接子類或間接子類.ViewGroup,ListView,LinearLayout,Button等等.繼承他們的間接子類還算是比較簡單的..因為View的子類或者是間接子類可以幫助我們做很多的事情.有很多的地方,我們可以不去實現,子類就幫我們做了(onMeasure,onLayout,onDraw)這些方法.直接繼承View,我們是必須要對這些方法進行重寫的,來實現我們自定義View.
因此我這里就沒有去直接繼承View,而是選擇繼承了他的子類.由於ListView的使用還是相當的廣泛的,也是有些費勁的.因此決定從ListView開始..這里先把自定義的ListView代碼先粘出來.然后再一步一步的分析.
package com.view; import android.content.Context; import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.Button; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.PopupWindow; import com.example.administrator.myview.R; /** * Created by totem on 2016/7/10. * @author 代碼丶如風 */ public class MyListView extends ListView { private static final String TAG = "ListView"; private LayoutInflater inflater; /** * 手指按下的x,y坐標,以及移動以后的x,y坐標 */ private int xDown; private int yDown; private int xMove; private int yMove; private boolean isRightSliding; private boolean isLeftSliding; //滑動的最小距離 private int touchSlop; //PopWindow彈窗 private PopupWindow popupWindow; private int popWindowWidth; private int popWindowHeight; private Button delButton; private int mCurrentViewPosition; private View mCurrentView; //回調接口 private DeleteItemListener deleteItemListener; /** * 初始化操作 */ public MyListView(Context context, AttributeSet attrs) { super(context, attrs); inflater = LayoutInflater.from(context); touchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); View view = inflater.inflate(R.layout.delete_item, null); delButton = (Button) view.findViewById(R.id.id_item_btn); popupWindow = new PopupWindow(view, LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); popupWindow.getContentView().measure(0, 0); popWindowWidth = popupWindow.getContentView().getMeasuredWidth(); popWindowHeight = popupWindow.getContentView().getMeasuredHeight(); } @Override public boolean dispatchTouchEvent(MotionEvent event) { int action = event.getAction(); int x = (int) event.getX(); int y = (int) event.getY(); switch (action) { case MotionEvent.ACTION_DOWN: xDown = x; yDown = y; if (popupWindow.isShowing()) { dismissPopWindow(); } mCurrentViewPosition = pointToPosition(xDown, yDown); View view = getChildAt(mCurrentViewPosition - getFirstVisiblePosition()); mCurrentView = view; break; case MotionEvent.ACTION_MOVE: xMove = x; yMove = y; int offsetX = xDown - xMove; int offsetY = yDown - yMove; if (xMove < xDown && Math.abs(offsetX) > touchSlop && Math.abs(offsetY) < touchSlop) { isLeftSliding = true; }else if(xMove >xDown && Math.abs(offsetX) >touchSlop && Math.abs(offsetY) < touchSlop){ isRightSliding = true; } break; } return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); if (isLeftSliding) { switch (action) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: int location[] = new int[2]; mCurrentView.getLocationOnScreen(location); popupWindow.setAnimationStyle(R.style.popwindow_delete_btn_anim_style); //設置彈窗的動畫效果 popupWindow.update(); popupWindow.showAtLocation(mCurrentView, Gravity.LEFT | Gravity.TOP, location[0] + mCurrentView.getWidth(), location[1] + mCurrentView.getHeight() / 2 - popWindowHeight / 2); delButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (deleteItemListener != null) { deleteItemListener.DeleteItem(mCurrentViewPosition); popupWindow.dismiss(); } } }); break; case MotionEvent.ACTION_UP: isLeftSliding = false; break; } //防止與Item點擊事件沖突 return true; }else if(isRightSliding){ switch (action) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: if(popupWindow.isShowing()){ dismissPopWindow(); } break; case MotionEvent.ACTION_UP: isRightSliding = false; break; } return true; } return super.onTouchEvent(event); } public void setDeleteItemListener(DeleteItemListener listener) { deleteItemListener = listener; } public interface DeleteItemListener { void DeleteItem(int position); } private void dismissPopWindow() { if (popupWindow != null && popupWindow.isShowing()) { popupWindow.dismiss(); } } }
這里一部分是鴻洋大牛的.我又簡單的優化了點.他寫的只有向左滑動彈出,而沒有向右滑動隱藏.因此我這里就給加上了.主要還是理解其中的思想才是關鍵.畢竟自定義View的學習沒有什么相關的書籍,只能看這些牛人的博客了.看完也是受益匪淺的.廢話不多說,我們總體縷一下思路,如何去實現這個自定義View是關鍵.
首先:
我們要清楚,用戶的操作,怎樣才算是滑動效果,怎樣算是從左往右划,怎樣算從右往左划,這也是問題的第一個關鍵所在,那么我們知道用戶滑動的時候首先是需要點擊屏幕的,因此這里定義了xDown和yDown來記錄手指點下的坐標.那么滑動完以后,必然有一個結束滑動的坐標,xMove,yMove.相比到這里我們就明確怎樣是向左滑動和向右滑動了.
xMove - xDown > 0 ? 向左滑動:向右滑動。。同時這里還要滿足xMove - xDown要大於最小的滑動距離,這里最小的滑動距離就是用touchslop來記錄的即:touchslop = ViewConfiguration.get(context).getScaledTouchSlop().這樣我們就先解決了第一個問題,如何判斷滑動.
其次:
滑動之后需要彈出一個窗口,那么這里就使用PopWindow來實現了.因此這里就創建了一個PopWindow.這個PopWindow可以顯示出滑動后的刪除按鈕,因此在new的時候,需要把相關的view附加上.
/** * <p>Create a new non focusable popup window which can display the * <tt>contentView</tt>. The dimension of the window must be passed to * this constructor.</p> * * <p>The popup does not provide any background. This should be handled * by the content view.</p> * * @param contentView the popup's content * @param width the popup's width * @param height the popup's height */ public PopupWindow(View contentView, int width, int height) { this(contentView, width, height, false); }
源碼是這樣寫的,創建一個無焦點的PopWindow用於顯示我們傳入的contentView.在這里這個PopWindow用於顯示刪除按鈕.但是我們需要明確一個地方.這個PopWindow是我們new出來的,並沒有在xml文件中進行書寫,我們在獲取它的寬高時,需要調用measure()方法,先對這個PopWindow進行測量,測量之后我們才能夠拿到相應的寬度和高度,因為這個PopWindow並沒有在我們的ListView中,也沒有在Item中,而是我們手動加上的,因此ListView在onMeasure()的時候是不會對這個PopWindow進行測量的.這個取決於View的加載機制,這里我先不進行多說,等到后期我會補上View的加載機制,如果讀者想現在弄明白怎么回事,推薦先去看看郭林大牛的博客,關於View的四篇文章,讀完那四篇文章,就能夠理解這塊到底是怎么回事了.反正在這里讀者只需要先記住就可以,不調用measure()方法是拿不到寬高的.讀者可以自己去試一下.好了,這樣我們就解決了第二個問題.
最后:回調接口
首先,我們在MyView中引入了一個按鈕,也就是PopWindow顯示的按鈕,但是這個按鈕需要做事情,它需要在被點擊的時候移除掉當前的Item,我們知道只有主線程才有權利對View進行操作,我們定義的這個View是沒有權利的,這樣就需要一個回調接口,在Button被點擊的時候出發回調事件,觸發的時候需要傳遞position參數,也就是當前Item的position.這個position傳入后,被稱為登記回調函數.觸發的同時登記回調函數告知主線程這個事件被觸發了,需要主函數進行處理,那么主線程在對Item進行操作,這樣就符合規則了,因此回調函數這個概念想必大家就清楚了.
前一陣子在知乎上看到有解釋這個的概念,覺得說的非常的合理,在這里放上,這樣就更方便大家的理解了.
回調函數是什么?
鏈接:http://www.zhihu.com/question/19801131/answer/13005983 來源:知乎
你到一個商店買東西,剛好你要的東西沒有貨,於是你在店員那里留下了你的電話,過了幾天店里有貨了,店員就打了你的電話,然后你接到電話后就到店里去取了貨。在這個例子里,你的電話號碼就叫回調函數,你把電話留給店員就叫登記回調函數,店里后來有貨了叫做觸發了回調關聯的事件,店員給你打電話叫做調用回調函數,你到店里去取貨叫做響應回調事件.
然后事件分發機制了.dispatchTouchEvent和onTouchEvent,其實中間還有一個interpectTouchEvent,用於判斷是否需要對事件進行攔截,這個就不說了,對ACTION_DOWN,ACTION_UP,ACTION_DOWN事件進行處理.dispatchTouchEvent用於分發事件,只在這里進行了簡單的操作,對當前被點擊的Item進行記錄,以及對isRightSliding,isLeftSliding屬性進行賦值,分發之后就交給了onTouchEvent去處理.它來完成對ACTION的處理.這樣我們的總體思路就非常清晰了.
首先:對滑動事件的判斷,其次:加入我們滑動后需要顯示的效果,接着添加相應的回調函數用來處理View的觸發事件,最后對事件分發處理,就解決了自定義View.最后附上MainActivity的源代碼.
package com.example.administrator.myview; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import com.view.MyListView; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class MainActivity extends Activity { private MyListView myListView; private ArrayAdapter<String> adapter; private List<String> mData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView(){ myListView = (MyListView) findViewById(R.id.MyListView); mData = new ArrayList<>(Arrays.asList("1","2","3","4","5","6","7","8","9","10")); adapter = new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,mData); myListView.setAdapter(adapter); myListView.setDeleteItemListener(new MyListView.DeleteItemListener() { @Override public void DeleteItem(int position) { adapter.remove(adapter.getItem(position)); } }); myListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { //Item的點擊事件 } }); } }
我們看到MainActivity實現回調接口,然后對View進行處理,與我們所說一致.這樣就OK了,這里只簡單的寫了個Adapter,如果想寫更復雜的就自己去實現Adapter了.
博客園的文件上傳限制在10M,沒有辦法只能分享個百度雲的鏈接了.
源代碼:http://pan.baidu.com/s/1o8KVlXK