From c7d727356b500744efde2632af3257fc0051cf86 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 24 Sep 2023 22:59:56 +0200 Subject: [PATCH 1/2] feat: show large, zoomable channel avatar on click --- app/src/main/AndroidManifest.xml | 3 + .../github/libretube/constants/IntentData.kt | 1 + .../ui/activities/ZoomableImageActivity.kt | 25 +++ .../libretube/ui/fragments/ChannelFragment.kt | 11 + .../libretube/ui/views/ZoomableImageView.kt | 210 ++++++++++++++++++ .../res/layout/activity_zoomable_image.xml | 13 ++ 6 files changed, 263 insertions(+) create mode 100644 app/src/main/java/com/github/libretube/ui/activities/ZoomableImageActivity.kt create mode 100644 app/src/main/java/com/github/libretube/ui/views/ZoomableImageView.kt create mode 100644 app/src/main/res/layout/activity_zoomable_image.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0abecd057..41d01e6a4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,6 +51,9 @@ android:name=".ui.activities.HelpActivity" android:label="@string/settings" /> + + + mScaleDetector.onTouchEvent(event) + bitmapMatrix.getValues(m) + val x = m[Matrix.MTRANS_X] + val y = m[Matrix.MTRANS_Y] + val curr = PointF(event.x, event.y) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + last[event.x] = event.y + start.set(last) + mode = DRAG + } + + MotionEvent.ACTION_POINTER_DOWN -> { + last[event.x] = event.y + start.set(last) + mode = ZOOM + } + + MotionEvent.ACTION_MOVE -> //if the mode is ZOOM or + //if the mode is DRAG and already zoomed + if (mode == ZOOM || mode == DRAG && saveScale > minScale) { + var deltaX = curr.x - last.x // x difference + var deltaY = curr.y - last.y // y difference + val scaleWidth = (origWidth * saveScale).roundToInt() + .toFloat() // width after applying current scale + val scaleHeight = (origHeight * saveScale).roundToInt() + .toFloat() // height after applying current scale + //if scaleWidth is smaller than the views width + //in other words if the image width fits in the view + //limit left and right movement + if (scaleWidth < width) { + deltaX = 0f + if (y + deltaY > 0) deltaY = -y else if (y + deltaY < -bottom) deltaY = + -(y + bottom) + } else if (scaleHeight < height) { + deltaY = 0f + if (x + deltaX > 0) deltaX = -x else if (x + deltaX < -right) deltaX = + -(x + right) + } else { + if (x + deltaX > 0) deltaX = -x else if (x + deltaX < -right) deltaX = + -(x + right) + if (y + deltaY > 0) deltaY = -y else if (y + deltaY < -bottom) deltaY = + -(y + bottom) + } + //move the image with the matrix + bitmapMatrix.postTranslate(deltaX, deltaY) + //set the last touch location to the current + last[curr.x] = curr.y + } + + 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() + } + + MotionEvent.ACTION_POINTER_UP -> mode = NONE + } + imageMatrix = bitmapMatrix + invalidate() + true + } + } + + override fun setImageBitmap(bm: Bitmap) { + super.setImageBitmap(bm) + bmWidth = bm.width.toFloat() + bmHeight = bm.height.toFloat() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + width = MeasureSpec.getSize(widthMeasureSpec).toFloat() + height = MeasureSpec.getSize(heightMeasureSpec).toFloat() + // Fit to screen. + val scaleX = width / bmWidth + val scaleY = height / bmHeight + val scale = scaleX.coerceAtMost(scaleY) + bitmapMatrix.setScale(scale, scale) + imageMatrix = bitmapMatrix + saveScale = 1f + + // Center the image + redundantYSpace = height - scale * bmHeight + redundantXSpace = width - scale * bmWidth + redundantYSpace /= 2f + redundantXSpace /= 2f + bitmapMatrix.postTranslate(redundantXSpace, redundantYSpace) + origWidth = width - 2 * redundantXSpace + origHeight = height - 2 * redundantYSpace + right = width * saveScale - width - 2 * redundantXSpace * saveScale + bottom = height * saveScale - height - 2 * redundantYSpace * saveScale + imageMatrix = bitmapMatrix + } + + 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 + } + right = width * saveScale - width - 2 * redundantXSpace * saveScale + bottom = height * saveScale - height - 2 * redundantYSpace * saveScale + if (origWidth * saveScale <= width || origHeight * saveScale <= height) { + bitmapMatrix.postScale(mScaleFactor, mScaleFactor, width / 2, height / 2) + if (mScaleFactor < 1) { + bitmapMatrix.getValues(m) + val x = m[Matrix.MTRANS_X] + val y = m[Matrix.MTRANS_Y] + + if ((origWidth * saveScale).roundToInt() < width) { + if (y < -bottom) bitmapMatrix.postTranslate( + 0f, + -(y + bottom) + ) else if (y > 0) bitmapMatrix.postTranslate(0f, -y) + } else { + if (x < -right) bitmapMatrix.postTranslate( + -(x + right), + 0f + ) else if (x > 0) bitmapMatrix.postTranslate(-x, 0f) + } + } + } else { + bitmapMatrix.postScale(mScaleFactor, mScaleFactor, detector.focusX, detector.focusY) + bitmapMatrix.getValues(m) + val x = m[Matrix.MTRANS_X] + val y = m[Matrix.MTRANS_Y] + + if (mScaleFactor < 1) { + if (x < -right) bitmapMatrix.postTranslate( + -(x + right), + 0f + ) else if (x > 0) bitmapMatrix.postTranslate(-x, 0f) + if (y < -bottom) bitmapMatrix.postTranslate( + 0f, + -(y + bottom) + ) else if (y > 0) bitmapMatrix.postTranslate(0f, -y) + } + } + return true + } + } + + companion object { + const val NONE = 0 + const val DRAG = 1 + const val ZOOM = 2 + const val CLICK = 3 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_zoomable_image.xml b/app/src/main/res/layout/activity_zoomable_image.xml new file mode 100644 index 000000000..1478ac7ce --- /dev/null +++ b/app/src/main/res/layout/activity_zoomable_image.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file From 8e7d85ecdf4ccf806ecb34950e9d1e977af231b8 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 24 Sep 2023 23:09:58 +0200 Subject: [PATCH 2/2] feat: zoomable preview of channel banner --- .../github/libretube/constants/IntentData.kt | 2 +- .../libretube/helpers/NavigationHelper.kt | 10 ++++++++++ .../ui/activities/ZoomableImageActivity.kt | 19 +++++++++++++++++-- .../libretube/ui/fragments/ChannelFragment.kt | 12 ++++++------ .../res/layout/activity_zoomable_image.xml | 8 +++++++- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/github/libretube/constants/IntentData.kt b/app/src/main/java/com/github/libretube/constants/IntentData.kt index 1928175a9..a2efc7d68 100644 --- a/app/src/main/java/com/github/libretube/constants/IntentData.kt +++ b/app/src/main/java/com/github/libretube/constants/IntentData.kt @@ -31,5 +31,5 @@ object IntentData { const val streamItem = "streamItem" const val url = "url" const val videoStats = "videoStats" - const val bitmap = "bitmap" + const val bitmapUrl = "bitmapUrl" } diff --git a/app/src/main/java/com/github/libretube/helpers/NavigationHelper.kt b/app/src/main/java/com/github/libretube/helpers/NavigationHelper.kt index 81c1665dd..9361582c2 100644 --- a/app/src/main/java/com/github/libretube/helpers/NavigationHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/NavigationHelper.kt @@ -17,6 +17,7 @@ import com.github.libretube.constants.PreferenceKeys import com.github.libretube.enums.PlaylistType import com.github.libretube.extensions.toID import com.github.libretube.parcelable.PlayerData +import com.github.libretube.ui.activities.ZoomableImageActivity import com.github.libretube.ui.fragments.AudioPlayerFragment import com.github.libretube.ui.fragments.PlayerFragment import com.github.libretube.ui.views.SingleViewTouchableMotionLayout @@ -110,6 +111,15 @@ object NavigationHelper { } } + /** + * Open a large, zoomable image preview + */ + fun openImagePreview(context: Context, url: String) { + val intent = Intent(context, ZoomableImageActivity::class.java) + intent.putExtra(IntentData.bitmapUrl, url) + context.startActivity(intent) + } + /** * Needed due to different MainActivity Aliases because of the app icons */ diff --git a/app/src/main/java/com/github/libretube/ui/activities/ZoomableImageActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/ZoomableImageActivity.kt index c68d4029d..8385c9553 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/ZoomableImageActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/ZoomableImageActivity.kt @@ -4,9 +4,16 @@ import android.annotation.SuppressLint import android.graphics.Bitmap import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.github.libretube.constants.IntentData import com.github.libretube.databinding.ActivityZoomableImageBinding import com.github.libretube.extensions.parcelableExtra +import com.github.libretube.helpers.ImageHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * An activity that allows you to zoom and rotate an image @@ -19,7 +26,15 @@ class ZoomableImageActivity : AppCompatActivity() { val binding = ActivityZoomableImageBinding.inflate(layoutInflater) setContentView(binding.root) - val bitmap: Bitmap = intent.parcelableExtra(IntentData.bitmap)!! - binding.imageView.setImageBitmap(bitmap) + lifecycleScope.launch(Dispatchers.IO) { + val bitmapUrl = intent.getStringExtra(IntentData.bitmapUrl)!! + val bitmap = ImageHelper.getImage(this@ZoomableImageActivity, bitmapUrl) ?: return@launch + + withContext(Dispatchers.Main) { + binding.imageView.setImageBitmap(bitmap) + binding.progress.isGone = true + binding.imageView.isVisible = true + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt index 256dc95ba..11a856870 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt @@ -27,6 +27,7 @@ import com.github.libretube.extensions.TAG import com.github.libretube.extensions.formatShort import com.github.libretube.extensions.toID import com.github.libretube.helpers.ImageHelper +import com.github.libretube.helpers.NavigationHelper import com.github.libretube.obj.ChannelTabs import com.github.libretube.obj.ShareData import com.github.libretube.ui.activities.ZoomableImageActivity @@ -201,12 +202,11 @@ class ChannelFragment : Fragment() { ImageHelper.loadImage(response.avatarUrl, binding.channelImage) binding.channelImage.setOnClickListener { - lifecycleScope.launch(Dispatchers.IO) onclick@ { - val image = ImageHelper.getImage(requireContext(), response.avatarUrl) ?: return@onclick - val intent = Intent(context, ZoomableImageActivity::class.java) - intent.putExtra(IntentData.bitmap, image) - context?.startActivity(intent) - } + NavigationHelper.openImagePreview(requireContext(), response.avatarUrl ?: return@setOnClickListener) + } + + binding.channelBanner.setOnClickListener { + NavigationHelper.openImagePreview(requireContext(), response.bannerUrl ?: return@setOnClickListener) } // recyclerview of the videos by the channel diff --git a/app/src/main/res/layout/activity_zoomable_image.xml b/app/src/main/res/layout/activity_zoomable_image.xml index 1478ac7ce..ee826c44f 100644 --- a/app/src/main/res/layout/activity_zoomable_image.xml +++ b/app/src/main/res/layout/activity_zoomable_image.xml @@ -3,11 +3,17 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + android:layout_gravity="center" + android:visibility="gone" /> \ No newline at end of file