需求
实际开发中,往往需要用到图集展示(Gallery)效果。做过android开发的都知道android有个控件叫Gallery,就是专门用来实现图集展示效果的。使用方法也很简单,一个item布局+一个adapter就可以搞定。
然而Google官方由于某些原因决定弃用Gallery控件(具体原因不明,似乎是因为使用Gallery容易造成内存泄露),官方文档中提倡大家使用ViewPager和HorizontalScrollView代替它。在android 5.0后 Google又推出了一个新控件RecyclerView,也可以用来实现Gallery效果,这里不做讨论。
本文主要讲解如何使用HorizontalScrollView实现完美的Gallery效果。
参考鸿神的博客Android 自定义 HorizontalScrollView 打造再多图片(控件)也不怕 OOM 的横向滑动效果
在上面这篇博客中,主要实现了以下几个功能:
1.自定义HorizontalScrollView实现动态加载、删除图片,避免出现OOM
2.当某一图片滑动到屏幕左边时,将该图片置为当前选中项,并通过回调接口将该图片位置信息传递给界面。
3.实现图片的点击监听,点击图片时通过回调接口将该图片信息传递给界面。
本文主要是在上面的HorizontalScrollView的基础上做一些功能优化和补充,主要有以下两个功能:
1、当点击某一子项时,将HorizontalScrollView自动滑动到该子项位置
2、解决HorizontalScrollView无法滑动到最后一个子项的问题
效果演示图
实现&代码
先构建一个Adapter,方便HorizontalScrollView获取子项图片的数据
定制Adapter
public class HorizontalScrollViewAdapter extends BaseAdapter{
private LayoutInflater mInflater;
private List<Integer> mData;
public HorizontalScrollViewAdapter(Context context, List<Integer> mData)
{
mInflater = LayoutInflater.from(context);
this.mData = mData;
}
public int getCount()
{
return mData.size();
}
public Object getItem(int position)
{
return mData.get(position);
}
public long getItemId(int position)
{
return position;
}
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder viewHolder = null;
if (convertView == null)
{
viewHolder = new ViewHolder();
convertView = mInflater.inflate(
R.layout.item_gallery, parent, false);
viewHolder.mImg = (ImageView) convertView
.findViewById(R.id.item_gallery_iv);
convertView.setTag(viewHolder);
} else
{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.mImg.setImageResource(mData.get(position));
return convertView;
}
private class ViewHolder
{
ImageView mImg;
}
}
这里每个子项都只包含了一个ImageView,所以布局文件也很简单。
使用资源文件dimen定义图片的宽高,除了方便修改之外,也方便程序中可以动态获得图片的宽高。
Gallery_item布局
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/item_gallery_iv"
android:layout_width="@dimen/gallery_width"
android:layout_height="@dimen/gallery_height">
</ImageView>
把最复杂的放到最后,先来看看用法。
注:这里为了演示效果所以使两个HorizontalScrollView分别实现了滑动监听和点击监听,实际应用中可以同时监听两个接口。
用法
public class MainActivity extends ActionBarActivity {
//实现滑动监听的HorizontalScrollView
private MyHorizontalScrollView mSlideGallery;
//实现点击监听的HorizontalScrollView
private MyHorizontalScrollView mClickGallery;
//用于展示当前选中的图片
private ImageView mImg;
//图片资源文件数组
private List<Integer> mData = 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, R.drawable.h,
R.drawable.i, R.drawable.j, R.drawable.k));
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImg = (ImageView) findViewById(R.id.id_content);
mSlideGallery = (MyHorizontalScrollView) findViewById(R.id.slide_gallery);
mClickGallery = (MyHorizontalScrollView) findViewById(R.id.click_gallery);
//添加滚动回调
mSlideGallery.setCurrentImageChangedListener(
new MyHorizontalScrollView.CurrentImageChangedListener() {
@Override
public void onCurrentImgChanged(int position, View viewIndicator) {
mImg.setImageResource(mData.get(position));
viewIndicator.setAlpha(1f);
}
});
//初始化,配置adapter
mSlideGallery.initData(new HorizontalScrollViewAdapter(this, mData));
//添加点击回调
mClickGallery.setOnItemClickListener(
new MyHorizontalScrollView.OnItemClickListener() {
@Override
public void onItemClick(View view, int position) {
mImg.setImageResource(mData.get(position));
}
});
//初始化,配置adapter
mClickGallery.initData(new HorizontalScrollViewAdapter(this, mData));
}
}
再来看看主界面布局
主界面布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@android:color/white">
<FrameLayout
android:layout_width="fill_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
android:id="@+id/id_content"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center"
android:layout_margin="10dp"
android:scaleType="centerCrop"
android:src="@drawable/ic_launcher"/>
</FrameLayout>
<com.yetwish.horizatalscrollviewdemo.widget.MyHorizontalScrollView
android:id="@+id/slide_gallery"
android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:color/white"
android:scrollbars="none">
<LinearLayout
android:layout_width="@dimen/gallery_width"
android:layout_height="@dimen/gallery_height"
android:layout_gravity="center_vertical"
android:orientation="horizontal">
</LinearLayout>
</com.yetwish.horizatalscrollviewdemo.widget.MyHorizontalScrollView>
<com.yetwish.horizatalscrollviewdemo.widget.MyHorizontalScrollView
android:layout_marginTop="10dp"
android:id="@+id/click_gallery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:color/white"
android:scrollbars="none">
<LinearLayout
android:layout_width="@dimen/gallery_width"
android:layout_height="@dimen/gallery_height"
android:layout_gravity="center_vertical"
android:orientation="horizontal">
</LinearLayout>
</com.yetwish.horizatalscrollviewdemo.widget.MyHorizontalScrollView>
</LinearLayout>
最后是实现自定义HorizontalScrollView,先来看需要用到的成员变量。
自定义HorizontalScrollView
public class MyHorizontalScrollView extends HorizontalScrollView implements
View.OnClickListener {
/**
* 屏幕宽度
*/
private int mScreenWidth;
/**
* 图片的宽度
*/
private int mChildWidth;
/**
* horizontalScrollView 下的 linearLayout
*/
private LinearLayout mContainer;
/**
* 每屏最多显示的View的个数
*/
private int mCountOneScreen;
/**
* adapter
*/
private HorizontalScrollViewAdapter mAdapter;
/**
* 保存View与位置的键值对
*/
private Map<View, Integer> mViewPos = new HashMap<>();
/**
* 当前屏幕显示的最后一张图片的下标
*/
private int mLastIndex;
/**
* 当前第一张图片的下标
*/
private int mFirstIndex;
/**
* 当前图片切换 回调接口
*/
private CurrentImageChangedListener mItemChangedListener;
/**
* 点击图片 回调接口
*/
private OnItemClickListener mItemClickListener;
/**
* 可额外添加的空白图片的个数
*/
private int mAdditionalCount;
/**
* 标识是否已加载完所有空白图片
*/
private boolean isLoaded = false;
/**
* 当前点击选中的图片
*/
private int mCurrentClickedItem;
其中
/**
* 可额外添加的空白图片的个数
*/
private int mAdditionalCount;
/**
* 标识是否已加载完所有空白图片
*/
private boolean isLoaded = false;
这两个成员变量就是为了解决HorizontalScrollView无法滑动到最后一张图片的问题。
由于我们在主布局中,将HorizontalScrollView的宽度置为”wrap_content”,所以当滑动到最后一屏图片时,即表示HorizontalScrollView已经滑到底,无法再向左滑动,则最后一屏的图片无法滑到屏幕左边。
这时候有一个比较简单的解决方法就是当滑动到最后一屏时,动态地给HorizontalScrollView添加相应个数(对应最后一屏图片的个数)具备相同宽度的空白View,这样就可以将HorizontalScrollView滑动到最后一张图片。当然,如果这时候从底部往前滑动,就需要动态地将空白view清除掉。
再看看MyHorizontalScrollView的构造方法和初始化方法
public MyHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
// 获取屏幕宽度
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
mScreenWidth = outMetrics.widthPixels;
}
/**
* 初始化数据和adapter
*/
public void initData(HorizontalScrollViewAdapter adapter) {
mAdapter = adapter;
mContainer = (LinearLayout) getChildAt(0);
// 获取适配器的第一个view
View view = mAdapter.getView(0, null, mContainer);
mContainer.addView(view);
// 计算当前view的宽高
if (mChildWidth == 0) {
mChildWidth = (int) getResources().getDimension(
R.dimen.gallery_width) + 1;
// 计算每次加载多少个view
mCountOneScreen = mScreenWidth / mChildWidth + 2;
mAdditionalCount = mCountOneScreen-1;
// 如果adapter中view 的总数比能一屏能加载的少,则把最多能加载数置为view总数
if (mCountOneScreen > mAdapter.getCount())
mCountOneScreen = mAdapter.getCount();
}
// 初始化第一屏幕
initFirstScreenChild(mCountOneScreen);
}
这里我们通过DisplayMetrics获取屏幕的宽度,再由屏幕宽度/图片的宽度获得每屏最多可显示的图片个数
因为当有mScreenWidth / mChildWidth个图片滑动到屏幕中间部分时,屏幕两边也会显示到另外两张图片的边缘
所以这里需要mCountOneScreen = mScreenWidth / mChildWidth + 2;
另外,我们通过mChildWidth = (int) getResources().getDimension(
来获取图片的宽度
R.dimen.gallery_width);
/**
* 加载第一屏的图片
*/
public void initFirstScreenChild(int mCountOneScreen) {
mContainer = (LinearLayout) getChildAt(0);
mContainer.removeAllViews();
mViewPos.clear();
for (int i = 0; i < mCountOneScreen; i++) {
View view = mAdapter.getView(i, null, mContainer);
view.setOnClickListener(this);
//初始化时 默认选中第一个
if (i == 0) view.setAlpha(1f);
else view.setAlpha(0.5f);
mContainer.addView(view);
mViewPos.put(view, i);
mLastIndex = i;
}
if (mItemChangedListener != null) {
notifyCurrentItemChanged();
}
}
回调接口定义
//滑动切换回调接口
public interface CurrentImageChangedListener {
void onCurrentImgChanged(int position, View viewIndicator);
}
//点击切换回调接口
public interface OnItemClickListener {
void onItemClick(View view, int position);
}
//设置滑动切换接口
public void setCurrentImageChangedListener(
CurrentImageChangedListener listener) {
mItemChangedListener = listener;
}
//设置点击切换接口
public void setOnItemClickListener(OnItemClickListener listener) {
mItemClickListener = listener;
}
接下来就是核心部分,通过实现onTouchEvent()实现动态加载上一张图片和加载下一张图片:
@Override
public boolean onTouchEvent(@NonNull MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
int scrollX = getScrollX();
// 如果当前scrollX 为图片的宽度,加载下一张,
if (scrollX >= mChildWidth) {
loadNextImage();
}
// 如果scrollX = 0 ,加载上一张
if (scrollX == 0) {
loadPreImage();
}
break;
}
return super.onTouchEvent(ev);
}
加载下一张图片的同时移除第一张,避免出现OOM
加载到最后一屏之前,每次根据adapter中存放的数据进行加载下一张图片,并更新mFirstIndex和mCurrentIndex
当加载到最后一屏时,即当mFirstInde==mAdapter.getCount() - mCountOneScreen时,判断mAdditionalCount 是否大于0,是则还未滑动到底,则加载空白image并加入到HorizontalScrollView中;否则表示已滑到底,无需添加空白image,则直接return
private void loadNextImage() {
View view;
if (isLoaded) {
return;
}
// 后面没有
if (mFirstIndex >= mAdapter.getCount() - mCountOneScreen
&& mAdditionalCount > 0) {
view = new ImageView(getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(
(int) getContext().getResources().getDimension(R.dimen.gallery_width),
(int) getContext().getResources().getDimension(R.dimen.gallery_height)));
mAdditionalCount--;
if (mAdditionalCount == 0)
isLoaded = true;
} else {
// 获取下一张图片
view = mAdapter.getView(++mLastIndex, null, mContainer);
view.setOnClickListener(this);
}
// 移除第一张图片,并将水平滚动位置置0
scrollTo(0, 0);
mViewPos.remove(mContainer.getChildAt(0));
mContainer.removeViewAt(0);
mContainer.addView(view);
mViewPos.put(view, mLastIndex);
//如果生成的next view并不是当前选择的view,则将其透明度设为50%,否则设为1
if (mLastIndex != mCurrentClickedItem)
view.setAlpha(0.5f);
else view.setAlpha(1f);
// 当后面还有图片时,更新第一张图片的下标
if (mFirstIndex < mAdapter.getCount() - 1)
mFirstIndex++;
//通知item changed 回调
if (mItemChangedListener != null) {
notifyCurrentItemChanged();
}
}
加载上一张的同时,移除最后一张,避免出现OOM
当滑动到最后一屏时,因为在loadNextImage()中,添加的是空白的image,并没有更新mLastIndex,所以在loadPreImage()中,当处于最后一屏时,不更新mLastIndex
注:滑动过程中,由于当前点击的图片可能被滑到不可见区域而被remove掉,所以当加载新的图片时,需要判断其是否为当前点击的图片,如果是则将其置为选中状态
private void loadPreImage() {
// 前面没有了
if (mFirstIndex == 0) {
return;
}
// 如果当前可添加空白image的值小于初始值,则表示此时正滑到最后一屏,则更新mAdditionalCount,且将isLoaded置为true
else if (mAdditionalCount < mCountOneScreen-1) {
mAdditionalCount++;
isLoaded = false;
}
// 获取当前应该显示为第一张图片的下标
int index = mFirstIndex - 1;
if (index >= 0) {
// 移除最后一张
int oldViewPos = mContainer.getChildCount() - 1;
mViewPos.remove(mContainer.getChildAt(oldViewPos));
mContainer.removeViewAt(oldViewPos);
View view = mAdapter.getView(index, null, mContainer);
if (index != mCurrentClickedItem)
view.setAlpha(0.5f);
else view.setAlpha(1f);
view.setOnClickListener(this);
mContainer.addView(view, 0);
mViewPos.put(view, index);
// 水平滚动位置向左移动view的宽度个像素
scrollTo(mChildWidth, 0);
// 当滑动到最后一屏之前,每次load pre 则mCurrentPos --
if (mFirstIndex <= mAdapter.getCount() - mCountOneScreen)
mLastIndex--;
mFirstIndex--;
//通知item changed 回调
if (mItemChangedListener != null) {
notifyCurrentItemChanged();
}
}
}
实现接口回调
实现点击图片时,HorizontalScrollView自动滑动到该图片位置,主要是要计算点击时需要滑动的距离,调用smoothScrollBy(x,y),并更新当前点击项即可
//滑动切换接口回调
private void notifyCurrentItemChanged() {
for (int i = 0; i < mContainer.getChildCount(); i++) {
mContainer.getChildAt(i).setAlpha(0.5f);
}
mItemChangedListener.onCurrentImgChanged(mFirstIndex, mContainer.getChildAt(0));
}
//点击接口回调
@Override
public void onClick(View view) {
if (mItemClickListener != null) {
for (int i = 0; i < mContainer.getChildCount(); i++) {
mContainer.getChildAt(i).setAlpha(0.5f);
}
view.setAlpha(1f);
mCurrentClickedItem = mViewPos.get(view);
// 点击项与当前显示项之间 图片个数
int itemCount = mViewPos.get(view) - mFirstIndex;
// 点击时,由于每次只加载了显示在屏幕上的几个图片,所以应先根据itemCount加载出后面的图片,才能scroll,否则会无法滑动
for (int i = 0; i < itemCount; i++) {
loadNextImage();
}
smoothScrollBy(calculateScrollWidth(itemCount), 0);
mItemClickListener.onItemClick(view, mViewPos.get(view));
}
}
/**
* 计算点击时 将点击项滑动到屏幕最左边,需要滑动的距离
*
* @param itemCount , 前后两项 间隔的图片个数
* @return 需滑动的距离
*/
private int calculateScrollWidth(int itemCount) {
int scrollWidth;
if (itemCount > 2)
scrollWidth = mChildWidth * (itemCount - 3);
else if (itemCount > 1) {
scrollWidth = mChildWidth * (itemCount - 2);
} else {
scrollWidth = mChildWidth * (itemCount - 1);
}
return scrollWidth;
}
到此就实现了超赞的Gallery效果,大家可以试着写个demo,希望对个位看官有所帮助。有任何问题可评论或者发邮件跟我联系yetwish@gmail.com
在实际项目中,图集展示往往要涉及到图片压缩和图片的异步加载,博主这几天自己写了个ImageLoader,使用线程池管理并发,使用handler、looper实现异步,但感觉还不是很科学,所以这两天先研究研究volley的ImageLoader的实现机制,之后再整理一套异步加载图片的机制出来