Android自定義View的實現方法,帶你一步步深入瞭解View(四)
不知不覺中,帶你一步步深入瞭解View系列的文章已經寫到第四篇了,回顧一下,我們一共學習了LayoutInflater的原理分析、檢視的繪製流程、檢視的狀態及重繪等知識,算是把View中很多重要的知識點都涉及到了。如果你還沒有看過我前面的幾篇文章,建議先去閱讀一下,多瞭解一些原理方面的東西。
之前我有承諾過,會在View這個話題上多寫幾篇部落格,講一講View的工作原理,以及自定義View的方法。現在前半部分的承諾已經如約兌現了,那麼今天我就要來兌現後面部分的承諾,講一講自定義View的實現方法,同時這也是帶你一步步深入瞭解View系列的完結篇。
一些接觸Android不久的朋友對自定義View都有一絲畏懼感,總感覺這是一個比較高階的技術,但其實自定義View並不複雜,有時候只需要簡單幾行程式碼就可以完成了。
如果說要按型別來劃分的話,自定義View的實現方式大概可以分為三種,自繪控制元件、組合控制元件、以及繼承控制元件。那麼下面我們就來依次學習一下,每種方式分別是如何自定義View的。
一、自繪控制元件
自繪控制元件的意思就是,這個View上所展現的內容全部都是我們自己繪製出來的。繪製的程式碼是寫在onDraw()方法中的,而這部分內容我們已經在 Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二) 中學習過了。
下面我們準備來自定義一個計數器View,這個View可以響應使用者的點選事件,並自動記錄一共點選了多少次。新建一個CounterView繼承自View,程式碼如下所示:
public class CounterView extends View implements OnClickListener { private Paint mPaint; private Rect mBounds; private int mCount; public CounterView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBounds = new Rect(); setOnClickListener(this); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setColor(Color.BLUE); canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); mPaint.setColor(Color.YELLOW); mPaint.setTextSize(30); String text = String.valueOf(mCount); mPaint.getTextBounds(text, 0, text.length(), mBounds); float textWidth = mBounds.width(); float textHeight = mBounds.height(); canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2 + textHeight / 2, mPaint); } @Override public void onClick(View v) { mCount++; invalidate(); } } 可以看到,首先我們在CounterView的建構函式中初始化了一些資料,並給這個View的本身註冊了點選事件,這樣當CounterView被點選的時候,onClick()方法就會得到呼叫。而onClick()方法中的邏輯就更加簡單了,只是對mCount這個計數器加1,然後呼叫invalidate()方法。通過 Android檢視狀態及重繪流程分析,帶你一步步深入瞭解View(三) 這篇文章的學習我們都已經知道,呼叫invalidate()方法會導致檢視進行重繪,因此onDraw()方法在稍後就將會得到呼叫。
既然CounterView是一個自繪檢視,那麼最主要的邏輯當然就是寫在onDraw()方法裡的了,下面我們就來仔細看一下。這裡首先是將Paint畫筆設定為藍色,然後呼叫Canvas的drawRect()方法繪製了一個矩形,這個矩形也就可以當作是CounterView的背景圖吧。接著將畫筆設定為黃色,準備在背景上面繪製當前的計數,注意這裡先是呼叫了getTextBounds()方法來獲取到文字的寬度和高度,然後呼叫了drawText()方法去進行繪製就可以了。
這樣,一個自定義的View就已經完成了,並且目前這個CounterView是具備自動計數功能的。那麼剩下的問題就是如何讓這個View在介面上顯示出來了,其實這也非常簡單,我們只需要像使用普通的控制元件一樣來使用CounterView就可以了。比如在佈局檔案中加入如下程式碼:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.customview.CounterView android:layout_width="100dp" android:layout_height="100dp" android:layout_centerInParent="true" /> </RelativeLayout> 可以看到,這裡我們將CounterView放入了一個RelativeLayout中,然後可以像使用普通控制元件來給CounterView指定各種屬性,比如通過layout_width和layout_height來指定CounterView的寬高,通過android:layout_centerInParent來指定它在佈局里居中顯示。只不過需要注意,自定義的View在使用的時候一定要寫出完整的包名,不然系統將無法找到這個View。
好了,就是這麼簡單,接下來我們可以執行一下程式,並不停地點選CounterView,效果如下圖所示。
怎麼樣?是不是感覺自定義View也並不是什麼高階的技術,簡單幾行程式碼就可以實現了。當然了,這個CounterView功能非常簡陋,只有一個計數功能,因此只需幾行程式碼就足夠了,當你需要繪製比較複雜的View時,還是需要很多技巧的。
二、組合控制元件
組合控制元件的意思就是,我們並不需要自己去繪製檢視上顯示的內容,而只是用系統原生的控制元件就好了,但我們可以將幾個系統原生的控制元件組合到一起,這樣創建出的控制元件就被稱為組合控制元件。
舉個例子來說,標題欄就是個很常見的組合控制元件,很多介面的頭部都會放置一個標題欄,標題欄上會有個返回按鈕和標題,點選按鈕後就可以返回到上一個介面。那麼下面我們就來嘗試去實現這樣一個標題欄控制元件。
新建一個title.xml佈局檔案,程式碼如下所示:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="50dp" android:background="#ffcb05" > <Button android:id="@+id/button_left" android:layout_width="60dp" android:layout_height="40dp" android:layout_centerVertical="true" android:layout_marginLeft="5dp" android:background="@drawable/back_button" android:text="Back" android:textColor="#fff" /> <TextView android:id="@+id/title_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="This is Title" android:textColor="#fff" android:textSize="20sp" /> </RelativeLayout> 在這個佈局檔案中,我們首先定義了一個RelativeLayout作為背景佈局,然後在這個佈局裡定義了一個Button和一個TextView,Button就是標題欄中的返回按鈕,TextView就是標題欄中的顯示的文字。
接下來建立一個TitleView繼承自FrameLayout,程式碼如下所示:
public class TitleView extends FrameLayout { private Button leftButton; private TextView titleText; public TitleView(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater.from(context).inflate(R.layout.title, this); titleText = (TextView) findViewById(R.id.title_text); leftButton = (Button) findViewById(R.id.button_left); leftButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ((Activity) getContext()).finish(); } }); } public void setTitleText(String text) { titleText.setText(text); } public void setLeftButtonText(String text) { leftButton.setText(text); } public void setLeftButtonListener(OnClickListener l) { leftButton.setOnClickListener(l); } } TitleView中的程式碼非常簡單,在TitleView的構建方法中,我們呼叫了LayoutInflater的inflate()方法來載入剛剛定義的title.xml佈局,這部分內容我們已經在 Android LayoutInflater原理分析,帶你一步步深入瞭解View(一) 這篇文章中學習過了。
接下來呼叫findViewById()方法獲取到了返回按鈕的例項,然後在它的onClick事件中呼叫finish()方法來關閉當前的Activity,也就相當於實現返回功能了。
另外,為了讓TitleView有更強地擴充套件性,我們還提供了setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法,分別用於設定標題欄上的文字、返回按鈕上的文字、以及返回按鈕的點選事件。
到了這裡,一個自定義的標題欄就完成了,那麼下面又到了如何引用這個自定義View的部分,其實方法基本都是相同的,在佈局檔案中新增如下程式碼:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.customview.TitleView android:id="@+id/title_view" android:layout_width="match_parent" android:layout_height="wrap_content" > </com.example.customview.TitleView> </RelativeLayout> 這樣就成功將一個標題欄控制元件引入到佈局檔案中了,執行一下程式,效果如下圖所示:
現在點選一下Back按鈕,就可以關閉當前的Activity了。如果你想要修改標題欄上顯示的內容,或者返回按鈕的預設事件,只需要在Activity中通過findViewById()方法得到TitleView的例項,然後呼叫setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法進行設定就OK了。
三、繼承控制元件
繼承控制元件的意思就是,我們並不需要自己重頭去實現一個控制元件,只需要去繼承一個現有的控制元件,然後在這個控制元件上增加一些新的功能,就可以形成一個自定義的控制元件了。這種自定義控制元件的特點就是不僅能夠按照我們的需求加入相應的功能,還可以保留原生控制元件的所有功能,比如 Android PowerImageView實現,可以播放動畫的強大ImageView 這篇文章中介紹的PowerImageView就是一個典型的繼承控制元件。
為了能夠加深大家對這種自定義View方式的理解,下面我們再來編寫一個新的繼承控制元件。ListView相信每一個Android程式設計師都一定使用過,這次我們準備對ListView進行擴充套件,加入在ListView上滑動就可以顯示出一個刪除按鈕,點選按鈕就會刪除相應資料的功能。
首先需要準備一個刪除按鈕的佈局,新建delete_button.xml檔案,程式碼如下所示:
<?xml version="1.0" encoding="utf-8"?> <Button xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/delete_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/delete_button" > </Button> 這個佈局檔案很簡單,只有一個按鈕而已,並且我們給這個按鈕指定了一張刪除背景圖。
接著建立MyListView繼承自ListView,這就是我們自定義的View了,程式碼如下所示:
public class MyListView extends ListView implements OnTouchListener, OnGestureListener { private GestureDetector gestureDetector; private OnDeleteListener listener; private View deleteButton; private ViewGroup itemLayout; private int selectedItem; private boolean isDeleteShown; public MyListView(Context context, AttributeSet attrs) { super(context, attrs); gestureDetector = new GestureDetector(getContext(), this); setOnTouchListener(this); } public void setOnDeleteListener(OnDeleteListener l) { listener = l; } @Override public boolean onTouch(View v, MotionEvent event) { if (isDeleteShown) { itemLayout.removeView(deleteButton); deleteButton = null; isDeleteShown = false; return false; } else { return gestureDetector.onTouchEvent(event); } } @Override public boolean onDown(MotionEvent e) { if (!isDeleteShown) { selectedItem = pointToPosition((int) e.getX(), (int) e.getY()); } return false; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (!isDeleteShown && Math.abs(velocityX) > Math.abs(velocityY)) { deleteButton = LayoutInflater.from(getContext()).inflate( R.layout.delete_button, null); deleteButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { itemLayout.removeView(deleteButton); deleteButton = null; isDeleteShown = false; listener.onDelete(selectedItem); } }); itemLayout = (ViewGroup) getChildAt(selectedItem - getFirstVisiblePosition()); RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); params.addRule(RelativeLayout.CENTER_VERTICAL); itemLayout.addView(deleteButton, params); isDeleteShown = true; } return false; } @Override public boolean onSingleTapUp(MotionEvent e) { return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; } @Override public void onLongPress(MotionEvent e) { } public interface OnDeleteListener { void onDelete(int index); } } 由於程式碼邏輯比較簡單,我就沒有加註釋。這裡在MyListView的構造方法中建立了一個GestureDetector的例項用於監聽手勢,然後給MyListView註冊了touch監聽事件。然後在onTouch()方法中進行判斷,如果刪除按鈕已經顯示了,就將它移除掉,如果刪除按鈕沒有顯示,就使用GestureDetector來處理當前手勢。
當手指按下時,會呼叫OnGestureListener的onDown()方法,在這裡通過pointToPosition()方法來判斷出當前選中的是ListView的哪一行。當手指快速滑動時,會呼叫onFling()方法,在這裡會去載入delete_button.xml這個佈局,然後將刪除按鈕新增到當前選中的那一行item上。注意,我們還給刪除按鈕添加了一個點選事件,當點選了刪除按鈕時就會回撥onDeleteListener的onDelete()方法,在回撥方法中應該去處理具體的刪除操作。
好了,自定義View的功能到此就完成了,接下來我們需要看一下如何才能使用這個自定義View。首先需要建立一個ListView子項的佈局檔案,新建my_list_view_item.xml,程式碼如下所示:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:descendantFocusability="blocksDescendants" android:orientation="vertical" > <TextView android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="50dp" android:layout_centerVertical="true" android:gravity="left|center_vertical" android:textColor="#000" /> </RelativeLayout> 然後建立一個介面卡MyAdapter,在這個介面卡中去載入my_list_view_item佈局,程式碼如下所示: public class MyAdapter extends ArrayAdapter<String> { public MyAdapter(Context context, int textViewResourceId, List<String> objects) { super(context, textViewResourceId, objects); } @Override public View getView(int position, View convertView, ViewGroup parent) { View view; if (convertView == null) { view = LayoutInflater.from(getContext()).inflate(R.layout.my_list_view_item, null); } else { view = convertView; } TextView textView = (TextView) view.findViewById(R.id.text_view); textView.setText(getItem(position)); return view; } } 到這裡就基本已經完工了,下面在程式的主佈局檔案裡面引入MyListView這個控制元件,如下所示: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.customview.MyListView android:id="@+id/my_list_view" android:layout_width="match_parent" android:layout_height="wrap_content" > </com.example.customview.MyListView> </RelativeLayout> 最後在Activity中初始化MyListView中的資料,並處理了onDelete()方法的刪除邏輯,程式碼如下所示: public class MainActivity extends Activity { private MyListView myListView; private MyAdapter adapter; private List<String> contentList = new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); initList(); myListView = (MyListView) findViewById(R.id.my_list_view); myListView.setOnDeleteListener(new OnDeleteListener() { @Override public void onDelete(int index) { contentList.remove(index); adapter.notifyDataSetChanged(); } }); adapter = new MyAdapter(this, 0, contentList); myListView.setAdapter(adapter); } private void initList() { contentList.add("Content Item 1"); contentList.add("Content Item 2"); contentList.add("Content Item 3"); contentList.add("Content Item 4"); contentList.add("Content Item 5"); contentList.add("Content Item 6"); contentList.add("Content Item 7"); contentList.add("Content Item 8"); contentList.add("Content Item 9"); contentList.add("Content Item 10"); contentList.add("Content Item 11"); contentList.add("Content Item 12"); contentList.add("Content Item 13"); contentList.add("Content Item 14"); contentList.add("Content Item 15"); contentList.add("Content Item 16"); contentList.add("Content Item 17"); contentList.add("Content Item 18"); contentList.add("Content Item 19"); contentList.add("Content Item 20"); } } 這樣就把整個例子的程式碼都完成了,現在執行一下程式,會看到MyListView可以像ListView一樣,正常顯示所有的資料,但是當你用手指在MyListView的某一行上快速滑動時,就會有一個刪除按鈕顯示出來,如下圖所示:
點選一下刪除按鈕就可以將第6行的資料刪除了。此時的MyListView不僅保留了ListView原生的所有功能,還增加了一個滑動進行刪除的功能,確實是一個不折不扣的繼承控制元件。
到了這裡,我們就把自定義View的幾種實現方法全部講完了,雖然每個例子都很簡單,但是萬變不離其宗,複雜的View也是由這些簡單的原理堆積出來的。經過了四篇文章的學習,相信每個人對View的理解都已經較為深入了,那麼帶你一步步深入瞭解View系列的文章就到此結束,感謝大家有耐心看到最後。