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.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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