Detect single tap, create zoomable imageview
This commit is contained in:
parent
114f642181
commit
52cefe63aa
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue