這是一個很簡單的功能,作為新手,做一下筆記。也給其它有需要的人提供一個參考。
首先HorizontalScrollView都知道用途了。它可以實現類似“桌面程序”左右切換頁的效果。一般情況下里面的頁數都是固定的,但是也有可能遇到不固定頁數的,比如動態加載照片,或者像我這次需要實現的情況。
實現功能:實現日歷的“日視圖”。一頁表示某一天的情況,向右翻頁可翻到下一天,向左翻到上一天。而且可以隨意翻到任意一天。每一頁記錄有當天的待辦事項(這個就另外寫了,這里只實現無限左右翻頁)。
由於每天作為一頁,把所有天數都加載到ScrollView中是不現實的。考慮到內存占用問題,ScrollView中肯定不要放太多東西的好。所以只放3頁。如下圖所示:

HorizontalScrollView中僅有3頁。在第0頁,向左翻的時候,松手的一瞬間,1頁消失,-2頁加載,然后smoothScroll到 -1頁。向右亦然。因此可以保證不占用過多內存,又可以無限翻頁。
實現的時候還遇到一點小問題,順便也一起寫下來。
一開始我很自然地想到重寫HorizontalScrollView。即代碼中的KamHorizontalScrollView。
- 主要是重寫onTouchEvent方法,用於處理用戶手指滑動事件。
- 由於每個子View占一屏,不可以出現兩個View各占一半的現象,需要有一個將子View分頁化的方法。在我的代碼中就是public boolean scrollToPage(int);該方法傳入的是頁碼,可以保證滑動到正確的位置,而不會滑一半。
- 還有一點要注意,翻頁的時候,為了保證ScrollView只有3頁,需要增加刪除子View。在末尾增刪沒有問題,但在首部增加,所有的子View會向后移動,在首部刪除,所有的子View會向前移動,因此這兩個操作需要立即改變ScrollView的scroll值以保證屏幕顯示順滑。這一點在方法public boolean addLeft(View);和public boolean removeLeft();中均有體現。
接下來是我寫代碼過程中出現的一些問題:
1、重寫HorizontalScrollView后,我在構造函數中進行初始化,添加三個初始的子View,報錯。
解決:構造函數調用的時候,ScrollView還沒有實例化,因此這個時候不能添加子View。應該等實例化之后再添加。重寫protected void onFinishInflate();方法即可得到實例化完成的時機,在該方法下初始化。
2、左右滑動的時候,我調用的是scrollToPage(1);但是總是滑動到第2頁或第0頁。就是不能定在第1頁。
解決:這個問題我花了不少時間(其實如果我去認真看看API就能很快搞定的)。通過Log,發現addView之后,新的View的Left值仍是0。或者說addView之后的一瞬間,layout中的所有子View還是保持原有的狀態。過了一陣子才又重新排列的。所以我需要獲得他們重新排列的時機,才能scroll到正確位置。之前寫JavaSE的自定義布局有重寫過排列布局的方法,所以這個也有。說白了就是onLayout方法(我以為是onMeasure,試了不行很糾結)。onLayout方法中,應該就是對所有子View進行重新排列了。(這一點可以自己去試試,先addView,然后立刻獲取剛才這個View的位置和尺寸,會發現都是0。然后你可以通過按鍵事件再獲取一次,會發現得到正確值了。因為從addView到按鍵這段時間足夠他重新排列了。)
所以通過LinearLayout.addOnLayoutChangeListener(listener);就可以監聽重新排列的時機。但是該方法需要APILevel 11及以上。剛好我在用我的G12測試,APILevel10。雖然很糾結,我也只能重寫了LinearLayout,即代碼中的KamLinearLayout,還有自定義監聽器kamLayoutChangeListener。重寫僅僅是為了在Android2.3監聽onLayout。
另外加了一點小細節,翻頁的機制除了手指滑動的距離,還有手指滑動的速度。自己寫的SpeedChange三個方法。測試了一下感覺效果挺不錯。
最后把源碼附上,注釋寫了比較詳細的,希望能幫助到初學者。不要像我走太多彎路。(注意改包名)
KamHorizontalScrollView.java
1 package com.kam.horizontalscrollviewtest.view; 2 3 import com.kam.horizontalscrollviewtest.R; 4 5 import android.content.Context; 6 import android.graphics.Color; 7 import android.util.AttributeSet; 8 import android.util.DisplayMetrics; 9 import android.util.Log; 10 import android.view.Gravity; 11 import android.view.MotionEvent; 12 import android.view.View; 13 import android.view.ViewGroup; 14 import android.widget.HorizontalScrollView; 15 import android.widget.LinearLayout; 16 import android.widget.TextView; 17 import android.widget.FrameLayout.LayoutParams; 18 /*如果不需要支持Android2.3,可以將代碼中所有KamLinearLayout替換為ViewGroup*/ 19 public class KamHorizontalScrollView extends HorizontalScrollView { 20 private static String tag = "KamHorizontalScrollView"; 21 private Context context; 22 23 /*記錄當前的頁數標識(做日視圖的時候可以和該值今日的日期作差)*/ 24 private int PageNo=0; 25 26 /*保存ScrollView中的ViewGroup,如果不需要支持Android2.3,可以將KamLinearLayout替換為ViewGroup*/ 27 private KamLinearLayout childGroup = null; 28 29 /*這是判斷左右滑動用的(個人喜好,其實不需要這么麻煩)*/ 30 private int poscache[] = new int[4]; 31 private int startpos; 32 33 public KamHorizontalScrollView(Context context, AttributeSet attrs, 34 int defStyle) { 35 super(context, attrs, defStyle); 36 // TODO Auto-generated constructor stub 37 this.context=context; 38 } 39 public KamHorizontalScrollView(Context context, AttributeSet attrs) { 40 super(context, attrs); 41 // TODO Auto-generated constructor stub 42 this.context=context; 43 } 44 public KamHorizontalScrollView(Context context) { 45 super(context); 46 // TODO Auto-generated constructor stub 47 this.context=context; 48 } 49 50 /*重寫觸摸事件,判斷左右滑動*/ 51 @Override 52 public boolean onTouchEvent(MotionEvent ev) { 53 switch (ev.getAction()) { 54 case MotionEvent.ACTION_DOWN: 55 startpos = (int) ev.getX(); 56 /*用於判斷觸摸滑動的速度*/ 57 initSpeedChange((int) ev.getX()); 58 break; 59 case MotionEvent.ACTION_MOVE: { 60 /*更新觸摸速度信息*/ 61 movingSpeedChange((int) ev.getX()); 62 } 63 break; 64 case MotionEvent.ACTION_UP: 65 case MotionEvent.ACTION_CANCEL: { 66 /*先根據速度來判斷向左或向右*/ 67 int speed = releaseSpeedChange((int) ev.getX()); 68 if(speed>0){ 69 nextPage(); 70 return true; 71 } 72 if(speed<0){ 73 prevPage(); 74 return true; 75 } 76 77 /*這里是根據觸摸起始和結束位置來判斷向左或向右*/ 78 if (Math.abs((ev.getX() - startpos)) > getWidth() / 4) { 79 if (ev.getX() - startpos > 0) { 80 /*向左*/ 81 prevPage(); 82 } else { 83 /*向右*/ 84 nextPage(); 85 } 86 } else { 87 /*不變*/ 88 scrollToPage(1); 89 } 90 return true; 91 } 92 } 93 return super.onTouchEvent(ev); 94 } 95 96 /*完成實例化*/ 97 @Override 98 protected void onFinishInflate(){ 99 super.onFinishInflate(); 100 Log.i(tag, "onFinishInflate Called!"); 101 init(); 102 } 103 104 /*初始化,加入三個子View*/ 105 private void init(){ 106 this.childGroup=(KamLinearLayout) findViewById(R.id.container); 107 /*添加LayoutChange監聽器*/ 108 childGroup.addKamLayoutChangeListener(listener); 109 /*調用其自身的LayoutChange監聽器(不支持Android2.3)*/ 110 /*childGroup.addOnLayoutChangeListener(listener);*/ 111 112 addRight(createExampleView(-1)); 113 addRight(createExampleView(0)); 114 addRight(createExampleView(1)); 115 } 116 /*添加監聽器*/ 117 kamLayoutChangeListener listener = new kamLayoutChangeListener() { 118 119 @Override 120 public void onLayoutChange() { 121 // TODO Auto-generated method stub 122 Log.i(tag, "onLayoutChanged Called!"); 123 scrollToPage(1); 124 } 125 }; 126 /* 127 //注意,如果不需要支持Android2.3,可以將上面的listener替換成下方listener 128 OnLayoutChangeListener listener = new OnLayoutChangeListener() { 129 130 @Override 131 public void onLayoutChange(View arg0, int arg1, int arg2, int arg3, 132 int arg4, int arg5, int arg6, int arg7, int arg8) { 133 // TODO Auto-generated method stub 134 Log.i(tag, "onLayoutChanged Called!"); 135 scrollToPage(1); 136 } 137 }; 138 */ 139 140 /*左翻頁*/ 141 public void prevPage(){ 142 PageNo--; 143 addLeft(createExampleView(PageNo-1)); 144 removeRight(); 145 } 146 147 /*右翻頁*/ 148 public void nextPage(){ 149 PageNo++; 150 addRight(createExampleView(PageNo+1)); 151 removeLeft(); 152 } 153 154 155 /*獲取某個孩子的X坐標*/ 156 private int getChildLeft(int index){ 157 if (index>=0 && childGroup != null) { 158 if(index< childGroup.getChildCount()) 159 return childGroup.getChildAt(index).getLeft(); 160 } 161 return 0; 162 } 163 164 /** 165 * 向右邊添加View 166 * @param view 需要添加的View 167 * @return true添加成功|false添加失敗 168 */ 169 public boolean addRight(View view){ 170 if(view==null || childGroup==null)return false; 171 childGroup.addView(view); 172 return true; 173 } 174 175 /** 176 * 刪除右邊的View 177 * @return true成功|false失敗 178 */ 179 public boolean removeRight(){ 180 if( childGroup==null || childGroup.getChildCount()<=0)return false; 181 childGroup.removeViewAt(childGroup.getChildCount()-1); 182 return true; 183 } 184 185 /** 186 * 向左邊添加View 187 * @param view 需要添加的View 188 * @return true添加成功|false添加失敗 189 */ 190 public boolean addLeft(View view){ 191 if(view==null || childGroup==null)return false; 192 childGroup.addView(view, 0); 193 194 /*因為在左邊增加了View,因此所有View的x坐標都會增加,因此需要讓ScrollView也跟着移動,才能從屏幕看來保持平滑。*/ 195 int tmpwidth = view.getLayoutParams().width; 196 if(tmpwidth==0)tmpwidth=getWinWidth(); 197 Log.i(tag, "the new view's width = "+view.getLayoutParams().width); 198 this.scrollTo(this.getScrollX()+tmpwidth, 0); 199 200 return true; 201 } 202 203 /** 204 * 刪除左邊的View 205 * @return true成功|false失敗 206 */ 207 public boolean removeLeft(){ 208 if( childGroup==null || childGroup.getChildCount()<=0)return false; 209 210 /*因為在左邊刪除了View,因此所有View的x坐標都會減少,因此需要讓ScrollView也跟着移動。*/ 211 int tmpwidth=childGroup.getChildAt(0).getWidth(); 212 childGroup.removeViewAt(0); 213 this.scrollTo((int) (this.getScrollX()-tmpwidth), 0); 214 215 return true; 216 } 217 218 /** 219 * 跳轉到指定的頁面 220 * 221 * @param index 跳轉的頁碼 222 * @return 223 */ 224 public boolean scrollToPage(int index){ 225 if(childGroup==null)return false; 226 if(index<0 || index >= childGroup.getChildCount())return false; 227 smoothScrollTo(getChildLeft(index), 0); 228 return true; 229 } 230 231 private int getWinWidth() { 232 DisplayMetrics dm = new DisplayMetrics(); 233 // 獲取屏幕信息 234 dm = context.getResources().getDisplayMetrics(); 235 return dm.widthPixels; 236 } 237 238 private int getWinHeight() { 239 DisplayMetrics dm = new DisplayMetrics(); 240 // 獲取屏幕信息 241 dm = context.getResources().getDisplayMetrics(); 242 return dm.heightPixels; 243 } 244 /*生成一個測試用View。真正使用的時候就不需要這個了。*/ 245 private View createExampleView(int index){ 246 LayoutParams params = new LayoutParams(getWinWidth(), getWinHeight()); 247 /*設置不同的背景色使效果更加明顯*/ 248 int colorarr[] = { 249 Color.rgb(240, 180, 180), 250 Color.rgb(240, 240, 180), 251 Color.rgb(180, 240, 240), 252 Color.rgb(180, 240, 180)}; 253 TextView txtview = new TextView(context); 254 txtview.setBackgroundColor(colorarr[(index%4+4) % 4]); 255 txtview.setText(index + ""); 256 txtview.setTextSize(40); 257 txtview.setGravity(Gravity.CENTER); 258 txtview.setLayoutParams(params); 259 260 return txtview; 261 } 262 263 264 /*下面的方法僅僅是個人喜好加上的,用於判斷用戶手指左右滑動的速度。*/ 265 private void initSpeedChange(int x){ 266 if(poscache.length<=1)return; 267 poscache[0]=1; 268 for(int i=1;i<poscache.length;i++){ 269 270 } 271 } 272 private void movingSpeedChange(int x){ 273 poscache[0]%=poscache.length-1; 274 poscache[0]++; 275 //Log.i(tag, "touch speed:"+(x-poscache[poscache[0]])); 276 poscache[poscache[0]]=x; 277 } 278 private int releaseSpeedChange(int x){ 279 return releaseSpeedChange(x, 30); 280 } 281 private int releaseSpeedChange(int x,int limit){ 282 poscache[0]%=poscache.length-1; 283 poscache[0]++; 284 /*檢測到向左的速度很大*/ 285 if(poscache[poscache[0]]-x>limit)return 1; 286 /*檢測到向右的速度很大*/ 287 if(x-poscache[poscache[0]]>limit)return -1; 288 289 return 0; 290 } 291 }
KamLinearLayout.java (如果不需要支持APILevel 10及以下,可以無視這個類)
1 package com.kam.horizontalscrollviewtest.view; 2 3 import android.content.Context; 4 import android.util.AttributeSet; 5 import android.widget.LinearLayout; 6 7 public class KamLinearLayout extends LinearLayout { 8 kamLayoutChangeListener listener = null; 9 10 public void addKamLayoutChangeListener(kamLayoutChangeListener listener){ 11 this.listener=listener; 12 } 13 14 public KamLinearLayout(Context context) { 15 super(context); 16 // TODO Auto-generated constructor stub 17 } 18 public KamLinearLayout(Context context, AttributeSet attrs) { 19 super(context, attrs); 20 // TODO Auto-generated constructor stub 21 } 22 23 24 @Override 25 public void onLayout(boolean changed, 26 int l, int t, int r, int b){ 27 super.onLayout(changed, l, t, r, b); 28 if(this.listener!=null)this.listener.onLayoutChange(); 29 } 30 31 } 32 /*自定義監聽器*/ 33 interface kamLayoutChangeListener{ 34 abstract void onLayoutChange(); 35 36 }
MainActivity.java
1 package com.kam.horizontalscrollviewtest; 2 3 import android.support.v7.app.ActionBarActivity; 4 import android.os.Bundle; 5 import android.view.Menu; 6 import android.view.MenuItem; 7 8 public class MainActivity extends ActionBarActivity { 9 10 @Override 11 protected void onCreate(Bundle savedInstanceState) { 12 super.onCreate(savedInstanceState); 13 setContentView(R.layout.kamhsview); 14 } 15 16 @Override 17 public boolean onCreateOptionsMenu(Menu menu) { 18 // Inflate the menu; this adds items to the action bar if it is present. 19 getMenuInflater().inflate(R.menu.main, menu); 20 return true; 21 } 22 23 @Override 24 public boolean onOptionsItemSelected(MenuItem item) { 25 // Handle action bar item clicks here. The action bar will 26 // automatically handle clicks on the Home/Up button, so long 27 // as you specify a parent activity in AndroidManifest.xml. 28 int id = item.getItemId(); 29 if (id == R.id.action_settings) { 30 return true; 31 } 32 return super.onOptionsItemSelected(item); 33 } 34 }
kamhsview.xml
<?xml version="1.0" encoding="utf-8"?> <com.kam.horizontalscrollviewtest.view.KamHorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/kamscrollview" android:fadingEdge="none" android:scrollbars="none" > <!-- 如果你不需要支持Android2.3,可以把后面的KamLinearLayout替換成普通的LinearLayout <LinearLayout android:id="@+id/container1" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > </LinearLayout > --> <com.kam.horizontalscrollviewtest.view.KamLinearLayout android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> </com.kam.horizontalscrollviewtest.view.KamLinearLayout> </com.kam.horizontalscrollviewtest.view.KamHorizontalScrollView>
AndroidManifest就是默認的那個,沒有改。
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.kam.horizontalscrollviewtest" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="10" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppBaseTheme" > <activity android:name=".MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
