轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/17357967html
不知不覺中,帶你一步步深刻了解View系列的文章已經寫到第四篇了,回顧一下,咱們一共學習了LayoutInflater的原理分析、視圖的繪製流程、視圖的狀態及重繪等知識,算是把View中不少重要的知識點都涉及到了。若是你尚未看過我前面的幾篇文章,建議先去閱讀一下,多瞭解一些原理方面的東西。java
以前我有承諾過,會在View這個話題上多寫幾篇博客,講一講View的工做原理,以及自定義View的方法。如今前半部分的承諾已經如約兌現了,那麼今天我就要來兌現後面部分的承諾,講一講自定義View的實現方法,同時這也是帶你一步步深刻了解View系列的完結篇。android
一些接觸Android不久的朋友對自定義View都有一絲畏懼感,總感受這是一個比較高級的技術,但其實自定義View並不複雜,有時候只須要簡單幾行代碼就能夠完成了。程序員
若是說要按類型來劃分的話,自定義View的實現方式大概能夠分爲三種,自繪控件、組合控件、以及繼承控件。那麼下面咱們就來依次學習一下,每種方式分別是如何自定義View的。canvas
1、自繪控件
自繪控件的意思就是,這個View上所展示的內容所有都是咱們本身繪製出來的。繪製的代碼是寫在onDraw()方法中的,而這部份內容咱們已經在 Android視圖繪製流程徹底解析,帶你一步步深刻了解View(二) 中學習過了。ide
下面咱們準備來自定義一個計數器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時,仍是須要不少技巧的。
2、組合控件
組合控件的意思就是,咱們並不須要本身去繪製視圖上顯示的內容,而只是用系統原生的控件就行了,但咱們能夠將幾個系統原生的控件組合到一塊兒,這樣建立出的控件就被稱爲組合控件。
舉個例子來講,標題欄就是個很常見的組合控件,不少界面的頭部都會放置一個標題欄,標題欄上會有個返回按鈕和標題,點擊按鈕後就能夠返回到上一個界面。那麼下面咱們就來嘗試去實現這樣一個標題欄控件。
新建一個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了。
3、繼承控件
繼承控件的意思就是,咱們並不須要本身重頭去實現一個控件,只須要去繼承一個現有的控件,而後在這個控件上增長一些新的功能,就能夠造成一個自定義的控件了。這種自定義控件的特色就是不只可以按照咱們的需求加入相應的功能,還能夠保留原生控件的全部功能,好比 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系列的文章就到此結束,感謝你們有耐心看到最後。