支持縮放的ImageView的實現(xiàn)原理分析

效果圖

實現(xiàn)原理:

  • canvas.DrawBitmap() + GestureDetector + Scroller
    通過GestureDetector得到用戶的onDoubleTap事件(用于放大縮小圖片)媳板,onScroll事件(用于拖動圖片)皮迟,onFling事件(用于快速滑動圖片)店枣,根據(jù)事件計算圖片偏移量
  • 借助Scroller計算fling情形下的滑動

代碼分析

根據(jù)偏移量繪制圖片

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        if (canvas == null) {
            return
        }
        val scale = smallScale + (bigScale - smallScale) * fraction
        canvas.translate(offsetX * fraction, offsetY * fraction)
        canvas.scale(scale, scale, width / 2f, height / 2f)
        canvas.translate(originOffsetX, originOffsetY)
        canvas.drawBitmap(bitmap, 0f, 0f, paint)
    }

初始化GestureDetector

    private val gestureDetector: GestureDetector = GestureDetector(context, this)
    init {
        gestureDetector.setOnDoubleTapListener(this)
    }

監(jiān)聽雙擊事件開啟縮放動畫

    // 縮放動畫
    private var animator: ObjectAnimator = ObjectAnimator.ofFloat(this, "fraction", 0f, 1f)
    override fun onDoubleTap(e: MotionEvent?): Boolean {
        bigMode = !bigMode
        if (bigMode) {
            animator.start()
            offsetX = getValidValue(-(e!!.x - width / 2) * bigScale / 2, -getMaxOffsetX(), getMaxOffsetX())
            offsetY = getValidValue(-(e.y - height / 2) * bigScale / 2, -getMaxOffsetY(), getMaxOffsetY())
        } else {
            animator.reverse()
        }
        return true
    }

監(jiān)聽滑動事件内颗,計算偏移量,實現(xiàn)拖動圖片的效果

    override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
        if (bigMode) {
            offsetX = getValidValue(offsetX - distanceX, -getMaxOffsetX(), getMaxOffsetX())
            offsetY = getValidValue(offsetY - distanceY, -getMaxOffsetY(), getMaxOffsetY())
            invalidate()
        }
        return false
    }

監(jiān)聽onFling事件坛缕,配合Scroller秋茫,實現(xiàn)快速滑動的效果

    override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
        scroller.fling(offsetX.toInt(), offsetY.toInt(),
                velocityX.toInt(), velocityY.toInt(),
                -getMaxOffsetX().toInt(), getMaxOffsetX().toInt(),
                -getMaxOffsetY().toInt(), getMaxOffsetY().toInt(),
                (getMaxOffsetX() * .1f).toInt(), (getMaxOffsetX() * .1f).toInt())
        postOnAnimation(this@ScalableImageView)
        return true
    }

    override fun run() {
        if (scroller.computeScrollOffset()) {
            offsetX = scroller.currX.toFloat()
            offsetY = scroller.currY.toFloat()
            invalidate()
            postOnAnimation(this)
        }
    }

完整代碼:

package com.zxz.hencoderplus.lesson_11_scalable_image_view

import android.animation.Animator
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.widget.OverScroller
import com.zxz.hencoderplus.R
import com.zxz.hencoderplus.util.BitmapUtil

/**
 * <pre>
 *     @author : Zhan Xuzhao
 *     time   : 2018/7/29 10:16
 *     desc   :
 *     version: 1.0
 * </pre>
 */
class ScalableImageView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet), Runnable,
        GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener, Animator.AnimatorListener {

    companion object {
        private const val SCALE_FRACTION = 3f
    }

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val bitmap: Bitmap = BitmapUtil.getSquareBitmap(context, R.drawable.artanis)
    private val bitmapWidth = bitmap.width
    private val bitmapHeight = bitmap.height
    private var originOffsetX = 0f
    private var originOffsetY = 0f
    private var offsetX = 0f
    private var offsetY = 0f
    private var bigScale = 0f
    private var smallScale = 0f
    private val scroller = OverScroller(context)
    var fraction = 0f
        set(value) {
            field = value
            invalidate()
        }
    private var animator: ObjectAnimator = ObjectAnimator.ofFloat(this, "fraction", 0f, 1f)
    private var bigMode: Boolean = false
    private val gestureDetector: GestureDetector = GestureDetector(context, this)

    init {
        animator.addListener(this)
        gestureDetector.setOnDoubleTapListener(this)
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        if (canvas == null) {
            return
        }
        val scale = smallScale + (bigScale - smallScale) * fraction
        canvas.translate(offsetX * fraction, offsetY * fraction)
        canvas.scale(scale, scale, width / 2f, height / 2f)
        canvas.translate(originOffsetX, originOffsetY)
        canvas.drawBitmap(bitmap, 0f, 0f, paint)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        originOffsetX = (measuredWidth / 2 - bitmapWidth / 2).toFloat()
        originOffsetY = (measuredHeight / 2 - bitmapHeight / 2).toFloat()
        val widthScale = 1f * measuredWidth / bitmapWidth
        val heightScale = 1f * measuredHeight / bitmapHeight
        smallScale = Math.min(widthScale, heightScale)
        bigScale = Math.max(widthScale, heightScale) * SCALE_FRACTION
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent?): Boolean {
        val onTouchEvent = gestureDetector.onTouchEvent(event)
        if (!onTouchEvent) {
            super.onTouchEvent(event)
        }
        return onTouchEvent
    }

    override fun run() {
        if (scroller.computeScrollOffset()) {
            offsetX = scroller.currX.toFloat()
            offsetY = scroller.currY.toFloat()
            invalidate()
            postOnAnimation(this)
        }
    }

    override fun onShowPress(e: MotionEvent?) {}

    override fun onSingleTapUp(e: MotionEvent?): Boolean {
        return false
    }

    override fun onDown(e: MotionEvent?): Boolean {
        // 觸摸時停止fling
        scroller.abortAnimation()
        return true
    }

    override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
        scroller.fling(offsetX.toInt(), offsetY.toInt(),
                velocityX.toInt(), velocityY.toInt(),
                -getMaxOffsetX().toInt(), getMaxOffsetX().toInt(),
                -getMaxOffsetY().toInt(), getMaxOffsetY().toInt(),
                (getMaxOffsetX() * .1f).toInt(), (getMaxOffsetX() * .1f).toInt())
        postOnAnimation(this@ScalableImageView)
        return true
    }

    override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
        if (bigMode) {
            offsetX = getValidValue(offsetX - distanceX, -getMaxOffsetX(), getMaxOffsetX())
            offsetY = getValidValue(offsetY - distanceY, -getMaxOffsetY(), getMaxOffsetY())
            invalidate()
        }
        return false
    }

    override fun onLongPress(e: MotionEvent?) {}

    override fun onDoubleTap(e: MotionEvent?): Boolean {
        bigMode = !bigMode
        if (bigMode) {
            animator.start()
            offsetX = getValidValue(-(e!!.x - width / 2) * bigScale / 2, -getMaxOffsetX(), getMaxOffsetX())
            offsetY = getValidValue(-(e.y - height / 2) * bigScale / 2, -getMaxOffsetY(), getMaxOffsetY())
        } else {
            animator.reverse()
        }
        return true
    }

    override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
        return false
    }

    override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
        return false
    }

    override fun onAnimationRepeat(animation: Animator?) {}

    override fun onAnimationEnd(animation: Animator?) {}

    override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
        if (isReverse) {
            offsetX = 0f
            offsetY = 0f
        }
    }

    override fun onAnimationCancel(animation: Animator?) {}

    override fun onAnimationStart(animation: Animator?) {}

    private fun getMaxOffsetY() = bitmapHeight * bigScale / 2f - height / 2f

    private fun getMaxOffsetX() = bitmapWidth * bigScale / 2f - width / 2f

    private fun getValidValue(target: Float, min: Float, max: Float): Float {
        return Math.max(Math.min(target, max), min)
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末工猜,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子币狠,更是在濱河造成了極大的恐慌游两,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漩绵,死亡現(xiàn)場離奇詭異贱案,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)止吐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門宝踪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碍扔,你說我怎么就攤上這事瘩燥。” “怎么了不同?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵厉膀,是天一觀的道長。 經(jīng)常有香客問我,道長站蝠,這世上最難降的妖魔是什么汰具? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮菱魔,結(jié)果婚禮上留荔,老公的妹妹穿的比我還像新娘。我一直安慰自己澜倦,他們只是感情好聚蝶,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著藻治,像睡著了一般碘勉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上桩卵,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天验靡,我揣著相機(jī)與錄音,去河邊找鬼雏节。 笑死胜嗓,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的钩乍。 我是一名探鬼主播辞州,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼寥粹!你這毒婦竟也來了变过?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤涝涤,失蹤者是張志新(化名)和其女友劉穎媚狰,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妄痪,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡哈雏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了衫生。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片裳瘪。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖罪针,靈堂內(nèi)的尸體忽然破棺而出彭羹,到底是詐尸還是另有隱情,我是刑警寧澤泪酱,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布派殷,位于F島的核電站还最,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏毡惜。R本人自食惡果不足惜拓轻,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望经伙。 院中可真熱鬧扶叉,春花似錦、人聲如沸帕膜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽垮刹。三九已至达吞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荒典,已是汗流浹背酪劫。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留寺董,地道東北人契耿。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像螃征,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子透敌,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容