不知不覺中,帶你一步步深入了解View系列的文章已經寫到第四篇了,回顧一下,我們一共學習了LayoutInflater的原理分析、視圖的繪制流程、視圖的狀態及重繪等知識,算是把View中很多重要的知識點都涉及到了。如果你還沒有看過我前面的幾篇文章,建議先去閱讀一下,多了解一些原理方面的東西。
之前我有承諾過,會在View這個話題上多寫幾篇博客,講一講View的工作原理,以及自定義View的方法。現在前半部分的承諾已經如約兌現了,那么今天我就要來兌現后面部分的承諾,講一講自定義View的實現方法,同時這也是帶你一步步深入了解View系列的完結篇。
一些接觸Android不久的朋友對自定義View都有一絲畏懼感,總感覺這是一個比較高級的技術,但其實自定義View并不復雜,有時候只需要簡單幾行代碼就可以完成了。
如果說要按類型來劃分的話,自定義View的實現方式大概可以分為三種,自繪控件、組合控件、以及繼承控件。那么下面我們就來依次學習一下,每種方式分別是如何自定義View的。
一、自繪控件
自繪控件的意思就是,這個View上所展現的內容全部都是我們自己繪制出來的。繪制的代碼是寫在onDraw()方法中的,而這部分內容我們已經在 Android視圖繪制流程完全解析,帶你一步步深入了解View(二)?中學習過了。
下面我們準備來自定義一個計數器View,這個View可以響應用戶的點擊事件,并自動記錄一共點擊了多少次。新建一個CounterView繼承自View,代碼如下所示:
[java] view plaincopy
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就可以了。比如在布局文件中加入如下代碼:
[html] view plaincopy
<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布局文件,代碼如下所示:
[html] view plaincopy
<?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,代碼如下所示:
[java] view plaincopy
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的部分,其實方法基本都是相同的,在布局文件中添加如下代碼:
[html] view plaincopy
<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文件,代碼如下所示:
[html] view plaincopy
<?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了,代碼如下所示:
[java] view plaincopy
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,代碼如下所示:
[html] view plaincopy
<?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布局,代碼如下所示:
[java] view plaincopy
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這個控件,如下所示:
[html] view plaincopy
<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()方法的刪除邏輯,代碼如下所示:
[java] view plaincopy
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系列的文章就到此結束,感謝大家有耐心看到最后。
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀
總結
以上是生活随笔為你收集整理的Android自定义View的实现方法,带你一步步深入了解View(四)的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。