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.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.GestureDetectorCompat
import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
import kotlin.math.absoluteValue
@ -35,13 +38,11 @@ import kotlin.math.sign
* 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).
*/
class NestedScrollableHost : ConstraintLayout {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
class NestedScrollableHost(context: Context, attrs: AttributeSet? = null) :
ConstraintLayout(context, attrs) {
private var mDetector: GestureDetectorCompat
private var touchSlop = 0
private var initialX = 0f
private var initialY = 0f
private val parentViewPager: ViewPager2?
get() {
var v: View? = parent as? View
@ -51,12 +52,14 @@ class NestedScrollableHost : ConstraintLayout {
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
init {
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
mDetector = GestureDetectorCompat(context, MyGestureListener())
}
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
@ -69,35 +72,49 @@ class NestedScrollableHost : ConstraintLayout {
}
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
handleInterceptTouchEvent(e)
mDetector.onTouchEvent(e)
return super.onInterceptTouchEvent(e)
}
private fun handleInterceptTouchEvent(e: MotionEvent) {
val orientation = parentViewPager?.orientation ?: return
private inner class MyGestureListener : GestureDetector.SimpleOnGestureListener() {
if (e.action == MotionEvent.ACTION_DOWN) {
initialX = e.x
initialY = e.y
doubleTapCallback?.invoke(true)
}
// Early return if child can't scroll in same direction as parent
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return
}
override fun onDown(e: MotionEvent): Boolean {
val orientation = parentViewPager?.orientation ?: return true
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
return true
}
if (e.action == MotionEvent.ACTION_DOWN) {
parent.requestDisallowInterceptTouchEvent(true)
} else if (e.action == MotionEvent.ACTION_MOVE) {
val dx = e.x - initialX
val dy = e.y - initialY
return true
}
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
// assuming ViewPager2 touch-slop is 2x touch-slop of child
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f/ touchSlopModifier else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f/touchSlopModifier
if(dx.absoluteValue * .5f > touchSlop || scaledDy > touchSlop) doubleTapCallback?.invoke(false)
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f / touchSlopModifier else 1f
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f / touchSlopModifier
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
var clicked = false
binding.postPagerHost.doubleTapCallback = {
if(!it) clicked = false
else lifecycleScope.launchWhenCreated {
lifecycleScope.launchWhenCreated {
//Check that the post isn't hidden
if(binding.sensitiveWarning.visibility == View.GONE) {
//Check for double click
if(clicked) {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
if (binding.liker.isChecked) {
// Button is active, unlike
binding.liker.isChecked = false
unLikePostCall(api)
} else {
// Button is inactive, like
binding.liker.playAnimation()
binding.liker.isChecked = true
binding.likeAnimation.animateView()
likePostCall(api)
}
if (binding.sensitiveWarning.visibility == View.GONE) {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
if (binding.liker.isChecked) {
// Button is active, unlike
binding.liker.isChecked = false
unLikePostCall(api)
} else {
clicked = true
//Reset clicked to false after 500ms
binding.postPager.handler.postDelayed(fun() { clicked = false }, 500)
// Button is inactive, like
binding.liker.playAnimation()
binding.liker.isChecked = true
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
}
}