android-自定義viewGroup-支持滑動


引子

自定義ViewGroup,用於實現復雜的控件特效。凡是見到的非常花哨牛逼的效果,大多可以分解為若干個 小的效果,然后通過自定義ViewGroup進行組合。但是,在組合的過程中,明明兩個牛逼控件各自運行好好的,組合起來就渾身毛病,比較多見的就是滑動沖突。

今天,提供一個可橫向滑動的ViewGroup,內部可以放置多個子View,而且子View可以帶豎向滑動效果。

本文只提供一個基礎控件,重在提供一個寫控件的思路,也讓我自己日后溫故知新。

 

注意:以下控件並沒有考慮ViewGroup的padding和margin,所以,如果放到真實場景下,必然要做修改。

 

效果圖

(每一個子view都是listView,縱向的滑動效果我沒有錄,相信大家都能看明白)

 

 關鍵類或方法:

1)重寫自定義layout的onMeasure,onLayout,讓某一個子view占滿layout,其他的都在屏幕之外

2)View基類本身自帶的scrollBy方法,配合自定義layout的onTouchEvent截取的觸摸事件,實現滑動

3)重寫自定義layout的onInterceptTouchEvent方法,解決滑動沖突

4)Scroller類,實現layout的平滑回滾,用於當你滑到layout邊界之外時回滾到界內,或者你想滾到某一個子view

5)VelocityTracker類,實現滑動速率的監聽,當滑動速率超過臨界值時,就算沒有滑到下一個子view的臨界點,也要用Scroller來平滑滾動到下一個子view

5)最后提一下,上面幾個都是基於android框架的內容,但是僅僅有他們還不夠,最后需要我們用自己的計算方式,結合1,2,3,4,5的原理,實現我們自己想要的效果。

 

我觀察過網上很多人寫的博客,發現每個人實現這個效果的計算方式各不相同。android框架的原理也許我們都能理解,但是能夠寫出來的控件質量有高有低,就看個人的數學修為了。

不得不說,數學思維邏輯還是很有用的。

 

源代碼(拷貝到項目內可以直接使用)

HorizontalScrollViewEx.java 這個是自定義控件的源碼
 1 package tt.zhou;  2 
 3 import android.content.Context;  4 import android.util.AttributeSet;  5 import android.util.Log;  6 import android.view.MotionEvent;  7 import android.view.VelocityTracker;  8 import android.view.ViewConfiguration;  9 import android.view.ViewGroup;  10 import android.widget.Scroller;  11 
 12 /**
 13  * 可以橫向滾動的viewGroup,兼容縱向滾動的子view  14  */
 15 public class HorizontalScrollViewEx extends ViewGroup {  16 
 17     //第一步,定義一個追蹤器引用
 18     private VelocityTracker mVelocityTracker;//滑動速度追蹤器
 19 
 20 
 21     public HorizontalScrollViewEx(Context context) {  22         this(context, null);  23  }  24 
 25     public HorizontalScrollViewEx(Context context, AttributeSet attrs) {  26         this(context, attrs, 0);  27  }  28 
 29     public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {  30         super(context, attrs, defStyleAttr);  31  init(context);  32  }  33 
 34     private void init(Context context) {  35         mScroller = new Scroller(context);  36         //初始化追蹤器
 37         mVelocityTracker = VelocityTracker.obtain();//獲得追蹤器對象,這里用obtain,按照谷歌的尿性,應該是考慮了對象重用
 38  }  39 
 40     int childCount;  41 
 42     /**
 43  * 確定每一個子view的寬高  44  * <p>  45  * 如果是逐個去測量子view的話,必須在測量之后,調用setMeasuredDimension來設置寬高  46  * <p>  47  * 這里測量出來的寬高,會在onLayout中用來作為參考  48  *  49  * @param widthMeasureSpec  50  * @param heightMeasureSpec  51      */
 52  @Override  53     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//spec 測量模式,
 54 
 55         int width = MeasureSpec.getSize(widthMeasureSpec);  56         int height = MeasureSpec.getSize(heightMeasureSpec);  57         int widthMode = MeasureSpec.getMode(widthMeasureSpec);  58         int heightMode = MeasureSpec.getMode(heightMeasureSpec);  59 
 60         childCount = getChildCount();  61         measureChildren(widthMeasureSpec, heightMeasureSpec);//逐個測量所有的子view
 62 
 63         if (childCount == 0) {//如果子view數量為0,
 64             setMeasuredDimension(0, 0);//那么整個viewGroup寬高也就是0
 65         } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {//如果viewGroup的寬高都是matchParent
 66             width = childCount * getChildAt(0).getMeasuredWidth();// 那么,本viewGroup的寬,就是index為0的子view的測量寬度 乘以 子view的個數
 67             height = getChildAt(0).getMeasuredHeight();//高,就是子view的高
 68             setMeasuredDimension(width, height);//用子view的寬高,來設定
 69         } else if (widthMode == MeasureSpec.AT_MOST) {  70             width = childCount * getChildAt(0).getMeasuredWidth();  71  setMeasuredDimension(width, height);  72         } else {  73             height = getChildAt(0).getMeasuredHeight();  74  setMeasuredDimension(width, height);  75             Log.d("setMeasuredDimension", "" + width);  76  }  77  }  78 
 79     /**
 80  * 這個方法用於,處理布局所有的子view,讓他們按照代碼寫的規則去排布  81  *  82  * @param changed  83  * @param l left,當前viewGroup的左邊線距離父組件左邊線的距離  84  * @param t top,當前viewGroup的上邊線距離父組件上邊線的距離  85  * @param r right,當前viewGroup的左邊線距離父組件右邊線的距離  86  * @param b bottom,當前viewGroup的上邊線距離父組件下邊線的距離  87      */
 88  @Override  89     protected void onLayout(boolean changed, int l, int t, int r, int b) {  90         Log.d("onLayout", ":" + l + "-" + t + "-" + r + "-" + b);  91         int count = getChildCount();  92         int offsetX = 0;  93         for (int i = 0; i < count; i++) {  94             int w = getChildAt(i).getMeasuredWidth();  95             int h = getChildAt(i).getMeasuredHeight();  96             Log.d("onLayout", "w:" + w + " - h:" + h);  97 
 98             getChildAt(i).layout(offsetX + l, t, offsetX + l + w, b);//保證每次都最多只完整顯示一個子view,因為在onMeasure中,已經將子view的寬度設置為了 本viewGroup的寬度
 99             offsetX += w;//每次的偏移量都遞增
100  } 101  } 102 
103 
104     private float lastInterceptX, lastInterceptY; 105 
106     /**
107  * 事件的攔截, 108  * 109  * @param event 110  * @return
111      */
112  @Override 113     public boolean onInterceptTouchEvent(MotionEvent event) { 114         boolean ifIntercept = false; 115         switch (event.getAction()) { 116             case MotionEvent.ACTION_DOWN: 117                 lastInterceptX = event.getRawX(); 118                 lastInterceptY = event.getRawY(); 119                 break; 120             case MotionEvent.ACTION_MOVE: 121                 //檢查是橫向移動的距離大,還是縱向
122                 float xDistance = Math.abs(lastInterceptX - event.getRawX()); 123                 float yDistance = Math.abs(lastInterceptY - event.getRawY()); 124                 if (xDistance > yDistance) { 125                     ifIntercept = true; 126                 } else { 127                     ifIntercept = false; 128  } 129                 break; 130             case MotionEvent.ACTION_UP: 131                 break; 132             case MotionEvent.ACTION_CANCEL: 133                 break; 134  } 135         return ifIntercept; 136  } 137 
138     private float downX; 139     private float distanceX; 140     private boolean isFirstTouch = true; 141     private int childIndex = -1; 142 
143  @Override 144     public boolean onTouchEvent(MotionEvent event) { 145         int scrollX = getScrollX();//控件的左邊界,與屏幕原點的X軸坐標
146         int scrollXMax = (getChildCount() - 1) * getChildAt(1).getMeasuredWidth(); 147         final int childWidth = getChildAt(0).getWidth(); 148         mVelocityTracker.addMovement(event);//在onTouchEvent這里,截取event對象
149         ViewConfiguration configuration = ViewConfiguration.get(getContext()); 150         switch (event.getAction()) { 151             case MotionEvent.ACTION_DOWN: 152                 break; 153             case MotionEvent.ACTION_MOVE: 154                 //先讓你滑動起來
155                 float moveX = event.getRawX(); 156                 if (isFirstTouch) {//一次事件序列,只會賦值一次?
157                     downX = moveX; 158                     isFirstTouch = false; 159  } 160                 Log.d("distanceX", "" + downX + "|" + moveX + "|" + distanceX); 161                 distanceX = downX - moveX; 162 
163                 //判定是否可以滑動 164                 //這里有一個隱患,由於不知道Move事件,會以什么頻率來分發,所以,這里多少都會出現一點誤差
165                 if (getChildCount() >= 2) {//子控件在2個或者2個以上時,才有下面的效果 166                     //如果命令是向左滑動,distanceX>0 ,那么判斷命令是否可以執行 167                     //如果命令是向右滑動,distanceX<0 ,那么判斷命令是否可以執行
168                     Log.d("scrollX", "scrollX:" + scrollX); 169                     if (distanceX <= 0) { 170                         if (scrollX >= 0) 171                             scrollBy((int) distanceX, 0);//滑動
172                     } else { 173                         if (scrollX <= scrollXMax) 174                             scrollBy((int) distanceX, 0);//滑動
175  } 176                 }//如果只有一個,則不允許滑動,防止bug
177                 break; 178             case MotionEvent.ACTION_UP:// 當手指松開的時候,要顯示某一個完整的子view
179                 mVelocityTracker.computeCurrentVelocity(1000, configuration.getScaledMaximumFlingVelocity());//計算,最近的event到up之間的速率
180                 float xVelocity = mVelocityTracker.getXVelocity();//當前橫向的移動速率
181                 float edgeXVelocity = configuration.getScaledMinimumFlingVelocity();//臨界點
182                 childIndex = (scrollX + childWidth / 2) / childWidth;//整除的方式,來確定X軸應該所在的單元,將每一個item的豎向中間線定為滑動的臨界線
183                 if (Math.abs(xVelocity) > edgeXVelocity) {//如果當前橫向的速率大於零界點,
184                     childIndex = xVelocity > 0 ? (childIndex - 1) : (childIndex + 1);//xVelocity正數,表示從左往右滑,所以child應該是要顯示前面一個
185  } 186 // childIndex = Math.min(getChildCount() - 1, Math.max(childIndex, 0));//不可以超出左右邊界,這種寫法可能很難一眼看懂,那就替換成下面的寫法
187                 if (childIndex < 0)//計算出的childIndex可能是負數。那就賦值為0
188                     childIndex = 0; 189                 else if (childIndex >= getChildCount()) {//也有可能超出childIndex的最大值,那就賦值為最大值-1
190                     childIndex = getChildCount() - 1; 191  } 192                 smoothScrollBy(childIndex * childWidth - scrollX, 0);// 回滾的距離
193  mVelocityTracker.clear(); 194                 isFirstTouch = true; 195                 break; 196             case MotionEvent.ACTION_CANCEL: 197                 break; 198  } 199         downX = event.getRawX(); 200         return super.onTouchEvent(event); 201  } 202 
203     //實現平滑地回滾
204 
205     /**
206  * 最叼的還是這個方法,平滑地回滾,從當前位置滾到目標位置 207  * @param dx 208  * @param dy 209      */
210     void smoothScrollBy(int dx, int dy) { 211         mScroller.startScroll(getScrollX(), getScrollY(), dx, dy, 500);//從當前滑動的位置,平滑地過度到目標位置
212  invalidate(); 213  } 214 
215  @Override 216     public void computeScroll() { 217         if (mScroller.computeScrollOffset()) { 218  scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); 219  invalidate(); 220  } 221  } 222 
223     private Scroller mScroller;//這個scroller是為了平滑滑動
224 }

 

 

 activity_main.xml 這個是引用自定義控件的布局文件(記得改控件的包名)

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     xmlns:tools="http://schemas.android.com/tools"
 4     android:layout_width="match_parent"
 5     android:layout_height="match_parent"
 6     tools:context=".MainActivity">
 7 
 8 
 9     <tt.zhou.HorizontalScrollViewEx 10         android:layout_width="match_parent"
11         android:layout_height="match_parent">
12 
13         <ListView 14             android:id="@+id/lv_1"
15             android:layout_width="match_parent"
16             android:layout_height="match_parent"
17             android:background="@android:color/holo_blue_dark"></ListView>
18 
19         <ListView 20             android:id="@+id/lv_2"
21             android:layout_width="match_parent"
22             android:layout_height="match_parent"
23             android:background="@android:color/holo_green_light"></ListView>
24 
25         <ListView 26             android:id="@+id/lv_3"
27             android:layout_width="match_parent"
28             android:layout_height="match_parent"
29             android:background="@android:color/darker_gray"></ListView>
30 
31         <ListView 32             android:id="@+id/lv_4"
33             android:layout_width="match_parent"
34             android:layout_height="match_parent"
35             android:background="@android:color/holo_blue_dark"></ListView>
36 
37         <ListView 38             android:id="@+id/lv_5"
39             android:layout_width="match_parent"
40             android:layout_height="match_parent"
41             android:background="@android:color/holo_green_light"></ListView>
42     </tt.zhou.HorizontalScrollViewEx>
43 
44 
45 </LinearLayout>

MainActivity.java  

 1 package tt.zhou;  2 
 3 import android.app.Activity;  4 import android.os.Bundle;  5 import android.widget.ArrayAdapter;  6 import android.widget.ListView;  7 
 8 import java.util.ArrayList;  9 import java.util.List; 10 
11 public class MainActivity extends Activity { 12 
13  ListView lv_1, lv_2, lv_3, lv_4, lv_5; 14 
15  @Override 16     protected void onCreate(Bundle savedInstanceState) { 17         super.onCreate(savedInstanceState); 18  setContentView(R.layout.activity_main); 19  initData(); 20  init(); 21  } 22 
23     private void init() { 24         lv_1 = findViewById(R.id.lv_1); 25         lv_2 = findViewById(R.id.lv_2); 26         lv_3 = findViewById(R.id.lv_3); 27         lv_4 = findViewById(R.id.lv_4); 28         lv_5 = findViewById(R.id.lv_5); 29 
30         ArrayAdapter<String> adapter1 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data1); 31  lv_1.setAdapter(adapter1); 32         ArrayAdapter<String> adapter2 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data2); 33  lv_2.setAdapter(adapter2); 34         ArrayAdapter<String> adapter3 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data3); 35  lv_3.setAdapter(adapter3); 36         ArrayAdapter<String> adapter4 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data4); 37  lv_4.setAdapter(adapter4); 38         ArrayAdapter<String> adapter5 = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, data5); 39  lv_5.setAdapter(adapter5); 40  } 41 
42     private List<String> data1, data2, data3, data4, data5; 43 
44     private void initData() { 45         data1 = new ArrayList<>(); 46         for (int i = 0; i < 100; i++) { 47             data1.add("d1-" + i); 48  } 49         data2 = new ArrayList<>(); 50         for (int i = 0; i < 100; i++) { 51             data2.add("d2-" + i); 52  } 53         data3 = new ArrayList<>(); 54         for (int i = 0; i < 100; i++) { 55             data3.add("d3-" + i); 56  } 57         data4 = new ArrayList<>(); 58         for (int i = 0; i < 100; i++) { 59             data4.add("d4-" + i); 60  } 61         data5 = new ArrayList<>(); 62         for (int i = 0; i < 100; i++) { 63             data5.add("d5-" + i); 64  } 65  } 66 }

 結語

上面的例子是,橫向的layout,兼容豎向滑動的子view。

那么,按照這個原理,實現一個豎向的laytou,兼容橫向滑動的子view,理解了上面提到的5個原理的同志們應該很容易寫出來啦。

 

就醬紫咯。๑乛◡乛๑


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM