引子
自定義ViewGroup,用於實現複雜的控件特效。凡是見到的很是花哨牛逼的效果,大多能夠分解爲若干個 小的效果,而後經過自定義ViewGroup進行組合。可是,在組合的過程當中,明明兩個牛逼控件各自運行好好的,組合起來就渾身毛病,比較多見的就是滑動衝突。java
今天,提供一個可橫向滑動的ViewGroup,內部能夠放置多個子View,並且子View能夠帶豎向滑動效果。android
本文只提供一個基礎控件,重在提供一個寫控件的思路,也讓我本身往後溫故知新。app
注意:如下控件並無考慮ViewGroup的padding和margin,因此,若是放到真實場景下,必然要作修改。框架
效果圖
(每個子view都是listView,縱向的滑動效果我沒有錄,相信你們都能看明白)
關鍵類或方法:
1)重寫自定義layout的onMeasure,onLayout,讓某一個子view佔滿layout,其餘的都在屏幕以外ide
2)View基類自己自帶的scrollBy方法,配合自定義layout的onTouchEvent截取的觸摸事件,實現滑動佈局
3)重寫自定義layout的onInterceptTouchEvent方法,解決滑動衝突this
4)Scroller類,實現layout的平滑回滾,用於當你滑到layout邊界以外時回滾到界內,或者你想滾到某一個子viewspa
5)VelocityTracker類,實現滑動速率的監聽,當滑動速率超過臨界值時,就算沒有滑到下一個子view的臨界點,也要用Scroller來平滑滾動到下一個子viewcode
5)最後提一下,上面幾個都是基於android框架的內容,可是僅僅有他們還不夠,最後須要咱們用本身的計算方式,結合1,2,3,4,5的原理,實現咱們本身想要的效果。xml
我觀察過網上不少人寫的博客,發現每一個人實現這個效果的計算方式各不相同。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個原理的同志們應該很容易寫出來啦。
就醬紫咯。๑乛◡乛๑