HorizontalScrollView实现Gallery效果,可滑到最后项且点击可滑动到相应位置

需求

实际开发中,往往需要用到图集展示(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的实现机制,之后再整理一套异步加载图片的机制出来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值