Merge pull request #4844 from Bnyro/image-preview

feat: zoomable preview of channel avatar and banner
This commit is contained in:
Bnyro 2023-09-25 10:03:21 +02:00 committed by GitHub
commit c3cd1aa2ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 294 additions and 0 deletions

View File

@ -51,6 +51,9 @@
android:name=".ui.activities.HelpActivity"
android:label="@string/settings" />
<activity
android:name=".ui.activities.ZoomableImageActivity" />
<activity
android:name=".ui.activities.AddToQueueActivity"
android:enabled="true"

View File

@ -31,4 +31,5 @@ object IntentData {
const val streamItem = "streamItem"
const val url = "url"
const val videoStats = "videoStats"
const val bitmapUrl = "bitmapUrl"
}

View File

@ -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
*/

View File

@ -0,0 +1,40 @@
package com.github.libretube.ui.activities
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
*/
class ZoomableImageActivity : AppCompatActivity() {
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityZoomableImageBinding.inflate(layoutInflater)
setContentView(binding.root)
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
}
}
}
}

View File

@ -1,5 +1,6 @@
package com.github.libretube.ui.fragments
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@ -26,8 +27,10 @@ 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
import com.github.libretube.ui.adapters.SearchAdapter
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.dialogs.ShareDialog
@ -198,6 +201,14 @@ class ChannelFragment : Fragment() {
ImageHelper.loadImage(response.bannerUrl, binding.channelBanner)
ImageHelper.loadImage(response.avatarUrl, binding.channelImage)
binding.channelImage.setOnClickListener {
NavigationHelper.openImagePreview(requireContext(), response.avatarUrl ?: return@setOnClickListener)
}
binding.channelBanner.setOnClickListener {
NavigationHelper.openImagePreview(requireContext(), response.bannerUrl ?: return@setOnClickListener)
}
// recyclerview of the videos by the channel
channelAdapter = VideosAdapter(
response.relatedStreams.toMutableList(),

View File

@ -0,0 +1,210 @@
package com.github.libretube.ui.views
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Matrix
import android.graphics.PointF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
import androidx.appcompat.widget.AppCompatImageView
import kotlin.math.abs
import kotlin.math.roundToInt
/**
* An image view that allows zooming the image inside
* credits: https://stackoverflow.com/posts/6650473
*/
class ZoomableImageView(context: Context, attr: AttributeSet?) : AppCompatImageView(context, attr) {
var bitmapMatrix = Matrix()
var mode = NONE
var last = PointF()
var start = PointF()
var minScale = 1f
var maxScale = 4f
var m: FloatArray
var redundantXSpace = 0f
var redundantYSpace = 0f
var width = 0f
var height = 0f
var saveScale = 1f
var right = 0f
var bottom = 0f
var origWidth = 0f
var origHeight = 0f
private var bmWidth = 0f
private var bmHeight = 0f
private var mScaleDetector: ScaleGestureDetector
init {
super.setClickable(true)
mScaleDetector = ScaleGestureDetector(context, ScaleListener())
bitmapMatrix.setTranslate(1f, 1f)
m = FloatArray(9)
imageMatrix = bitmapMatrix
scaleType = ScaleType.MATRIX
setOnTouchListener { _, event ->
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
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<com.github.libretube.ui.views.ZoomableImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>