0%

手写PhotoView


又是一段时间没有更新博客了,这段时间对以前的学习做了一个总结,还是很值得的,希望接下来的时间里,能继续保持学习的热情,稳定输出学习和工作的总结到博客,毕竟一个好习惯值得坚持下去~也希望和各位大佬一起学习和交流,接下来记录用kotlin手写PhotoView的过程


自定义View

  1. 居中位置绘制bitmap
  2. 计算小图,大图缩放比
  3. fling,双击,滑动时调整画布偏移值绘制
  4. 双击,缩放手势调整画布当前缩放比例绘制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
class PhotoView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
lateinit var bitmap: Bitmap
lateinit var paint: Paint
/**
* 原始位置
*/
var originalOffsetX = 0f
var originalOffsetY = 0f
/**
* 滑动偏移
*/
var offsetX = 0f
var offsetY = 0f
var smallScale = 0f // 小图缩放比
var bigScale = 0f // 大图缩放比
var currentScale = 0f // 当前缩放比
get() {
Log.i(TAG, "get: $field")
return field
}
set(value) {
field = value
invalidate()
Log.i(TAG, "set: $value")
}
var isLarge = false // 是否放大状态
lateinit var gestureDetector: GestureDetector // 手势回调
lateinit var scaleGestureDetector: ScaleGestureDetector // 缩放手势回调
val overScroller by lazy { OverScroller(context) } // 滚动工具类,边界回弹
// 缩放动画
val scaleAnimation: ObjectAnimator by lazy {
val temp = ObjectAnimator.ofFloat(this, "currentScale", 0f)
temp.setFloatValues(smallScale, bigScale)
temp
}

companion object {
const val SCALE_FACTOR = 1.5f
}

fun initBitmap(bitmap: Bitmap) {
if (width == 0 || height == 0) {
throw IllegalStateException("call after View is rendered")
}
this.bitmap = bitmap
paint = Paint(Paint.ANTI_ALIAS_FLAG)
gestureDetector = GestureDetector(context, PhotoGesture())
scaleGestureDetector = ScaleGestureDetector(context, PhotoScaleGestureListener())
// bitmap居中渲染
originalOffsetX = (width - bitmap.width) / 2f
originalOffsetY = (height - bitmap.height) / 2f
// 横图计算缩放比
if (bitmap.width.toFloat() / bitmap.height > width.toFloat() / height) {
smallScale = width.toFloat() / bitmap.width.toFloat()
bigScale = (height.toFloat() / bitmap.height.toFloat()) * SCALE_FACTOR
}
// 竖图计算缩放比
else {
smallScale = height.toFloat() / bitmap.height.toFloat()
bigScale = (width.toFloat() / bitmap.width.toFloat()) * SCALE_FACTOR
}
// 当前缩放为最小缩放
currentScale = smallScale
invalidate()
}

// 边界修正
private fun fixOffset(){
offsetX = min(offsetX,(bitmap.width * bigScale - width)/ 2)
offsetX = max(offsetX, -(bitmap.width * bigScale - width) / 2)
offsetY = min(offsetY, (bitmap.height * bigScale - height) / 2)
offsetY = max(offsetY, -(bitmap.height * bigScale - height) / 2)
}

override fun onDraw(canvas: Canvas?) {
if (this::bitmap.isInitialized) {
// 当前缩放占比
val scaleFaction = (currentScale - smallScale) / (bigScale - smallScale)
// 画布平移
canvas?.translate(offsetX * scaleFaction, offsetY * scaleFaction)
// 画布缩放
canvas?.scale(currentScale, currentScale, (width / 2).toFloat(), (height / 2).toFloat())
canvas?.drawBitmap(bitmap, originalOffsetX, originalOffsetY, paint)
}
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
// 默认缩放回调处理
var res = scaleGestureDetector.onTouchEvent(event)
// 不是缩放操作时,由手势回调处理
if (!scaleGestureDetector.isInProgress) {
res = gestureDetector.onTouchEvent(event)
}
return res
}

// 滑动处理
val runnable = object :Runnable{
override fun run() {
if (overScroller.computeScrollOffset()){
offsetX = overScroller.currX.toFloat()
offsetY = overScroller.currY.toFloat()
invalidate()
postOnAnimation(this)
}
}
}

inner class PhotoGesture : GestureDetector.SimpleOnGestureListener() {

override fun onDown(e: MotionEvent?): Boolean {
return true // 拦截DOWN事件
}

override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
// 当前放大状态,调overScroller的fling
if (isLarge) {
overScroller.fling(offsetX.toInt(), offsetY.toInt(), velocityX.toInt(), velocityY.toInt(),
(-(bitmap.width * bigScale - width) / 2).toInt(),
((bitmap.width * bigScale - width) / 2).toInt(),
(-(bitmap.height * bigScale - height) / 2).toInt(),
((bitmap.height * bigScale - height) / 2).toInt(),
300,300)
// 滑动刷新偏移值处理
postOnAnimation(runnable)
}
return super.onFling(e1, e2, velocityX, velocityY)
}

override fun onDoubleTap(e: MotionEvent?): Boolean {
isLarge = !isLarge
// 当前是放大状态
if (isLarge) {
// 刷新绘制位置
e?.let {
offsetX = (it.x - width / 2f) - (it.x - width / 2f) * bigScale / smallScale
offsetY = (it.y - height / 2f) - (it.y - height / 2f) * bigScale / smallScale
}
// 边界修正
fixOffset()
// 缩放动画开始
scaleAnimation.start()
} else {
scaleAnimation.reverse()
}

return super.onDoubleTap(e)
}

override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
// 当前是放大状态
if (isLarge) {
// 刷新绘制位置
offsetX -= distanceX
offsetY -= distanceY
// 边界修正
fixOffset()
invalidate()
}
return super.onScroll(e1, e2, distanceX, distanceY)
}

}

inner class PhotoScaleGestureListener:ScaleGestureDetector.OnScaleGestureListener{
var initScale = 0f
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
// 初始缩放比例
initScale = currentScale
return true
}

override fun onScaleEnd(detector: ScaleGestureDetector?) {

}

override fun onScale(detector: ScaleGestureDetector?): Boolean {
// 如果当前缩放比 大于 最小缩放比 并且 非放大状态 或 当前缩放比 等于 最小缩放比 并且非放大状态
if ((currentScale > smallScale && !isLarge) || (currentScale == smallScale && !isLarge)){
// 设置为放大状态
isLarge = !isLarge
}
// 如果初始缩放比 * 缩放系数 小于 最小缩放比 赋值为最小缩放比 否则为初始缩放比 * 缩放系数
detector?.let {
currentScale = if (initScale * it.scaleFactor < smallScale){
smallScale
} else{
initScale * it.scaleFactor
}
}
invalidate()

return false
}
}
}

在布局中使用

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.tws.moments.views.PhotoView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

设置bitmap

1
2
3
4
5
try {
photo.initBitmap(bitmap)
} catch (e: Exception) {
Log.i(TAG, "subscribe: ${e.stackTrace}")
}