如何实现一个循环显示超长图片的控件

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

某次被问到如何实现一个滚筒状的控件,就是可以将一张很长的图片沿着Y轴无限旋转,如下图所示:
如何实现一个循环显示超长图片的控件

大概就是这个意思,当时还不知道图片可以裁剪,想不出整个流程怎么搞,后来得知Bitmap有裁剪功能,才想到这个功能怎么实现,花了一下午时间整了一下有了成果。
这是这张长图:

如何实现一个循环显示超长图片的控件

然后旋转起来就是这个样子:

如何实现一个循环显示超长图片的控件

上面这个效果在实际运行过程中是非常流畅的,这张图片是按照每秒几帧截的,所以看起来一顿一顿的。

先来说说如何实现:

第一次:先按照屏幕的宽度截取这张长图的起始部分。
第二次:以偏移量开始,重复第一次的行为。

最后:当这张图片的结尾部分不足以支撑整个屏幕的宽度时,先截取这张图片的末尾部分,绘制。然后再以剩余的宽度截取图片的头部部分,绘制。依次进行,直至重新回到第一次。

/**
 * Created by shangbin on 2016/6/16.
 * Email: sahadev@foxmail.com
 */
public class CylinderImageView extends View {
    //用于裁剪的原始图片资源
    private Bitmap mSourceBitmap = null;

    // 图片的高宽
    private int mBitmapHeight, mBitmapWidth;

    // 移动单位,每次移动多少个单位
    private final int mMoveUnit = 1;

    // 图片整体移动的偏移量
    private int xOffset = 0;

    private Bitmap mPointerA, mPointerB;// 用于持有两张拼接图片的引用,并释放原先的图片资源

    /**
     * 循环滚动标志位
     */
    private boolean mRunningFlag;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 0) {
                invalidate();
            }
        }
    };

    public CylinderImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initVideoView();
    }

    public CylinderImageView(Context context) {
        super(context);
        initVideoView();
    }

    public CylinderImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initVideoView();
    }

    private void initVideoView() {
        // 获取需要循环展示的图片的高宽
        mSourceBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.android_m_hero_1200);
        mBitmapHeight = mSourceBitmap.getHeight();
        mBitmapWidth = mSourceBitmap.getWidth();

        mRunningFlag = true;

        setFocusableInTouchMode(true);
        requestFocus();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 简单设置一下控件的宽高,这里的高度以图片的高度为准
        setMeasuredDimension(widthMeasureSpec, MeasureSpec.makeMeasureSpec(mBitmapHeight, MeasureSpec.getMode(heightMeasureSpec)));
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        recycleTmpBitmap();

        final int left = getLeft();
        final int top = getTop();
        final int right = getRight();
        final int bottom = getBottom();

        // 计算图片的高度
        int height = bottom - top;
        // 第一张图的宽带
        int tempWidth = right - left;

        // 如果一张图片轮播完,则从头开始
        if (xOffset >= mBitmapWidth) {
            xOffset = 0;
        }

        // 重新计算截取的图的宽度
        tempWidth = xOffset + tempWidth >= mBitmapWidth ? mBitmapWidth - xOffset : tempWidth;

        mPointerA = Bitmap
                .createBitmap(mSourceBitmap, xOffset, 0, tempWidth, height > mBitmapHeight ? mBitmapHeight : height);

        Paint bitmapPaint = new Paint();

        // 绘制这张图
        canvas.drawBitmap(mPointerA, getMatrix(), bitmapPaint);

        // 如果最后的图片已经不足以填充整个屏幕,则截取图片的头部以连接上尾部,形成一个闭环
        if (tempWidth < right - left) {
            Rect dst = new Rect(tempWidth, 0, right, mBitmapHeight);
            mPointerB = Bitmap.createBitmap(mSourceBitmap, 0, 0, right - left - tempWidth,
                    height > mBitmapHeight ? mBitmapHeight : height);
            // 将另一张图片绘制在这张图片的后半部分
            canvas.drawBitmap(mPointerB, null, dst, bitmapPaint);
        }

        // 累计图片的偏移量
        xOffset += mMoveUnit;

        //由handler的延迟发送产生绘制间隔
        if (mRunningFlag) {
            mHandler.sendEmptyMessageDelayed(0, 1);
        }
    }

    /**
     * 回收临时图像
     */
    private void recycleTmpBitmap() {
        if (mPointerA != null) {
            mPointerA.recycle();
            mPointerA = null;
        }

        if (mPointerB != null) {
            mPointerB.recycle();
            mPointerB = null;
        }
    }

    /**
     * 恢复
     */
    public void resume() {
        mRunningFlag = true;
        invalidate();
    }

    /**
     * 暂停
     */
    public void pause() {
        mRunningFlag = false;
    }

    /**
     * 回收清理工作
     */
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        pause();
        recycleTmpBitmap();
        mSourceBitmap.recycle();
    }
}

以上是CylinderImageView的实现代码。

其中有两个公开方法:
resume() 用于在Activity的onResume()中调用,以便恢复旋转。
pause() 用于在Activiyt的onPause()中调用,以便暂停旋转。

下面是使用示例:

public class MainActivity extends AppCompatActivity {
    private CylinderImageView cylinderImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        cylinderImageView = (CylinderImageView) findViewById(R.id.cylinderImageView);
    }

    @Override
    protected void onResume() {
        super.onResume();

        cylinderImageView.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();

        cylinderImageView.pause();
    }
}

因为这个控件内部涉及大量的图片操作,所以大伙一定很关心内存的使用。我为此专门做了内存测试,结果内存占用非常小:

这张图是没有使用CylinderImageView时应用程序所占用的内存:17.9MB:
如何实现一个循环显示超长图片的控件

我这里所使用的示例图片的长宽是1200x353,也就是说它被加载到内存中所占用的内存大小是1200x353x4/1024/1024=1.61MB.

再加上在屏幕上所显示的Bitmap所占用的内存为:1080x353x4/1024/1024=1.45MB.(这里的1080是我的屏幕宽度,在屏幕上显示的图片占了整个屏幕的宽度,所以是1080)

因为内存回收并不是实时的,所以在内存使用最高峰时,所使用的内存=17.9+1.61+1.45x2=22.43.

实际的运行占用内存为:

如何实现一个循环显示超长图片的控件

如何实现一个循环显示超长图片的控件

上面两张图片的差距是图片内存回收的差值,但是这里的高峰内存值与我们计算的内存值有些差距,这是因为除了内存之外,我们还在XML布局文件中声明了控件以及加载控件也占用了一定的内存空间。

调用pause()方法的内存状况:

如何实现一个循环显示超长图片的控件

调用resume()方法的内存状况:

如何实现一个循环显示超长图片的控件

如何实现一个循环显示超长图片的控件

Activity销毁之后所占用的内存:

如何实现一个循环显示超长图片的控件

通过上面一系列图示说明这个控件将内存的消耗控制在了合理的范围之内,没有滥用内存。

最后,大功告成,不知道是否明白我说的呢?

相关Demo演示请参见:https://github.com/sahadev/CylinderImageView

上一篇:使用DataX同步MaxCompute数据到TableStore(原OTS)优化指南


下一篇:Vue入门---常用指令详解