Android利用HorizontalScrollView仿ViewPager设计简单相册

内容摘要
最近学习了一个视频公开课,讲到了利用HorizontalScrollView仿ViewPager设计的一个简单相册,其实主要用了ViewPager缓存的思想。此篇文章参考:Android自定义HorizontalScrollVie
文章正文

最近学习了一个视频公开课,讲到了利用HorizontalScrollView仿ViewPager设计的一个简单相册,其实主要用了ViewPager缓存的思想。此篇文章参考:Android自定义HorizontalScrollView打造超强Gallery效果(这篇文章与公开课的讲的大致一样)

 这里简单说一下ViewPager的缓存机制

       1.进入ViewPager时,加载当前页和后一页;

       2.当滑动ViewPager至下一页时,加载后一页,此时第一页是不会销毁的,同时加载当前页的下一页。

其实就是默认加载3页,当前页,前一页和后一页。

而此HorizontalScrollView是默认加载两页的,这个要注意,不然调度代码会让人晕。

话不多说,上代码:

代码结构如下图:

一个View,一个Adapter,一个MainActivity,相信不用解释,大家也相当清楚了,典型的MVC模式~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
package com.ssa.horizontalscrollview.myview;
  
import java.util.HashMap;
import java.util.Map;
  
import com.ssa.horizontalscrollview.myUtils.DisplayUtil;
  
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
  
public class GalleryHorizontalScrollView extends HorizontalScrollView implements
    OnClickListener {
  private LinearLayout mContainer;// MyHorizontalScrollView中的LinearLayout
  private int mChildWidth;// 子元素的宽度
  private int mChildHeight;// 子元素的高度
  
  private int mAllLastIndex;// 当前的最后一张的index
  private int mdisplayLastIndex;// 当前显示的最后一张的index
  private int mAllFirstIndex;// 当前的第一张index
  
  private GalleryHorizontalScrollViewAdapter mAdapter;// 数据适配器
  private int mScreenWidth;// 屏幕的宽度
  
  private int mCountOneScreen;
  
  private Map<View, Integer> mViewPos = new HashMap<View, Integer>();
  
  private OnCurrentImageChangeListener mOnCurrentImageChangeListener;
  
  private OnClickImageChangeListener mOnClickImageChangeListener;
  
  public void setmOnCurrentImageChangeListener(
      OnCurrentImageChangeListener mListener) {
    this.mOnCurrentImageChangeListener = mListener;
  }
  
  public void setmOnClickImageListener(OnClickImageChangeListener mListener) {
    this.mOnClickImageChangeListener = mListener;
  }
  
  /**
   * 图片滚动时回调接口
   */
  public interface OnCurrentImageChangeListener {
    void onCurrentImgChanged(int position, View view);
  }
  
  /**
   * 点击图片时回调接口
   */
  public interface OnClickImageChangeListener {
    void onClickImageChangeListener(int position, View view);
  }
  
  public GalleryHorizontalScrollView(Context context, AttributeSet attrs) {
    super(context, attrs);
    // 获取屏幕宽度
    mScreenWidth = getResources().getDisplayMetrics().widthPixels;
  }
  
  /**
   * 初始化数据,设置适配器
   */
  public void initData(GalleryHorizontalScrollViewAdapter mAdapter) {
    this.mAdapter = mAdapter;
    mContainer = (LinearLayout) getChildAt(0);
    final View view = mAdapter.getView(0, null, mContainer);
    mContainer.addView(view);
    if (mChildHeight == 0 && mChildWidth == 0) {
      /*int w = View.MeasureSpec.makeMeasureSpec(0,
          View.MeasureSpec.UNSPECIFIED);
      int h = View.MeasureSpec.makeMeasureSpec(0,
          View.MeasureSpec.UNSPECIFIED);*/
      /**
       * 上面注释掉的是一位老师的写法,但我查了好多资料,用参数0和View.MeasureSpec.UNSPECIFIED是一种不太优美的做法;
       * 好的做法应该是
       * 当View为match_parent时,无法测量出View的大小(任玉刚大神讲的,确实是这么一回事,这个具体的原因要结合源码分析,可以看一下任大神的博客)
       * 当View宽高为具体的数值时,比如100px:
       * int w =View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
       * int h =View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
       * view.measure(w, h);
       * 当View宽高为wrap_content时:
       * int w =View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
       * int h =View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
       * view.measure(w, h);
       *
       * 我的此View高度为固定的150dip,宽度为wrap_content
       */
      int heightPx = DisplayUtil.dip2px(getContext(), 150);
      int w =View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
      int h =View.MeasureSpec.makeMeasureSpec(heightPx, View.MeasureSpec.EXACTLY);
      view.measure(w, h);
      mChildHeight = view.getMeasuredHeight();
      mChildWidth = view.getMeasuredWidth();
      // 计算每次加载多少个item
      mdisplayLastIndex = mScreenWidth / mChildWidth;
      mCountOneScreen = mdisplayLastIndex + 1;
      initFirstScreenChildren(mdisplayLastIndex + 1);
  
    }
  }
  
  /**
   * 加载第一屏的元素
   *
   * @param mDisplayCountOneScreen
   */
  private void initFirstScreenChildren(int mDisplayCountOneScreen) {
    mContainer = (LinearLayout) getChildAt(0);
    mContainer.removeAllViews();
    mViewPos.clear();
    for (int i = 0; i < mDisplayCountOneScreen; i++) {
      View view = mAdapter.getView(i, null, mContainer);
      // 待完善的点击事件
      view.setOnClickListener(this);
      mContainer.addView(view);
      mViewPos.put(view, i);
      mAllLastIndex = i;
    }
  
    // 初始化并刷新界面
    if (null != mOnCurrentImageChangeListener) {
      notifyCurrentImgChanged();
    }
  }
  
  private void notifyCurrentImgChanged() {
    // 先清除所有的背景颜色,点击时设置为蓝色
    for (int i = 0; i < mContainer.getChildCount(); i++) {
      mContainer.getChildAt(i).setBackgroundColor(Color.WHITE);
    }
    mOnCurrentImageChangeListener.onCurrentImgChanged(mAllFirstIndex,
        mContainer.getChildAt(0));
  }
  
  @Override
  public boolean onTouchEvent(MotionEvent ev) {
    /*
     * Log.e("X", getX()+""); Log.e("ChildX",
     * mContainer.getChildAt(0).getX()+""); Log.e("RawX",getLeft() +"");
     */
    switch (ev.getAction()) {
  
    case MotionEvent.ACTION_MOVE:
      int scrollX = getScrollX();
      Log.e("ScrollX", scrollX + "");
      if (scrollX >= mChildWidth) {
        // 加载下一页,移除第一张
        loadNextImg();
      }
      if (scrollX == 0) {
        // 加载上一页,移除最后一张
        loadPreImg();
      }
      break;
    }
  
    return super.onTouchEvent(ev);
  }
  
  private void loadNextImg() {// 数组边界值计算
    if (mAllLastIndex == mAdapter.getCount() - 1) {
      return;
    }
    // 移除第一张图片,且将水平滚动位置置0
    scrollTo(0, 0);
    mViewPos.remove(mContainer.getChildAt(0));
    mContainer.removeViewAt(0);
  
    // 获取下一张图片,并且设置onclick事件,且加入容器中
    View view = mAdapter.getView(++mAllLastIndex, null, mContainer);
    view.setOnClickListener(this);
    mContainer.addView(view);
    mViewPos.put(view, mAllLastIndex);
  
    // 当前第一张图片小标
    mAllFirstIndex++;
    // 如果设置了滚动监听则触发
    if (mOnCurrentImageChangeListener != null) {
      notifyCurrentImgChanged();
    }
  
  }
  
  private void loadPreImg() {
    if (mAllFirstIndex == 0) {
      return;
    }
    int index = mAllLastIndex - mCountOneScreen;
    if (index >= 0) {
      // 移除最后一张
      int oldViewPos = mContainer.getChildCount() - 1;
      mViewPos.remove(mContainer.getChildAt(oldViewPos));
      mContainer.removeViewAt(oldViewPos);
      // 将加入的View放在第一个位置
      View view = mAdapter.getView(index, null, mContainer);
      mViewPos.put(view, index);
      mContainer.addView(view, 0);
      view.setOnClickListener(this);
      // 水平滚动位置向左移动View的宽度的像素
      scrollTo(mChildWidth, 0);
  
      mAllLastIndex--;
      mAllFirstIndex--;
  
      if (null != mOnCurrentImageChangeListener) {
        notifyCurrentImgChanged();
      }
    }
  }
  
  @Override
  public void onClick(View v) {
    if(null!=mOnClickImageChangeListener){
      mOnClickImageChangeListener.onClickImageChangeListener(mViewPos.get(v), v);
    }
  }
}

下面是Adapter的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.ssa.horizontalscrollview.myview;
  
import java.util.List;
  
import com.ssa.horizontalscrollview.R;
  
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
  
public class GalleryHorizontalScrollViewAdapter {
  private LayoutInflater mInflater;
  private List<Integer> mDatas;
  
  public GalleryHorizontalScrollViewAdapter(Context context, List<Integer> mDatas) {
    mInflater = LayoutInflater.from(context);
    this.mDatas = mDatas;
  }
  
  public Object getItem(int position) {
    return mDatas.get(position);
  }
  
  public long getItemId(int position) {
    return position;
  }
  
  public int getCount() {
    return mDatas.size();
  }
    
  public View getView(int position, View contentView, ViewGroup parent) {
    ViewHolder myHolder = null;
    if (null == contentView) {
      contentView = mInflater.inflate(R.layout.activity_gallery_item,
          parent, false);
      myHolder = new ViewHolder(contentView);
      contentView.setTag(myHolder);
    }else {
      myHolder = (ViewHolder)contentView.getTag();
    }
    myHolder.ivImg.setImageResource(mDatas.get(position));
    myHolder.tvText.setText("Img_"+position);
      
      
    return contentView;
  }
  
  private static class ViewHolder {
    ImageView ivImg;
    TextView tvText;
  
    public ViewHolder(View view) {
      ivImg = (ImageView)view.findViewById(R.id.iv_content);
      tvText =(TextView)view.findViewById(R.id.tv_index);
    }
  }
  
}

下面是MainActivity的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.ssa.horizontalscrollview;
  
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
  
import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
  
import com.ssa.horizontalscrollview.myview.GalleryHorizontalScrollView;
import com.ssa.horizontalscrollview.myview.GalleryHorizontalScrollView.OnClickImageChangeListener;
import com.ssa.horizontalscrollview.myview.GalleryHorizontalScrollView.OnCurrentImageChangeListener;
import com.ssa.horizontalscrollview.myview.GalleryHorizontalScrollViewAdapter;
  
public class MainActivity extends Activity {
  private GalleryHorizontalScrollView mHorizontalScrollView;
  private GalleryHorizontalScrollViewAdapter mAdapter;
  private ImageView mImg;
  private List<Integer> mDatas = new ArrayList<Integer>(Arrays.asList(
      R.drawable.a, R.drawable.b, R.drawable.c, R.drawable.d,
      R.drawable.e,R.drawable.f,R.drawable.g));
    
    
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mImg = (ImageView)findViewById(R.id.iv_content);
    mHorizontalScrollView = (GalleryHorizontalScrollView)findViewById(R.id.mhsv_gallery_container);
    mAdapter = new GalleryHorizontalScrollViewAdapter(this, mDatas);
    mHorizontalScrollView.setmOnCurrentImageChangeListener(new OnCurrentImageChangeListener() {
        
      @Override
      public void onCurrentImgChanged(int position, View view) {
        mImg.setImageResource(mDatas.get(position));
        view.setBackgroundColor(Color.parseColor("#6d9eeb"));
      }
    });
    mHorizontalScrollView.setmOnClickImageListener(new OnClickImageChangeListener() {
        
      @Override
      public void onClickImageChangeListener(int position, View view) {
        mImg.setImageResource(mDatas.get(position));
      }
    });
    mHorizontalScrollView.initData(mAdapter);
  }
}

至些,调试运行,读者会发现,整个相册会非常卡,

甚至有的图片还没有显示出来如img_4,看一下logcat,相信大家会发现原因:

信息已经提示的很清楚了,图片太大,

此时大家应该明白了,笔者故意选择了几张很大的图片加载,虽然没大到直接让应用崩掉,但是体验性已经变得非常差了,这是因为课堂上的老师讲课时用的图片都是几十K的小图片,加载当然不会有问题,所以要想使这个相册作为一个实用的相册,还要处理图片过大的问题,不然,依旧会造成OOM。

此时就用到这个工具类了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.ssa.horizontalscrollview.myUtils;
  
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
  
public class BitmapUtil {
  public static Bitmap decodeSampledBitmapFromResources(Resources res,
      int resId, int reqWidth, int reqHeight) {
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    options.inSampleSize = calculateInsampleSize(options, reqWidth,
        reqHeight);
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
  
  }
  
  public static int calculateInsampleSize(BitmapFactory.Options options,
      int reqWidth, int reqHeight) {
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
      final int halfHeight = height / 2;
      final int halfWidth = width / 2;
      while ((halfHeight / inSampleSize) >= reqHeight
          && (halfWidth / inSampleSize) >= reqWidth) {
        inSampleSize *= 2;
      }
    }
  
    return inSampleSize;
  }
}

添加了这个工具类,上面几个类的代码也要略微修改一下,具体怎么改,大家可以下载下面我上传的源码:
至于效果如下动图所示(生成的gif图有点卡,大家可以运行看效果):

源码下载:HorizontalScrollView仿ViewPager设计相册

以上就是本文的全部内容,希望对大家学习Android软件编程有所帮助。


代码注释

作者:喵哥笔记

IDC笔记

学的不仅是技术,更是梦想!