對於 Android 開發者來講,原生控件每每沒法知足要求,須要開發者自定義一些控件,所以,須要去了解自定義 view 的實現原理。這樣即便碰到須要自定義控件的時候,也能夠遊刃有餘。html
自定義 View 的實現方式有如下幾種:java
類型 | 定義 |
自定義組合控件 | 多個控件組合成爲一個新的控件,方便多處複用 |
繼承系統 View 控件 | 繼承自TextView等系統控件,在系統控件的基礎功能上進行擴展 |
繼承 View | 不復用系統控件邏輯,繼承View進行功能定義 |
繼承系統 ViewGroup | 繼承自LinearLayout等系統控件,在系統控件的基礎功能上進行擴展 |
繼承 View ViewGroup | 不復用系統控件邏輯,繼承ViewGroup進行功能定義 |
從上到下愈來愈難,須要的瞭解的知識也是愈來愈多的。android
當咱們在自定義 View 的時候,構造函數都是不可缺乏,須要對構造函數進行重寫,構造函數有多個,至少要重寫其中一個才行。例如咱們新建 MyTextView:canvas
public class MyTextView extends View {
/** * 在java代碼裏new的時候會用到 * @param context */ public MyTextView(Context context) { super(context); } /** * 在xml佈局文件中使用時自動調用 * @param context */ public MyTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } /** * 不會自動調用,若是有默認style時,在第二個構造函數中調用 * @param context * @param attrs * @param defStyleAttr */ public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 只有在API版本>21時纔會用到 * 不會自動調用,若是有默認style時,在第二個構造函數中調用 * @param context * @param attrs * @param defStyleAttr * @param defStyleRes */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); }
}
對於每一種構造函數的做用,都已經再代碼裏面寫出來了。api
寫過佈局的同窗都知道,系統控件的屬性在 xml 中都是以 android 開頭的。對於自定義 View,也能夠自定義屬性,在 xml 中使用。app
Android 自定義屬性可分爲如下幾步:ide
自定義一個 View函數
編寫 values/attrs.xml,在其中編寫 styleable 和 item 等標籤元素佈局
在佈局文件中 View 使用自定義的屬性(注意 namespace)post
在 View 的構造方法中經過 TypedArray 獲取
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.example.myapplication.MyTextView android:layout_width="100dp" android:layout_height="200dp" app:testAttr="520" app:text="helloWorld" /> </android.support.constraint.ConstraintLayout>
而後我在 values/attrs.xml 中添加自定義屬性:
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="test"> <attr name="text" format="string" /> <attr name="testAttr" format="integer" /> </declare-styleable> </resources>
記得在構造函數裏面說過,xml 佈局會調用第二個構造函數,所以在這個構造函數裏面獲取屬性和解析:
/** * 在xml佈局文件中使用時自動調用 * @param context */ public MyTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test); int textAttr = ta.getInteger(R.styleable.test_testAttr, -1); String text = ta.getString(R.styleable.test_text); Log.d(TAG, " text = " + text + ", textAttr = " + textAttr);
// toast 顯示獲取的屬性值 Toast.makeText(context, text + " " + textAttr, Toast.LENGTH_LONG).show(); ta.recycle(); }
注意當你在引用自定義屬性的時候,記得加上 name 前綴,不然會引用不到。
這裏本想截圖 log 的,奈何就是不顯示,就搞成 toast 了。
固然,你還能夠自定義不少其餘屬性,包括 color, string, integer, boolean, flag,甚至是混合等。
自定義組合控件就是將多個控件組合成爲一個新的控件,主要解決屢次重複使用同一類型的佈局。如咱們頂部的 HeaderView 以及 dailog 等,咱們均可以把他們組合成一個新的控件。
咱們經過一個自定義 MyView1 實例來了解自定義組合控件的用法。
xml 佈局
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:id="@+id/feed_item_com_cont_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" android:includeFontPadding="false" android:maxLines="2" android:text="title" /> <TextView android:id="@+id/feed_item_com_cont_desc" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/feed_item_com_cont_title" android:ellipsize="end" android:includeFontPadding="false" android:maxLines="2" android:text="desc" /> </merge>
自定義 View 代碼 :
package com.example.myapplication; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.RelativeLayout; import android.widget.TextView; public class MyView1 extends RelativeLayout { /** 標題 */ private TextView mTitle; /** 描述 */ private TextView mDesc; public MyView1(Context context) { this(context, null); } public MyView1(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } /** * 初使化界面視圖 * * @param context 上下文環境 */ protected void initView(Context context) { View rootView = LayoutInflater.from(getContext()).inflate(R.layout.my_view1, this); mDesc = rootView.findViewById(R.id.feed_item_com_cont_desc); mTitle = rootView.findViewById(R.id.feed_item_com_cont_title); } }
在佈局當中引用該控件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <TextView android:id="@+id/text" android:layout_width="100dp" android:layout_height="100dp" android:clickable="true" android:enabled="false" android:focusable="true" android:text="trsfnjsfksjfnjsdfjksdhfjksdjkfhdsfsdddddddddddddddddddddddddd" /> <com.example.myapplication.MyTextView android:id="@+id/myview" android:layout_width="100dp" android:layout_height="200dp" android:clickable="true" android:enabled="false" android:focusable="true" app:testAttr="520" app:text="helloWorld" /> <com.example.myapplication.MyView1 android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
最終效果以下圖所示 :
繼承系統的控件能夠分爲繼承 View子類(如 TextView 等)和繼承 ViewGroup 子類(如 LinearLayout 等),根據業務需求的不一樣,實現的方式也會有比較大的差別。這裏介紹一個比較簡單的,繼承自View的實現方式。
業務需求:爲文字設置背景,並在佈局中間添加一條橫線。
由於這種實現方式會複用系統的邏輯,大多數狀況下咱們但願複用系統的 onMeaseur
和 onLayout
流程,因此咱們只須要重寫 onDraw
方法 。實現很是簡單,話很少說,直接上代碼。
package com.example.myapplication; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Shader; import android.text.TextPaint; import android.util.AttributeSet; import android.widget.TextView; import static android.support.v4.content.ContextCompat.getColor; /** * 包含分割線的textView * 文字左右兩邊有一條漸變的分割線 * 樣式以下: * ———————— 文字 ———————— */ public class DividingLineTextView extends TextView { /** 線性漸變 */ private LinearGradient mLinearGradient; /** textPaint */ private TextPaint mPaint; /** 文字 */ private String mText = ""; /** 屏幕寬度 */ private int mScreenWidth; /** 開始顏色 */ private int mStartColor; /** 結束顏色 */ private int mEndColor; /** 字體大小 */ private int mTextSize; /** * 構造函數 */ public DividingLineTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mTextSize = getResources().getDimensionPixelSize(R.dimen.text_size); mScreenWidth = getCalculateWidth(getContext()); mStartColor = getColor(getContext(), R.color.colorAccent); mEndColor = getColor(getContext(), R.color.colorPrimary); mLinearGradient = new LinearGradient(0, 0, mScreenWidth, 0, new int[]{mStartColor, mEndColor, mStartColor}, new float[]{0, 0.5f, 1f}, Shader.TileMode.CLAMP); mPaint = new TextPaint(); } public DividingLineTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DividingLineTextView(Context context) { this(context, null); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setAntiAlias(true); mPaint.setTextSize(mTextSize); int len = getTextLength(mText, mPaint); // 文字繪製起始座標 int sx = mScreenWidth / 2 - len / 2; // 文字繪製結束座標 int ex = mScreenWidth / 2 + len / 2; int height = getMeasuredHeight(); mPaint.setShader(mLinearGradient); // 繪製左邊分界線,從左邊開始:左邊距15dp, 右邊距距離文字15dp canvas.drawLine(mTextSize, height / 2, sx - mTextSize, height / 2, mPaint); mPaint.setShader(mLinearGradient); // 繪製右邊分界線,從文字右邊開始:左邊距距離文字15dp,右邊距15dp canvas.drawLine(ex + mTextSize, height / 2, mScreenWidth - mTextSize, height / 2, mPaint); } /** * 返回指定文字的寬度,單位px * * @param str 要測量的文字 * @param paint 繪製此文字的畫筆 * @return 返回文字的寬度,單位px */ private int getTextLength(String str, TextPaint paint) { return (int) paint.measureText(str); } /** * 更新文字 * * @param text 文字 */ public void update(String text) { mText = text; setText(mText); // 刷新重繪 requestLayout(); } /** * 獲取須要計算的寬度,取屏幕高寬較小值, * * @param context context * @return 屏幕寬度值 */ public static int getCalculateWidth(Context context) { int height = context.getResources().getDisplayMetrics().heightPixels; // 動態屏幕寬度,在摺疊屏手機上寬度在分屏時會發生變化 int Width = context.getResources().getDisplayMetrics().widthPixels; return Math.min(Width, height); } }
對於 View 的繪製還須要對 Paint()
、canvas
以及 Path
的使用有所瞭解,不清楚的能夠稍微瞭解一下。
看下佈局裏面的引用:
xml 佈局
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> // ...... 跟前面同樣忽視 <com.example.myapplication.DividingLineTextView android:id="@+id/divide" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" /> </LinearLayout>
activty 裏面代碼以下 :
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DividingLineTextView te = findViewById(R.id.divide); te.update("DividingLineTextView"); }
這裏經過 update() 對來從新繪製,確保邊線在文字的兩邊。視覺效果以下:
直接繼承 View 會比上一種實現方複雜一些,這種方法的使用情景下,徹底不須要複用系統控件的邏輯,除了要重寫 onDraw
外還須要對 onMeasure
方法進行重寫。
咱們用自定義 View 來繪製一個正方形。
首先定義構造方法,以及作一些初始化操做
ublic class RectView extends View{ //定義畫筆 private Paint mPaint = new Paint(); /** * 實現構造方法 * @param context */ public RectView(Context context) { super(context); init(); } public RectView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint.setColor(Color.BLUE); } }
重寫 draw 方法,繪製正方形,注意對 padding 屬性進行設置:
/** * 重寫draw方法 * @param canvas */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //獲取各個編劇的padding值 int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); //獲取繪製的View的寬度 int width = getWidth()-paddingLeft-paddingRight; //獲取繪製的View的高度 int height = getHeight()-paddingTop-paddingBottom; //繪製View,左上角座標(0+paddingLeft,0+paddingTop),右下角座標(width+paddingLeft,height+paddingTop) canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint); }
在 View 的源碼當中並無對 AT_MOST
和 EXACTLY
兩個模式作出區分,也就是說 View 在 wrap_content
和 match_parent
兩個模式下是徹底相同的,都會是 match_parent
,顯然這與咱們平時用的 View 不一樣,因此咱們要重寫 onMeasure
方法。
/** * 重寫onMeasure方法 * * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); //處理wrap_contentde狀況 if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(300, 300); } else if (widthMode == MeasureSpec.AT_MOST) { setMeasuredDimension(300, heightSize); } else if (heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSize, 300); } }
最終效果如圖所示:
能夠發現,咱們設置的是 wrap_content,可是最後仍是有尺寸的。
整個過程大體以下,直接繼承 View 時須要有幾點注意:
在 onDraw 當中對 padding 屬性進行處理。
在 onMeasure 過程當中對 wrap_content 屬性進行處理。
至少要有一個構造方法。
自定義 ViewGroup 的過程相對複雜一些,由於除了要對自身的大小和位置進行測量以外,還須要對子 View 的測量參數負責。
實現一個相似於 Viewpager 的可左右滑動的佈局。
佈局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".MainActivity"> <com.example.myapplication.MyHorizonView android:layout_width="wrap_content" android:background="@color/colorAccent" android:layout_height="400dp"> <ListView android:id="@+id/list1" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAccent" /> <ListView android:id="@+id/list2" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimary" /> <ListView android:id="@+id/list3" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimaryDark" /> </com.example.myapplication.MyHorizonView> <TextView android:id="@+id/text" android:layout_width="100dp" android:layout_height="100dp" android:clickable="true" android:focusable="true" android:text="trsfnjsfksjfnjsdfjksdhfjksdjkfhdsfsdddddddddddddddddddddddddd" /> <com.example.myapplication.MyTextView android:id="@+id/myview" android:layout_width="1dp" android:layout_height="2dp" android:clickable="true" android:enabled="false" android:focusable="true" app:testAttr="520" app:text="helloWorld" /> <com.example.myapplication.RectView android:layout_width="wrap_content" android:layout_height="wrap_content" /> <com.example.myapplication.MyView1 android:layout_width="wrap_content" android:layout_height="wrap_content" /> <com.example.myapplication.DividingLineTextView android:id="@+id/divide" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" /> </LinearLayout>
一個 ViewGroup 裏面放入 3 個 ListView,注意 ViewGroup 設置的寬是 wrap_conten,在測量的時候,會對 wrap_content 設置成與父 View 的大小一致,具體實現邏輯可看後面的代碼。
代碼比較多,咱們結合註釋分析。
public class MyHorizonView extends ViewGroup { private static final String TAG = "HorizontaiView"; private List<View> mMatchedChildrenList = new ArrayList<>(); public MyHorizonView(Context context) { super(context); } public MyHorizonView(Context context, AttributeSet attributes) { super(context, attributes); } public MyHorizonView(Context context, AttributeSet attributes, int defStyleAttr) { super(context, attributes, defStyleAttr); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int left = 0; View child; for (int i = 0; i < childCount; i++) { child = getChildAt(i); if (child.getVisibility() != View.GONE) { int childWidth = child.getMeasuredWidth(); // 由於是水平滑動的,因此以寬度來適配 child.layout(left, 0, left + childWidth, child.getMeasuredHeight()); left += childWidth; } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mMatchedChildrenList.clear(); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); // 若是不是肯定的的值,說明是 AT_MOST,與父 View 同寬高 final boolean measureMatchParentChildren = heightSpecMode != MeasureSpec.EXACTLY || widthSpecMode != MeasureSpec.EXACTLY; int childCount = getChildCount(); View child; for (int i = 0; i < childCount; i++) { child = getChildAt(i); if (child.getVisibility() != View.GONE) { final LayoutParams layoutParams = child.getLayoutParams(); measureChild(child, widthMeasureSpec, heightMeasureSpec); if (measureMatchParentChildren) { // 須要先計算出父 View 的高度來再來測量子 view if (layoutParams.width == LayoutParams.MATCH_PARENT || layoutParams.height == LayoutParams.MATCH_PARENT) { mMatchedChildrenList.add(child); } } } } if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { // 若是寬高都是AT_MOST的話,即都是wrap_content佈局模式,就用View本身想要的寬高值 setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()); } else if (widthSpecMode == MeasureSpec.AT_MOST) { // 若是隻有寬度都是AT_MOST的話,即只有寬度是wrap_content佈局模式,寬度就用View本身想要的寬度值,高度就用父ViewGroup指定的高度值 setMeasuredDimension(getMeasuredWidth(), heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { // 若是隻有高度都是AT_MOST的話,即只有高度是wrap_content佈局模式,高度就用View本身想要的寬度值,寬度就用父ViewGroup指定的高度值 setMeasuredDimension(widthSpecSize, getMeasuredHeight()); } for (int i = 0; i < mMatchedChildrenList.size(); i++) { View matchChild = getChildAt(i); if (matchChild.getVisibility() != View.GONE) { final LayoutParams layoutParams = matchChild.getLayoutParams(); // 計算子 View 寬的 MeasureSpec final int childWidthMeasureSpec; if (layoutParams.width == LayoutParams.MATCH_PARENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width); } // 計算子 View 高的 MeasureSpec final int childHeightMeasureSpec; if (layoutParams.height == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.height); } // 根據 MeasureSpec 計算本身的寬高 matchChild.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } }
這裏咱們只是重寫了兩個繪製過程當中的重要的方法:onMeasure 和 onLayout 方法。
對於 onMeasure 方法具體邏輯以下:
super.onMeasure 會先計算自定義 view 的大小;
自定義 view 設置的寬高參數不是 MeasureSpec.EXACTLY 的話,對於子 View 是 match_parent 須要額外處理,同時也須要對 MeasureSpec.AT_MOST 狀況進行額外處理。
當自定義View 的大小肯定後,在對子 View 是 match_parent 從新測量;
上述的測量過程的代碼也是參考 FrameLayout 源碼的,具體能夠參看文章:
對於 onLayout 方法,由於是水平滑動的,因此要根據寬度來進行layout。
到這裏咱們的 View 佈局就已經基本結束了。可是要實現 Viewpager
的效果,還須要添加對事件的處理。事件的處理流程以前咱們有分析過,在製做自定義 View 的時候也是會常常用到的,不瞭解的能夠參考文章 Android Touch事件分發超詳細解析。
private void init(Context context) { mScroller = new Scroller(context); mTracker = VelocityTracker.obtain(); } /** * 由於咱們定義的是ViewGroup,從onInterceptTouchEvent開始。 * 重寫onInterceptTouchEvent,對橫向滑動事件進行攔截 * * @param event * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false;//必須不能攔截,不然後續的ACTION_MOME和ACTION_UP事件都會攔截。 break; case MotionEvent.ACTION_MOVE: intercepted = Math.abs(x - mLastX) > Math.abs(y - mLastY); break; } Log.d(TAG, "onInterceptTouchEvent: intercepted " + intercepted); mLastX = x; mLastY = y; return intercepted ? intercepted : super.onInterceptHoverEvent(event); } /** * 當ViewGroup攔截下用戶的橫向滑動事件之後,後續的Touch事件將交付給`onTouchEvent`進行處理。 * 重寫onTouchEvent方法 */ @Override public boolean onTouchEvent(MotionEvent event) { mTracker.addMovement(event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; Log.d(TAG, "onTouchEvent: deltaX " + deltaX); // scrollBy 方法將對咱們當前 View 的位置進行偏移 scrollBy(-deltaX, 0); break; case MotionEvent.ACTION_UP: Log.d(TAG, "onTouchEvent: " + getScrollX()); // getScrollX()爲在X軸方向發生的便宜,mChildWidth * currentIndex表示當前View在滑動開始以前的X座標 // distance存儲的就是這次滑動的距離 int distance = getScrollX() - mChildWidth * mCurrentIndex; //當本次滑動距離>View寬度的1/2時,切換View if (Math.abs(distance) > mChildWidth / 2) { if (distance > 0) { mCurrentIndex++; } else { mCurrentIndex--; } } else { //獲取X軸加速度,units爲單位,默認爲像素,這裏爲每秒1000個像素點 mTracker.computeCurrentVelocity(1000); float xV = mTracker.getXVelocity(); //當X軸加速度>50時,也就是產生了快速滑動,也會切換View if (Math.abs(xV) > 50) { if (xV < 0) { mCurrentIndex++; } else { mCurrentIndex--; } } } //對currentIndex作出限制其範圍爲【0,getChildCount() - 1】 mCurrentIndex = mCurrentIndex < 0 ? 0 : mCurrentIndex > getChildCount() - 1 ? getChildCount() - 1 : mCurrentIndex; //滑動到下一個View smoothScrollTo(mCurrentIndex * mChildWidth, 0); mTracker.clear(); break; } Log.d(TAG, "onTouchEvent: "); mLastX = x; mLastY = y; return super.onTouchEvent(event); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } private void smoothScrollTo(int destX, int destY) { // startScroll方法將產生一系列偏移量,從(getScrollX(), getScrollY()),destX - getScrollX()和destY - getScrollY()爲移動的距離 mScroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000); // invalidate方法會重繪View,也就是調用View的onDraw方法,而onDraw又會調用computeScroll()方法 invalidate(); } // 重寫computeScroll方法 @Override public void computeScroll() { super.computeScroll(); // 當scroller.computeScrollOffset()=true時表示滑動沒有結束 if (mScroller.computeScrollOffset()) { // 調用scrollTo方法進行滑動,滑動到scroller當中計算到的滑動位置 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 沒有滑動結束,繼續刷新View postInvalidate(); } }
具體效果以下圖所示:
對於 Scroller 的用法總結以下:
調用 Scroller 的 startScroll() 方法來進行一些滾動的初始化設置,而後迫使 View 進行繪製 (調用 View 的 invalidate() 或 postInvalidate() 就能夠從新繪製 View);
繪製 View 的時候 drawchild 方法會調用 computeScroll() 方法,重寫 computeScroll(),經過 Scroller 的 computeScrollOffset() 方法來判斷滾動有沒有結束;
scrollTo() 方法雖然會從新繪製 View,但仍是要調用下 invalidate() 或者 postInvalidate() 來觸發界面重繪,從新繪製 View 又觸發 computeScroll();
如此往復進入一個循環階段,便可達到平滑滾動的效果;
也許有人會問,幹嗎還要調用來調用去最後在調用 scrollTo() 方法,還不如直接調用 scrollTo() 方法來實現滾動,其實直接調用是能夠,只不過 scrollTo() 是瞬間滾動的,給人的用戶體驗不太好,因此 Android 提供了 Scroller 類實現平滑滾動的效果。
爲了方面你們理解,我畫了一個簡單的調用示意圖:
到此,自定義 view 的方法就講完了。但願對你們有用。
參考文獻: