diff --git a/app/src/main/java/org/pixeldroid/app/posts/NestedScrollableHost.kt b/app/src/main/java/org/pixeldroid/app/posts/NestedScrollableHost.kt index fa0157d7..fb2e84e4 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/NestedScrollableHost.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/NestedScrollableHost.kt @@ -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) } } diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt index a1127538..72b18cb3 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt @@ -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) } } } diff --git a/app/src/main/java/org/pixeldroid/app/posts/TouchImageView.kt b/app/src/main/java/org/pixeldroid/app/posts/TouchImageView.kt new file mode 100644 index 00000000..63f96e72 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/posts/TouchImageView.kt @@ -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 + } +} \ No newline at end of file