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

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

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

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

【Android】无限滚动的HorizontalScrollView-LMLPHP

  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

【Android】无限滚动的HorizontalScrollView-LMLPHP
  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 }
【Android】无限滚动的HorizontalScrollView-LMLPHP

KamLinearLayout.java (如果不需要支持APILevel 10及以下,可以无视这个类)

【Android】无限滚动的HorizontalScrollView-LMLPHP
 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 }
【Android】无限滚动的HorizontalScrollView-LMLPHP

MainActivity.java

【Android】无限滚动的HorizontalScrollView-LMLPHP
 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 }
【Android】无限滚动的HorizontalScrollView-LMLPHP

kamhsview.xml

【Android】无限滚动的HorizontalScrollView-LMLPHP
<?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>
【Android】无限滚动的HorizontalScrollView-LMLPHP

AndroidManifest就是默认的那个,没有改。

【Android】无限滚动的HorizontalScrollView-LMLPHP
<?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-LMLPHP
05-08 15:09