自定义viewAndroid技术知识Android开发

Android自定义View实现图片放大,平移和显示大图片

2017-07-05  本文已影响2116人  summerlyy

先放效果:

bitmap_display.png

假设A是图片,B是手机屏幕,有一道光照在A上,然后A在手机上有个投影A'。假设我们只能看到手机屏幕上的内容,那么如果光源的位置发生改变,那么A'在屏幕上的位置和大小也可能会随之改变。
而矩阵在这里就充当这光源的作用。
矩阵提供这么几类方法

  1. tranlate: 移动
  2. scale:缩放
  3. rotate:旋转
  4. skew:倾斜

所以我们可以定义一个类变量 matrixDraw 用来绘制 bitmap

val matrixDraw = Matrix()

对象池

因为图片在变换的时候需要用到不少矩形和矩阵,为了防止大量创建这些对象产生内存抖动,所以使用对象池进行缓存,每次拿一个使用后就存回去

    abstract class ObjectPool<T>(capacity: Int = 16) {
        val objects: Queue<T>
        var size = capacity
        init {
            objects = LinkedList()
        }
        /**
         *  get an object from pool , must restore it after using
         */
        fun get(): T {
            if (objects.size == 0) {
                return generateAnObject()
            }
            return reset(objects.poll())
        }
        fun restore(vararg t: T) {
            for (e in t) {
                if (objects.size < size) {
                    objects.offer(e)
                }
            }
        }
        protected abstract fun reset(t: T): T
        protected abstract fun generateAnObject(): T

    }

实现滑动

使用 GestureDetector 来处理滑动操作,向下和向右滑动时onScroll方法传过来的 distanceX和distanceY都是负数。
使用方法:

​ 重写 onTouchEvent然后调用 gestureDetector.onTouchEvent(event)

val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
            override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
                log("onScroll $distanceX,$distanceY")
                moveImageBy(-distanceX, -distanceY)
                return true
            }
        })

moveImageBy()的方法很简单,直接对 matrixDraw 进行平移,然后刷新view

fun moveImageBy(dx: Float, dy: Float) {
    matrixDraw.postTranslate(dx, dy)
    invalidate()
}

override fun onDraw(canvas: Canvas) = bitmap?.let {
    canvas.drawBitmap(it, matrixDraw, paint)
  } ?: Unit

滑动处理很简单,到这里就结束了。

实现缩放

缩放的手势监听和滑动监听一样,写一个ScaleGestureDetector然后在 onTouchEvent调用相关方法。

val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {

            private var scaleFactorOld = 1f //use to remember the last scale factor

            override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
                focusPoint.set(detector.focusX, detector.focusY)
                val rawPosition = focusPoint.getRawPosition()
                if (!rawPosition.isInBitmap()) {
                    log(" $rawPosition is not in bitmap")
                    focusPoint.set(1f, 1f)
                    return false
                }
                scaleFactorOld = this@RegionImageView.matrixDraw.getScale()
                log("onDraw : onScaleBegin .....--------------------")
                return true
            }


            override fun onScale(detector: ScaleGestureDetector): Boolean {
                scaleImage(scaleFactorOld * detector.scaleFactor, focusPoint)
                return false
            }
        })

但在缩放开始时,我们需要记录一些东西。
1. 缩放的焦点:通过detector.focusXdetector.focusY我们能知道屏幕上的手机缩放的焦点,而我们对图片进行缩放,所以必须知道改点对应在bitmap中的坐标。
2. 当前缩放的比例:由于detector.scaleFactor记录的是这一次整个过程缩放的比例,所以我们需要记录下上一次已经缩放的比例。

矩阵中public void mapPoints(float[] dst, float[] src)这个方法可以求出点src通过此矩阵变换后的坐标。所以我们将matrixDraw翻转后再调用此方法,便可以通过变换后的坐标求出原坐标。

    /**
     * use screen point to find this point of bitmap
     */
    private fun PointF.getRawPosition(): PointF = PointF(-1f, -1f).apply {
        matrixDraw.invert { invert ->
            val dest = FloatArray(2)
            invert.mapPoints(dest, floatArrayOf(this@getRawPosition.x, this@getRawPosition.y))
            x = dest[0]
            y = dest[1]
        }

    }

另外还有一个是判断焦点是否在图片内,如果不在图片内那就直接返回。

private fun PointF.isInBitmap() = x in 0..bitmapWidth && y in 0..bitmapHeight

开始缩放图片: 通过postScale来实现图片的放大和缩小。由于不管是放大还是缩小还是平移,都是从当前状态为基础进行操作的。比如假设此时已经对原图片放大了两倍了,那么再使用此方法放大两倍,就将以四倍进行显示。

    /**
     * @param scale : the scale factor of the picture
     * @param focus : the point of scale
     */
    fun scaleImage(scale: Float, focus: PointF) {
        log("scaleImage -- focus: $focus , scale: $scale")
        focusPoint.set(focus)
        matrixDraw.apply {
            //apply scale
            val oldScale = getScale()
            postScale(scale / oldScale, scale / oldScale, focus.x, focus.y)
        }
        invalidate()
    }

其中getScale就是获取当前矩阵的缩放比例,实现如下:

    private fun Matrix.getScale(): Float {
        val values = FloatArray(9)
        this.getValues(values)
        return values[0]
    }

此时我们就既能平移,也能放大缩小图片了。

显示大图片的显示

当一张图片非常非常大时,若想放大图片查看细节,肯定就要加载原图。但是图片又非常大,全部加载进来肯定就直接OOM了, BitmapRegionDecoder提供了一个方法,能够加载一部分图片。

public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options)

传入的矩形rect就是我们要加载图片的区域。

首先我们需要绘制一个经过缩放的图片在屏幕上,然后再在需要显示高清图片时再加载高清图到内存中。

为了不引起图片错位(原来有一个缩放后的 bitmap ,现在又在上面叠加一个高清的bitmap),所以必须将屏幕上显示的区域在原图中计算出来。就像这样...

images_viewer_display_97.png

右上角的绿色方框就是当前屏幕显示的区域。
那怎么计算呢?有两种方法,两种方法都差不多,所以只介绍一种

  1. 先得到一个矩形变量 rectScreen,它左上右下分别为0,0,屏幕宽,屏幕高

  2. 反转矩阵 matrixDraw 然后通过mapRect方法获取屏幕在原图上的位置,得到RectImage

  3. 因为RectImage有可能会超出原图的区域,所以再将RectImagebitmap的矩形相交,得到真实显示区域。

代码就不贴了,因为有点多,可以上Github RegionImageView 查看。

后记

所有的代码都在这里AndroidExamples

另外当前的实现非常简陋,很多问题还需要解决,

当然过程中也参考了不少别人的源码

subsampling-scale-image-view

PinchImageView

上一篇下一篇

猜你喜欢

热点阅读