aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt234
1 files changed, 234 insertions, 0 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt
new file mode 100644
index 00000000..eaa4e698
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt
@@ -0,0 +1,234 @@
+package com.pitchedapps.frost.views
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.PointF
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import ca.allanwang.kau.utils.dpToPx
+import ca.allanwang.kau.utils.scaleXY
+import com.devbrackets.android.exomedia.ui.widget.VideoView
+import com.pitchedapps.frost.utils.L
+
+/**
+ * Created by Allan Wang on 2017-10-13.
+ *
+ * VideoView with scalability
+ * Parent must have layout with both height & width as match_parent
+ */
+class FrostVideoView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : VideoView(context, attrs, defStyleAttr) {
+
+ /**
+ * Shortcut for actual video view
+ */
+ private inline val v
+ get() = videoViewImpl
+
+ var backgroundView: View? = null
+ var onFinishedListener: () -> Unit = {}
+ var viewerContract: FrostVideoViewerContract? = null
+
+ private val videoDimensions = PointF(0f, 0f)
+
+ companion object {
+
+ /**
+ * Padding between minimized video and the parent borders
+ * Note that this is double the actual padding
+ * as we are calculating then dividing by 2
+ */
+ private val MINIMIZED_PADDING = 10.dpToPx
+ private val SWIPE_TO_CLOSE_HORIZONTAL_THRESHOLD = 2f.dpToPx
+ private val SWIPE_TO_CLOSE_VERTICAL_THRESHOLD = 5f.dpToPx
+ private val SWIPE_TO_CLOSE_OFFSET_THRESHOLD = 75f.dpToPx
+ val ANIMATION_DURATION = 300L
+ private val FAST_ANIMATION_DURATION = 100L
+ }
+
+ private var upperMinimizedX = 0f
+ private var upperMinimizedY = 0f
+
+ var isExpanded: Boolean = true
+ set(value) {
+ if (field == value) return
+ if (videoDimensions.x <= 0f || videoDimensions.y <= 0f)
+ return L.d("Attempted to toggle video expansion when points have not been finalized")
+ field = value
+ if (field) {
+ animate().scaleXY(1f).translationX(0f).translationY(0f).setDuration(ANIMATION_DURATION).withStartAction {
+ backgroundView?.animate()?.alpha(1f)?.setDuration(ANIMATION_DURATION)
+ viewerContract?.onFade(1f, ANIMATION_DURATION)
+ }
+ } else {
+ hideControls()
+ val height = height
+ val width = width
+ val scale = Math.min(height / 4f / videoDimensions.y, width / 2.3f / videoDimensions.x)
+ val desiredHeight = scale * videoDimensions.y
+ val desiredWidth = scale * videoDimensions.x
+ val translationX = (width - MINIMIZED_PADDING - desiredWidth) / 2
+ val translationY = (height - MINIMIZED_PADDING - desiredHeight) / 2
+ upperMinimizedX = width - desiredWidth - MINIMIZED_PADDING
+ upperMinimizedY = height - desiredHeight - MINIMIZED_PADDING
+ animate().scaleXY(scale).translationX(translationX).translationY(translationY).setDuration(ANIMATION_DURATION).withStartAction {
+ backgroundView?.animate()?.alpha(0f)?.setDuration(ANIMATION_DURATION)
+ viewerContract?.onFade(0f, ANIMATION_DURATION)
+ }
+ }
+ }
+
+ init {
+ setOnPreparedListener {
+ start()
+ showControls()
+ }
+ setOnCompletionListener {
+ viewerContract?.onVideoComplete()
+ }
+ setOnTouchListener(FrameTouchListener(context))
+ v.setOnTouchListener(VideoTouchListener(context))
+ setOnVideoSizedChangedListener { intrinsicWidth, intrinsicHeight ->
+ val ratio = Math.min(width.toFloat() / intrinsicWidth, height.toFloat() / intrinsicHeight.toFloat())
+ videoDimensions.set(ratio * intrinsicWidth, ratio * intrinsicHeight)
+ }
+ }
+
+ fun jumpToStart() {
+ pause()
+ videoControls?.hide()
+ v.seekTo(0)
+ videoControls?.finishLoading()
+ }
+
+ private fun hideControls() {
+ if (videoControls?.isVisible == true)
+ videoControls?.hide()
+ }
+
+ private fun toggleControls() {
+ if (videoControls?.isVisible == true)
+ hideControls()
+ else
+ showControls()
+ }
+
+ fun shouldParentAcceptTouch(ev: MotionEvent): Boolean {
+ if (isExpanded) return true
+ return ev.x >= upperMinimizedX && ev.y >= upperMinimizedY
+ }
+
+ fun destroy() {
+ stopPlayback()
+ if (alpha > 0f)
+ animate().alpha(0f).setDuration(FAST_ANIMATION_DURATION).withEndAction { onFinishedListener() }.withStartAction {
+ viewerContract?.onFade(0f, FAST_ANIMATION_DURATION)
+ }.start()
+ else
+ onFinishedListener()
+ }
+
+ private fun onHorizontalSwipe(offset: Float) {
+ val alpha = Math.max((1f - Math.abs(offset / SWIPE_TO_CLOSE_OFFSET_THRESHOLD)) * 0.5f + 0.5f, 0f)
+ this.alpha = alpha
+ }
+
+ /*
+ * -------------------------------------------------------------------
+ * Touch Listeners
+ * -------------------------------------------------------------------
+ */
+
+ private inner class FrameTouchListener(context: Context) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
+
+ private val gestureDetector: GestureDetector = GestureDetector(context, this)
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouch(view: View, event: MotionEvent): Boolean {
+ if (!isExpanded) return false
+ gestureDetector.onTouchEvent(event)
+ return true
+ }
+
+ override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
+ if (viewerContract?.onSingleTapConfirmed(event) != true)
+ toggleControls()
+ return true
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ isExpanded = !isExpanded
+ return true
+ }
+ }
+
+ /**
+ * Monitors the view click events to show and hide the video controls if they have been specified.
+ */
+ private inner class VideoTouchListener(context: Context) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
+
+ private val gestureDetector: GestureDetector = GestureDetector(context, this)
+ private val downLoc = PointF()
+ private var baseSwipeX = -1f
+ private var baseTranslateX = -1f
+ private var checkForDismiss = true
+ private var onSwipe = false
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouch(view: View, event: MotionEvent): Boolean {
+ gestureDetector.onTouchEvent(event)
+ when (event.actionMasked) {
+ MotionEvent.ACTION_DOWN -> {
+ checkForDismiss = !isExpanded
+ onSwipe = false
+ downLoc.x = event.rawX
+ downLoc.y = event.rawY
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (onSwipe) {
+ val dx = baseSwipeX - event.rawX
+ translationX = baseTranslateX - dx
+ onHorizontalSwipe(dx)
+ } else if (checkForDismiss) {
+ if (Math.abs(event.rawY - downLoc.y) > SWIPE_TO_CLOSE_VERTICAL_THRESHOLD)
+ checkForDismiss = false
+ else if (Math.abs(event.rawX - downLoc.x) > SWIPE_TO_CLOSE_HORIZONTAL_THRESHOLD) {
+ onSwipe = true
+ baseSwipeX = event.rawX
+ baseTranslateX = translationX
+ }
+ }
+ }
+ MotionEvent.ACTION_UP -> {
+ if (onSwipe) {
+ if (Math.abs(baseSwipeX - event.rawX) > SWIPE_TO_CLOSE_OFFSET_THRESHOLD)
+ destroy()
+ else
+ animate().translationX(baseTranslateX).setDuration(FAST_ANIMATION_DURATION).withStartAction {
+ animate().alpha(1f)
+ }
+ }
+ }
+ }
+ return true
+ }
+
+ override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
+ if (viewerContract?.onSingleTapConfirmed(event) == true) return true
+ if (!isExpanded) {
+ isExpanded = true
+ return true
+ }
+ toggleControls()
+ return true
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ isExpanded = !isExpanded
+ return true
+ }
+ }
+} \ No newline at end of file