Android自定义View实现图片放大,平移和显示大图片
先放效果:
bitmap_display.png假设A是图片,B是手机屏幕,有一道光照在A上,然后A在手机上有个投影A'。假设我们只能看到手机屏幕上的内容,那么如果光源的位置发生改变,那么A'在屏幕上的位置和大小也可能会随之改变。
而矩阵在这里就充当这光源的作用。
矩阵提供这么几类方法
- tranlate: 移动
- scale:缩放
- rotate:旋转
- 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.focusX
和detector.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右上角的绿色方框就是当前屏幕显示的区域。
那怎么计算呢?有两种方法,两种方法都差不多,所以只介绍一种
-
先得到一个矩形变量
rectScreen
,它左上右下分别为0,0,屏幕宽,屏幕高 -
反转矩阵
matrixDraw
然后通过mapRect
方法获取屏幕在原图上的位置,得到RectImage
-
因为
RectImage
有可能会超出原图的区域,所以再将RectImage
与bitmap
的矩形相交,得到真实显示区域。
代码就不贴了,因为有点多,可以上Github RegionImageView 查看。
后记
所有的代码都在这里AndroidExamples。
另外当前的实现非常简陋,很多问题还需要解决,
当然过程中也参考了不少别人的源码