natario1 / ZoomLayout

2D zoom and pan behavior for View hierarchies, images, video streams, and much more, written in Kotlin for Android.
https://natario1.github.io/ZoomLayout
Apache License 2.0
1.05k stars 147 forks source link

Is it possible to have a non-pixelated vector-drawable loaded? #172

Closed PXNX closed 4 years ago

PXNX commented 4 years ago

What did I do?

I want to load a clickable vector-drawable inside the zoomlayout like this:

<com.otaliastudios.zoom.ZoomLayout
            android:id="@+id/zv_map"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:alignment="center"
            app:animationDuration="250"
            app:hasClickableChildren="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:maxZoom="25.0"
            app:maxZoomType="zoom"
            app:minZoom="1.0"
            app:minZoomType="zoom"
            app:scrollEnabled="true"
            app:twoFingersScrollEnabled="true"
            app:zoomEnabled="true">

            <com.richpath.RichPathView
                android:id="@+id/iv_map"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:scaleType="matrix"
                app:vector="@drawable/map_world" />

</com.otaliastudios.zoom.ZoomLayout>

I tried playing around with it for quite a bit, but it still seems to be pixelated.

For the clickable vector I used this library

Version used

Latest release (1.8.0)

PXNX commented 4 years ago

The vector seems to get pixelated whenever there is more than one layout wrapping it.

markusressel commented 4 years ago

This question comes up every now and then. Sadly I don't think we can assist with it, as we have not investigated SVGs. I'd like to keep this issue open though, if someone finds a solution or blueprint how to achieve this that we can include in the documentation.

PXNX commented 4 years ago

I kind of got it non-pixelated now. Here's the code:

Zoomview

package pxnx.row

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Matrix
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
import kotlin.math.*

class ZoomView : FrameLayout {
    constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(
        context!!,
        attrs,
        defStyle
    )

    constructor(context: Context?, attrs: AttributeSet?) : super(
        context!!, attrs
    )

    constructor(context: Context?) : super(context!!)

    interface ZoomViewListener {
        fun onZoomStarted(zoom: Float, zoomx: Float, zoomy: Float)
        fun onZooming(zoom: Float, zoomx: Float, zoomy: Float)
        fun onZoomEnded(zoom: Float, zoomx: Float, zoomy: Float)
    }

    private var zoom = 1.0f
    private var maxZoom = 40.0f
    private var smoothZoom = 1.0f
    private var zoomX = 0f
    private var zoomY = 0f
    private var smoothZoomX = 0f
    private var smoothZoomY = 0f
    private var scrolling = false

    private var miniMapHeight = -1

    private var lastTapTime: Long = 0
    private var touchStartX = 0f
    private var touchStartY = 0f
    private var touchLastX = 0f
    private var touchLastY = 0f
    private var startd = 0f
    private var pinching = false
    private var lastd = 0f
    private var lastdx1 = 0f
    private var lastdy1 = 0f
    private var lastdx2 = 0f
    private var lastdy2 = 0f

    private val m = Matrix()
    private val p = Paint()

    private var listener: ZoomViewListener? = null
    private var ch: Bitmap? = null
    fun getMaxZoom(): Float {
        return maxZoom
    }

    fun setMaxZoom(maxZoom: Float) {
        if (maxZoom < 1.0f) {
            return
        }
        this.maxZoom = maxZoom
    }

    fun zoomTo(zoom: Float, x: Float, y: Float) {
        this.zoom = min(zoom, maxZoom)
        zoomX = x
        zoomY = y
        smoothZoomTo(this.zoom, x, y)
    }

    fun smoothZoomTo(zoom: Float, x: Float, y: Float) {
        smoothZoom = clamp(1.0f, zoom, maxZoom)
        smoothZoomX = x
        smoothZoomY = y
        if (listener != null) {
            listener!!.onZoomStarted(smoothZoom, x, y)
        }
    }

    fun setListener(listener: ZoomViewListener?) {
        this.listener = listener
    }

    val zoomFocusX: Float
        get() = zoomX * zoom
    val zoomFocusY: Float
        get() = zoomY * zoom

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        if (ev.pointerCount == 1)
            processSingleTouchEvent(ev)
        if (ev.pointerCount == 2)
            processDoubleTouchEvent(ev)

        rootView.invalidate()
        invalidate()
        return true
    }

    private fun processSingleTouchEvent(ev: MotionEvent) {
        val x = ev.x
        val y = ev.y
        val w = miniMapHeight * width.toFloat() / height
        val h = miniMapHeight.toFloat()

        val lx = x - touchStartX
        val ly = y - touchStartY
        val l = hypot(lx.toDouble(), ly.toDouble()).toFloat()
        val dx = x - touchLastX
        val dy = y - touchLastY
        touchLastX = x
        touchLastY = y
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                touchStartX = x
                touchStartY = y
                touchLastX = x
                touchLastY = y
                scrolling = false
            }
            MotionEvent.ACTION_MOVE -> if (scrolling || smoothZoom > 1.0f && l > 30.0f) {
                if (!scrolling) {
                    scrolling = true
                    ev.action = MotionEvent.ACTION_CANCEL
                    super.dispatchTouchEvent(ev)
                }
                smoothZoomX -= dx / zoom
                smoothZoomY -= dy / zoom
                return
            }
            MotionEvent.ACTION_OUTSIDE, MotionEvent.ACTION_UP ->
                if (l < 30.0f) {
                    if (System.currentTimeMillis() - lastTapTime < 4) { // was 400
                        if (smoothZoom == 1.0f) {
                            smoothZoomTo(maxZoom, x, y)
                        } else {
                            smoothZoomTo(1.0f, width / 2.0f, height / 2.0f)
                        }
                        lastTapTime = 0
                        ev.action = MotionEvent.ACTION_CANCEL
                        super.dispatchTouchEvent(ev)
                        return
                    }
                    lastTapTime = System.currentTimeMillis()
                    performClick()
                }
            else -> {
            }
        }
        ev.setLocation(zoomX + (x - 0.5f * width) / zoom, zoomY + (y - 0.5f * height) / zoom)
        ev.x
        ev.y
        super.dispatchTouchEvent(ev)
    }

    private fun processDoubleTouchEvent(ev: MotionEvent) {
        val x1 = ev.getX(0)
        val dx1 = x1 - lastdx1
        lastdx1 = x1
        val y1 = ev.getY(0)
        val dy1 = y1 - lastdy1
        lastdy1 = y1
        val x2 = ev.getX(1)
        val dx2 = x2 - lastdx2
        lastdx2 = x2
        val y2 = ev.getY(1)
        val dy2 = y2 - lastdy2
        lastdy2 = y2

        // pointers distance
        val d = hypot(x2 - x1.toDouble(), y2 - y1.toDouble()).toFloat()
        val dd = d - lastd
        lastd = d
        val ld = abs(d - startd)
        atan2(y2 - y1.toDouble(), x2 - x1.toDouble())
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> {
                startd = d
                pinching = false
            }
            MotionEvent.ACTION_MOVE -> if (pinching || ld > 30.0f) {
                pinching = true
                val dxk = 0.5f * (dx1 + dx2)
                val dyk = 0.5f * (dy1 + dy2)
                smoothZoomTo(
                    1.0f.coerceAtLeast(zoom * d / (d - dd)),
                    zoomX - dxk / zoom,
                    zoomY - dyk / zoom
                )
            }
            MotionEvent.ACTION_UP -> pinching = false
            else -> pinching = false
        }
        ev.action = MotionEvent.ACTION_CANCEL
        super.dispatchTouchEvent(ev)
    }

    private fun clamp(min: Float, value: Float, max: Float): Float {
        return min.coerceAtLeast(value.coerceAtMost(max))
    }

    private fun lerp(a: Float, b: Float, k: Float): Float {
        return a + (b - a) * k
    }

    private fun bias(a: Float, b: Float, k: Float): Float {
        return if (abs(b - a) >= k) a + k * sign(b - a) else b
    }

    override fun dispatchDraw(canvas: Canvas) {
        zoom = lerp(bias(zoom, smoothZoom, 0.05f), smoothZoom, 0.2f)
        smoothZoomX =
            clamp(0.5f * width / smoothZoom, smoothZoomX, width - 0.5f * width / smoothZoom)
        smoothZoomY =
            clamp(0.5f * height / smoothZoom, smoothZoomY, height - 0.5f * height / smoothZoom)
        zoomX = lerp(bias(zoomX, smoothZoomX, 0.1f), smoothZoomX, 0.35f)
        zoomY = lerp(bias(zoomY, smoothZoomY, 0.1f), smoothZoomY, 0.35f)
        if (zoom != smoothZoom && listener != null) {
            listener!!.onZooming(zoom, zoomX, zoomY)
        }
        val animating =
            abs(zoom - smoothZoom) > 0.0000001f || abs(zoomX - smoothZoomX) > 0.0000001f || abs(
                zoomY - smoothZoomY
            ) > 0.0000001f

        if (childCount == 0)
            return

        m.setTranslate(0.5f * width, 0.5f * height)
        m.preScale(zoom, zoom)
        m.preTranslate(
            -clamp(0.5f * width / zoom, zoomX, width - 0.5f * width / zoom),
            -clamp(0.5f * height / zoom, zoomY, height - 0.5f * height / zoom)
        )

        val v = getChildAt(0)
        m.preTranslate(v.left.toFloat(), v.top.toFloat())

        if (animating && ch == null && isAnimationCacheEnabled) {
            v.isDrawingCacheEnabled = true
            ch = v.drawingCache
        }

        if (animating && isAnimationCacheEnabled && ch != null) {
            p.color = -0x1
            canvas.drawBitmap(ch!!, m, p)
        } else {
            ch = null
            canvas.save()
            canvas.concat(m)
            v.draw(canvas)
            canvas.restore()
        }

        rootView.invalidate()
        invalidate()
    }
}

XML

<com.otaliastudios.zoom.ZoomLayout
            android:id="@+id/zv_map"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:alignment="center"
            app:animationDuration="250"
            app:hasClickableChildren="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:maxZoom="40.0"
            app:maxZoomType="zoom"
            app:minZoom="1.0"
            app:minZoomType="zoom"
            app:scrollEnabled="true"
            app:twoFingersScrollEnabled="true"
            app:zoomEnabled="true">

            <pxnx.row.ZoomView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content">

                <com.richpath.RichPathView
                    android:id="@+id/iv_map"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_centerInParent="true"
                    android:adjustViewBounds="true"
                    android:scaleType="matrix"
                    app:vector="@drawable/map_de_nl" />

            </pxnx.row.ZoomView>

        </com.otaliastudios.zoom.ZoomLayout>
PXNX commented 4 years ago

I was just trying around, wrapped it and it worked. The I disabled the ZoomView's scrolling/zooming (as it's somewhat annoying).

It by the way feateres a way to zoom on double-tap. Such a feature might be cool to see here, too :D

markusressel commented 4 years ago

Thx for providing code, however I am not sure what to make of it.

Is this a custom implementation of a ZoomView thats not using the ZoomEngine? How is that related to this library? Would it be possible to integrate this into ZoomLayout (this library)? If so, could you integrate it and open a PR?

PXNX commented 4 years ago

It's not a custom implementation of this libary, but another approach taken to make my View zoomable.

The reason I use this library is, that the zooming, panning etc. work much nicer than with the Zoomview I used. The only caviat being that it got pixelated the way I intended to use it.

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had activity in the last 20 days. It will be closed if no further activity occurs within the next seven days. Thank you for your contributions.