Android 自定义 HorizontalScrollView 实现ViewPager效果,打造再多图片(控件)也不怕 OOM

1. 效果


这里写图片描述

上次实现了这个IOS Reveal效果,能够确保中间部分是彩色其余部分是灰色,但是也遗留了两个问题:
  (1)如果把它做成ViewPager效果的方式可能会更好;
  (2)HorizontalScrollView没有重用机制,虽然可能我们并不会加载很多的图片,但是一旦过多肯定会出现OOM问题;

2.分析和实现


2.1实现ViewPager效果:

  1.记录当前位置,跟踪触摸的实际事件,重载onTouch事件,获取滑动的速率,做出相应的处理:

public RevealScrollView extends HorizontalScrollView {

    // 当前的位置
    private int mCureentItem = 0;
    // 跟踪触摸实际事件,用来获取速率
    private VelocityTracker mVelocityTracker;
    private int mMaximumVelocity;

    /**
     * 初始化
     */
    private void init() {
        mVelocityTracker = VelocityTracker.obtain();
        mMaximumVelocity = ViewConfiguration.get(getContext())
                .getScaledMaximumFlingVelocity();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        final int action = event.getAction();
        switch (action) {
        case MotionEvent.ACTION_DOWN:
            mFingerDownX = (int) event.getRawX();
            mFingerDownY = (int) event.getRawY();
            mVelocityTracker.clear();
            mVelocityTracker.addMovement(event);
            return true;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
            int velocityX = (int) mVelocityTracker.getXVelocity();
            mVelocityTracker.clear();
            int fingerUpX = (int) event.getRawX();
            int fingerDx = mFingerDownX - fingerUpX;
            int fingerUpY = (int) event.getRawY();
            int yDiff = Math.abs(fingerUpY - mFingerDownY);
            int xDiff = Math.abs(fingerDx);

            if (Math.abs(velocityX) > 400 && (xDiff > yDiff)) {
                // 认为是快速的滑动
                flyingScollItem(velocityX);
            } else {
                // 滑动
                if (fingerDx == 0) {
                    // 不对其进行处理,直接break
                    break;
                }
                scrollItem(fingerDx);
            }
            return true;
        }
        return super.onTouchEvent(event);
    }

  2.获取滑动的速率,当大于一定值认为是快速滑动否则是普通滑动,实现这几个方法:

    /**
     * 普通滚动
     * 
     * @param fingerDx
     *            手指在X轴上的移动距离
     */
    private void scrollItem(int fingerDx) {

        int scrollX = getScrollX();
        int cureentItem = scrollX / mIconWidth;

        int left = scrollX % mIconWidth;

        // 无论向哪一边滑动只要超过1/3就切换
        if (fingerDx > 0) {
            // 向右边滑动
            if (left > mIconWidth / 3) {
                cureentItem += 1;
            }
        } else {
            // 向左边滑动
            if (left > mIconWidth * 2 / 3) {
                cureentItem += 1;
            }
        }

        scrollToItem(cureentItem);
    }

    /**
     * 快速滚动时的页面切换
     */
    private void flyingScollItem(int velocityX) {
        if (velocityX < 0) {
            // 滚动到下一张
            mCureentItem += 1;
            scrollToItem(mCureentItem);
        } else {
            // 滚动到上一张
            mCureentItem -= 1;
            scrollToItem(mCureentItem);
        }
    }

    /**
     * 滚动到当前条目
     */
    private void scrollToItem(int cureentItem) {
        mCureentItem = cureentItem;
        smoothScrollTo(mCureentItem * mIconWidth, 0);
    }

  3.测试一把,看看效果。下面重头戏来了,HorizontalScrollView内部是不会自动销毁和重用子条目的,如果添加的内容过多肯定会造成OOM:
  

2.1HorizontalScrollView的OOM问题:

1.ListView的Adapter我们已经用得不能再熟了,就把的源码拿出来用用,首先新建BaseAdapter,然后用到实现中。

public abstract class BaseAdapter {
    // 获取条目数
    public abstract int getCount();
    // 获取条目Id
    public abstract int getItemId(int position);
    // 获取条目
    public abstract Object getItem(int position);
    // 获取View
    public abstract View getView(int position, View convertView, ViewGroup parent);
}

public class RevealAdapter extends BaseAdapter {

    private int[] mColouredResouce;

    public RevealAdapter(int[] colouredResouce) {
        this.mColouredResouce = colouredResouce;
    }

    @Override
    public int getCount() {
        return mColouredResouce.length;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ImageView image;
        if (convertView == null) {
            image = new ImageView(parent.getContext());
        } else {
            image = (ImageView) convertView;
        }
        Drawable grayDrawable = parent.getContext().getResources()
                .getDrawable(mColouredResouce[position]);
        Drawable drawable = parent.getContext().getResources()
                .getDrawable(mColouredResouce[position]);
        RevealDrawable revealDrawable = new RevealDrawable(
                BitmapUtil.toGrayscale(grayDrawable), drawable);
        image.setImageDrawable(revealDrawable);
        return image;
    }

    @Override
    public int getItemId(int position) {
        return mColouredResouce[position];
    }

    @Override
    public Object getItem(int position) {
        return mColouredResouce[position];
    }

}

2.给HorizontalScrollView设置我们自定义的Adapter,在设置adapter之后默认load第一屏数据

// 数据适配源
private BaseAdapter mAdapter;
// ScrollView的宽度
private int mMeasuredWidth;
// 一屏幕显示多少个
private int mScreenCount;
// 第一个位置
private int mFirstPosition;
// 最后一个位置
private int mLastPosition;
// 位置记录
private HashMap<View, Integer> mRecordPos;

/**
 * 设置数据适配
*/
public void setAdapter(BaseAdapter adapter) {
    if (adapter == null) {
        throw new RuntimeException("adapter is null...");
    }

    this.mAdapter = null;
    this.mAdapter = adapter;

    initAdapterData();
}

/**
* 初始化数据适配
*/
private void initAdapterData() {
    mContainer.removeAllViews();
    mRecordPos.clear();

    // 获取第一个Item
    final ImageView firstView = (ImageView) mAdapter.getView(0, null,
                mContainer);
    // 第一张图片
    firstView.setImageLevel(5000);
    mContainer.addView(firstView);
    mRecordPos.put(firstView, 0);
    mFirstPosition = 0;

    post(new Runnable() {
        @Override
        public void run() {
            // 得到item的宽度
            mIconWidth = firstView.getMeasuredWidth();
            loadFirstScreenItem();
        }
    });
}

/**
* 加载第一屏的条目
*/
private void loadFirstScreenItem() {
    mMeasuredWidth = getMeasuredWidth();
    mCenterX = getWidth() / 2 - mIconWidth / 2;
    mContainer.setPadding(mCenterX, 0, mCenterX, 0);

    // 默认按一屏幕计算
    mScreenCount = mMeasuredWidth / mIconWidth + 2;

    // i从1开始,之前已经加入了一个
    for (int i = 1; i < mScreenCount; i++) {
        View view = mAdapter.getView(i, null, mContainer);
        mContainer.addView(view);
        mRecordPos.put(view, i);
    }
    mLastPosition = mScreenCount - 1;
}

3.接下来只要弄清楚什么时候需要加载下一个以及移除上一个就很好办事了,其实就是我们向右滑动一定距离之后我们要把第一个移除并且加载后一个,让第一个item作为复用的item传出去,反之向左移动一段距离就是移除最后一个,加载第一个:

@Override
public boolean onTouchEvent(MotionEvent event) {
    final int action = event.getAction();
    switch (action) {
    case MotionEvent.ACTION_MOVE:
        int scrollX = getScrollX();

        // 如果当前scrollX为view的宽度,加载下一张,移除第一张
        if (scrollX > mIconWidth + mCenterX) {
            loadNextImg();
        }

        // 如果当前scrollX = 0, 往前设置一张,移除最后一张
        if (scrollX <= mCenterX) {
            loadPreImg();
        }

        break;
    }
    return super.onTouchEvent(event);
}


/**
 * 加载下一张图片
 */
private void loadNextImg() {
    if (mLastPosition >= mAdapter.getCount() - 1) {
        return;
    }

    // 移除第一张
    View firstChild = mContainer.getChildAt(0);
    mContainer.removeView(firstChild);
    mFirstPosition += 1;
    mRecordPos.remove(firstChild);

    // 加载最后一张
    mLastPosition += 1;
    View lastChild = mAdapter.getView(mLastPosition, firstChild, mContainer);
    mContainer.addView(lastChild);
    mRecordPos.put(lastChild, mLastPosition);

    // 滚动到当前位置,设置当前位置为1
    scrollTo(getScrollX() - mIconWidth, 0);

    mCureentItem = getScrollX() / mIconWidth;
}

/**
* 加载前一张
*/
private void loadPreImg() {
    if (mFirstPosition == 0) {
        return;
    }

    // 移除掉最后一张
    View lastChild = mContainer.getChildAt(mContainer.getChildCount() - 1);
    mContainer.removeView(lastChild);
    mRecordPos.remove(lastChild);
    mLastPosition -= 1;

    // 加载第一张
    mFirstPosition -= 1;
    View firstView = mAdapter.getView(mFirstPosition, lastChild, mContainer);
    mContainer.addView(firstView, 0);
    mRecordPos.put(firstView, mFirstPosition);

    // 水平滚动位置向左移动view的宽度个像素
    scrollTo(getScrollX() + mIconWidth, 0);
    mCureentItem = getScrollX() / mIconWidth;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值