mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 00:10: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:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.activities.ZoomableImageActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.activities.AddToQueueActivity"
|
||||
android:enabled="true"
|
||||
|
@ -31,4 +31,5 @@ object IntentData {
|
||||
const val streamItem = "streamItem"
|
||||
const val url = "url"
|
||||
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.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
|
||||
*/
|
||||
|
@ -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
|
||||
|
||||
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(),
|
||||
|
@ -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