【Android】無限滾動的HorizontalScrollView


  這是一個很簡單的功能,作為新手,做一下筆記。也給其它有需要的人提供一個參考。

  首先HorizontalScrollView都知道用途了。它可以實現類似“桌面程序”左右切換頁的效果。一般情況下里面的頁數都是固定的,但是也有可能遇到不固定頁數的,比如動態加載照片,或者像我這次需要實現的情況。

  實現功能:實現日歷的“日視圖”。一頁表示某一天的情況,向右翻頁可翻到下一天,向左翻到上一天。而且可以隨意翻到任意一天。每一頁記錄有當天的待辦事項(這個就另外寫了,這里只實現無限左右翻頁)。

  由於每天作為一頁,把所有天數都加載到ScrollView中是不現實的。考慮到內存占用問題,ScrollView中肯定不要放太多東西的好。所以只放3頁。如下圖所示:

  HorizontalScrollView中僅有3頁。在第0頁,向左翻的時候,松手的一瞬間,1頁消失,-2頁加載,然后smoothScroll到 -1頁。向右亦然。因此可以保證不占用過多內存,又可以無限翻頁。

 

  實現的時候還遇到一點小問題,順便也一起寫下來。

  一開始我很自然地想到重寫HorizontalScrollView。即代碼中的KamHorizontalScrollView

  1. 主要是重寫onTouchEvent方法,用於處理用戶手指滑動事件。
  2. 由於每個子View占一屏,不可以出現兩個View各占一半的現象,需要有一個將子View分頁化的方法。在我的代碼中就是public boolean scrollToPage(int);該方法傳入的是頁碼,可以保證滑動到正確的位置,而不會滑一半。
  3. 還有一點要注意,翻頁的時候,為了保證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>

 


免責聲明!

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



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