【Android】无限滚动的HorizontalScrollView

  这是一个很简单的功能,作为新手,做一下笔记。也给其它有需要的人提供一个参考。

  首先HorizontalScrollView都知道用途了。它可以实现类似“桌面程序”左右切换页的效果。一般情况下里面的页数都是固定的,但是也有可能遇到不固定页数的,比如动态加载照片,或者像我这次需要实现的情况。

  实现功能:实现日历的“日视图”。一页表示某一天的情况,向右翻页可翻到下一天,向左翻到上一天。而且可以随意翻到任意一天。每一页记录有当天的待办事项(这个就另外写了,这里只实现无限左右翻页)。

  由于每天作为一页,把所有天数都加载到ScrollView中是不现实的。考虑到内存占用问题,ScrollView中肯定不要放太多东西的好。所以只放3页。如下图所示:

【Android】无限滚动的HorizontalScrollView

  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>

 

你可能感兴趣的:(【Android】无限滚动的HorizontalScrollView)