Detect single tap, create zoomable imageview

This commit is contained in:
Matthieu 2022-06-08 00:00:35 +02:00
parent 114f642181
commit 52cefe63aa
3 changed files with 252 additions and 48 deletions

View File

@ -18,10 +18,13 @@ package org.pixeldroid.app.posts
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.GestureDetectorCompat
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -35,13 +38,11 @@ import kotlin.math.sign
* This solution has limitations when using multiple levels of nested scrollable elements * This solution has limitations when using multiple levels of nested scrollable elements
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
*/ */
class NestedScrollableHost : ConstraintLayout { class NestedScrollableHost(context: Context, attrs: AttributeSet? = null) :
constructor(context: Context) : super(context) ConstraintLayout(context, attrs) {
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
private var mDetector: GestureDetectorCompat
private var touchSlop = 0 private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2? private val parentViewPager: ViewPager2?
get() { get() {
var v: View? = parent as? View var v: View? = parent as? View
@ -51,12 +52,14 @@ class NestedScrollableHost : ConstraintLayout {
return v as? ViewPager2 return v as? ViewPager2
} }
var doubleTapCallback: ((Boolean) -> Unit)? = null
var doubleTapCallback: (() -> Unit)? = null
private val child: View? get() = if (childCount > 0) getChildAt(0) else null private val child: View? get() = if (childCount > 0) getChildAt(0) else null
init { init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop touchSlop = ViewConfiguration.get(context).scaledTouchSlop
mDetector = GestureDetectorCompat(context, MyGestureListener())
} }
private fun canChildScroll(orientation: Int, delta: Float): Boolean { private fun canChildScroll(orientation: Int, delta: Float): Boolean {
@ -69,35 +72,49 @@ class NestedScrollableHost : ConstraintLayout {
} }
override fun onInterceptTouchEvent(e: MotionEvent): Boolean { override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e) mDetector.onTouchEvent(e)
return super.onInterceptTouchEvent(e) return super.onInterceptTouchEvent(e)
} }
private fun handleInterceptTouchEvent(e: MotionEvent) { private inner class MyGestureListener : GestureDetector.SimpleOnGestureListener() {
val orientation = parentViewPager?.orientation ?: return
if (e.action == MotionEvent.ACTION_DOWN) { override fun onDown(e: MotionEvent): Boolean {
initialX = e.x val orientation = parentViewPager?.orientation ?: return true
initialY = e.y
doubleTapCallback?.invoke(true) if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
} return true
// Early return if child can't scroll in same direction as parent }
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
if (e.action == MotionEvent.ACTION_DOWN) {
parent.requestDisallowInterceptTouchEvent(true) parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX return true
val dy = e.y - initialY }
override fun onDoubleTap(e: MotionEvent?): Boolean {
doubleTapCallback?.invoke()
return super.onDoubleTap(e)
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
//TODO open image full screen
Toast.makeText(this@NestedScrollableHost.context, "yay you did it", Toast.LENGTH_SHORT).show()
return super.onSingleTapConfirmed(e)
}
override fun onScroll(
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
val orientation = parentViewPager?.orientation ?: return true
val dx = e2.x - e1.x
val dy = e2.y - e1.y
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
// assuming ViewPager2 touch-slop is 2x touch-slop of child // assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f/ touchSlopModifier else 1f val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f / touchSlopModifier else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f/touchSlopModifier val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f / touchSlopModifier
if(dx.absoluteValue * .5f > touchSlop || scaledDy > touchSlop) doubleTapCallback?.invoke(false)
if (scaledDx > touchSlop || scaledDy > touchSlop) { if (scaledDx > touchSlop || scaledDy > touchSlop) {
@ -115,6 +132,7 @@ class NestedScrollableHost : ConstraintLayout {
} }
} }
} }
return super.onScroll(e1, e2, distanceX, distanceY)
} }
} }

View File

@ -460,31 +460,21 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
} }
//Activate double tap liking //Activate double tap liking
var clicked = false
binding.postPagerHost.doubleTapCallback = { binding.postPagerHost.doubleTapCallback = {
if(!it) clicked = false lifecycleScope.launchWhenCreated {
else lifecycleScope.launchWhenCreated {
//Check that the post isn't hidden //Check that the post isn't hidden
if(binding.sensitiveWarning.visibility == View.GONE) { if (binding.sensitiveWarning.visibility == View.GONE) {
//Check for double click val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
if(clicked) { if (binding.liker.isChecked) {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser() // Button is active, unlike
if (binding.liker.isChecked) { binding.liker.isChecked = false
// Button is active, unlike unLikePostCall(api)
binding.liker.isChecked = false
unLikePostCall(api)
} else {
// Button is inactive, like
binding.liker.playAnimation()
binding.liker.isChecked = true
binding.likeAnimation.animateView()
likePostCall(api)
}
} else { } else {
clicked = true // Button is inactive, like
binding.liker.playAnimation()
//Reset clicked to false after 500ms binding.liker.isChecked = true
binding.postPager.handler.postDelayed(fun() { clicked = false }, 500) binding.likeAnimation.animateView()
likePostCall(api)
} }
} }
} }

View File

@ -0,0 +1,196 @@
package org.pixeldroid.app.posts
import android.content.Context
import android.graphics.Matrix
import androidx.appcompat.widget.AppCompatImageView
import android.graphics.PointF
import android.view.ScaleGestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
import android.util.AttributeSet
import android.util.Log
import kotlin.math.abs
import kotlin.math.min
// See https://stackoverflow.com/a/29030243
class TouchImageView(context: Context, attrs: AttributeSet? = null) :
AppCompatImageView(context, attrs) {
var touchMatrix: Matrix? = Matrix()
var mode = NONE
// Remember some things for zooming
var last = PointF()
var start = PointF()
var minScale = 1f
var maxScale = 3f
var m: FloatArray = FloatArray(9)
var viewWidth = 0
var viewHeight = 0
var saveScale = 1f
private var origWidth = 0f
private var origHeight = 0f
var oldMeasuredWidth = 0
var oldMeasuredHeight = 0
var mScaleDetector: ScaleGestureDetector? = null
init {
super.setClickable(true)
mScaleDetector = ScaleGestureDetector(context, ScaleListener())
imageMatrix = touchMatrix
scaleType = ScaleType.MATRIX
setOnTouchListener { _, event ->
mScaleDetector!!.onTouchEvent(event)
val curr = PointF(event.x, event.y)
when (event.action) {
MotionEvent.ACTION_MOVE -> if (mode == DRAG) {
val deltaX = curr.x - last.x
val deltaY = curr.y - last.y
val fixTransX =
getFixDragTrans(deltaX, viewWidth.toFloat(), origWidth * saveScale)
val fixTransY =
getFixDragTrans(deltaY, viewHeight.toFloat(), origHeight * saveScale)
touchMatrix!!.postTranslate(fixTransX, fixTransY)
fixTrans()
last[curr.x] = curr.y
val transX = m[Matrix.MTRANS_X]
if ((getFixTrans(transX,
viewWidth.toFloat(),
origWidth * saveScale) + fixTransX).toInt() == 0
) startInterceptEvent() else stopInterceptEvent()
}
MotionEvent.ACTION_DOWN -> {
last.set(curr)
start.set(last)
mode = DRAG
stopInterceptEvent()
}
MotionEvent.ACTION_UP -> {
mode = NONE
val xDiff = abs(curr.x - start.x).toInt()
val yDiff = abs(curr.y - start.y).toInt()
if (xDiff < CLICK && yDiff < CLICK) performClick()
startInterceptEvent()
}
MotionEvent.ACTION_POINTER_UP -> mode = NONE
}
imageMatrix = touchMatrix
invalidate()
true // indicate event was handled
}
}
private fun startInterceptEvent() {
parent.requestDisallowInterceptTouchEvent(false)
}
private fun stopInterceptEvent() {
parent.requestDisallowInterceptTouchEvent(true)
}
fun setMaxZoom(x: Float) {
maxScale = x
}
private inner class ScaleListener : SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
mode = ZOOM
return true
}
override fun onScale(detector: ScaleGestureDetector): Boolean {
var mScaleFactor = detector.scaleFactor
val origScale = saveScale
saveScale *= mScaleFactor
if (saveScale > maxScale) {
saveScale = maxScale
mScaleFactor = maxScale / origScale
} else if (saveScale < minScale) {
saveScale = minScale
mScaleFactor = minScale / origScale
}
if (origWidth * saveScale <= viewWidth || origHeight * saveScale <= viewHeight) touchMatrix!!.postScale(
mScaleFactor,
mScaleFactor,
(viewWidth / 2).toFloat(),
(viewHeight / 2).toFloat()) else touchMatrix!!.postScale(mScaleFactor,
mScaleFactor,
detector.focusX,
detector.focusY)
fixTrans()
return true
}
}
fun fixTrans() {
touchMatrix!!.getValues(m)
val transX = m[Matrix.MTRANS_X]
val transY = m[Matrix.MTRANS_Y]
val fixTransX = getFixTrans(transX, viewWidth.toFloat(), origWidth * saveScale)
val fixTransY = getFixTrans(transY, viewHeight.toFloat(), origHeight * saveScale)
if (fixTransX != 0f || fixTransY != 0f) touchMatrix!!.postTranslate(fixTransX, fixTransY)
}
fun getFixTrans(trans: Float, viewSize: Float, contentSize: Float): Float {
val minTrans: Float
val maxTrans: Float
if (contentSize <= viewSize) {
minTrans = 0f
maxTrans = viewSize - contentSize
} else {
minTrans = viewSize - contentSize
maxTrans = 0f
}
if (trans < minTrans) return -trans + minTrans
return if (trans > maxTrans) -trans + maxTrans else 0f
}
fun getFixDragTrans(delta: Float, viewSize: Float, contentSize: Float): Float {
return if (contentSize <= viewSize) {
0f
} else delta
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
viewWidth = MeasureSpec.getSize(widthMeasureSpec)
viewHeight = MeasureSpec.getSize(heightMeasureSpec)
// Rescales image on rotation
if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight || viewWidth == 0 || viewHeight == 0) return
oldMeasuredHeight = viewHeight
oldMeasuredWidth = viewWidth
if (saveScale == 1f) {
//Fit to screen.
val scale: Float
val drawable = drawable
if (drawable == null || drawable.intrinsicWidth == 0 || drawable.intrinsicHeight == 0) return
val bmWidth = drawable.intrinsicWidth
val bmHeight = drawable.intrinsicHeight
Log.d("bmSize", "bmWidth: $bmWidth bmHeight : $bmHeight")
val scaleX = viewWidth.toFloat() / bmWidth.toFloat()
val scaleY = viewHeight.toFloat() / bmHeight.toFloat()
scale = min(scaleX, scaleY)
touchMatrix!!.setScale(scale, scale)
// Center the image
var redundantYSpace = viewHeight.toFloat() - scale * bmHeight.toFloat()
var redundantXSpace = viewWidth.toFloat() - scale * bmWidth.toFloat()
redundantYSpace /= 2f
redundantXSpace /= 2f
touchMatrix!!.postTranslate(redundantXSpace, redundantYSpace)
origWidth = viewWidth - 2 * redundantXSpace
origHeight = viewHeight - 2 * redundantYSpace
imageMatrix = touchMatrix
}
fixTrans()
}
companion object {
// We can be in one of these 3 states
const val NONE = 0
const val DRAG = 1
const val ZOOM = 2
const val CLICK = 3
}
}