mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 08:20:32 +05:30
Merge pull request #4844 from Bnyro/image-preview
feat: zoomable preview of channel avatar and banner
This commit is contained in:
commit
c3cd1aa2ee
@ -51,6 +51,9 @@
|
|||||||
android:name=".ui.activities.HelpActivity"
|
android:name=".ui.activities.HelpActivity"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.activities.ZoomableImageActivity" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.activities.AddToQueueActivity"
|
android:name=".ui.activities.AddToQueueActivity"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
@ -31,4 +31,5 @@ object IntentData {
|
|||||||
const val streamItem = "streamItem"
|
const val streamItem = "streamItem"
|
||||||
const val url = "url"
|
const val url = "url"
|
||||||
const val videoStats = "videoStats"
|
const val videoStats = "videoStats"
|
||||||
|
const val bitmapUrl = "bitmapUrl"
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import com.github.libretube.constants.PreferenceKeys
|
|||||||
import com.github.libretube.enums.PlaylistType
|
import com.github.libretube.enums.PlaylistType
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
import com.github.libretube.parcelable.PlayerData
|
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.AudioPlayerFragment
|
||||||
import com.github.libretube.ui.fragments.PlayerFragment
|
import com.github.libretube.ui.fragments.PlayerFragment
|
||||||
import com.github.libretube.ui.views.SingleViewTouchableMotionLayout
|
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
|
* Needed due to different MainActivity Aliases because of the app icons
|
||||||
*/
|
*/
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package com.github.libretube.ui.fragments
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
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.formatShort
|
||||||
import com.github.libretube.extensions.toID
|
import com.github.libretube.extensions.toID
|
||||||
import com.github.libretube.helpers.ImageHelper
|
import com.github.libretube.helpers.ImageHelper
|
||||||
|
import com.github.libretube.helpers.NavigationHelper
|
||||||
import com.github.libretube.obj.ChannelTabs
|
import com.github.libretube.obj.ChannelTabs
|
||||||
import com.github.libretube.obj.ShareData
|
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.SearchAdapter
|
||||||
import com.github.libretube.ui.adapters.VideosAdapter
|
import com.github.libretube.ui.adapters.VideosAdapter
|
||||||
import com.github.libretube.ui.dialogs.ShareDialog
|
import com.github.libretube.ui.dialogs.ShareDialog
|
||||||
@ -198,6 +201,14 @@ class ChannelFragment : Fragment() {
|
|||||||
ImageHelper.loadImage(response.bannerUrl, binding.channelBanner)
|
ImageHelper.loadImage(response.bannerUrl, binding.channelBanner)
|
||||||
ImageHelper.loadImage(response.avatarUrl, binding.channelImage)
|
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
|
// recyclerview of the videos by the channel
|
||||||
channelAdapter = VideosAdapter(
|
channelAdapter = VideosAdapter(
|
||||||
response.relatedStreams.toMutableList(),
|
response.relatedStreams.toMutableList(),
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
19
app/src/main/res/layout/activity_zoomable_image.xml
Normal file
19
app/src/main/res/layout/activity_zoomable_image.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user