aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/views
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-10-24 23:29:55 -0400
committerGitHub <noreply@github.com>2017-10-24 23:29:55 -0400
commitc2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66 (patch)
treea54665bb873b650b8f6f03b76cd59456ef79e296 /app/src/main/kotlin/com/pitchedapps/frost/views
parent64dbf74b7a44a25f41ed7ff2ebfa11db0bc91769 (diff)
downloadfrost-c2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66.tar.gz
frost-c2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66.tar.bz2
frost-c2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66.zip
WIP: Feature/pip video 2 (#405)
* Add dependency * Test new video view * Add initial video bindings * Implement drag to dismiss * Begin initial integration * Fix typo * Fix up url formatter * Update changelog * Create first fully integrated video build * Update translations * Update translations 2
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/views')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt234
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt121
2 files changed, 355 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
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt
new file mode 100644
index 00000000..0f7d49e8
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt
@@ -0,0 +1,121 @@
+package com.pitchedapps.frost.views
+
+import android.content.Context
+import android.graphics.Color
+import android.net.Uri
+import android.support.constraint.ConstraintLayout
+import android.support.v7.widget.Toolbar
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import ca.allanwang.kau.utils.*
+import com.mikepenz.google_material_typeface_library.GoogleMaterial
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.facebook.formattedFbUrl
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.Prefs
+import com.pitchedapps.frost.utils.frostDownload
+
+/**
+ * Created by Allan Wang on 2017-10-13.
+ */
+class FrostVideoViewer @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr), FrostVideoViewerContract {
+
+ override fun onFade(alpha: Float, duration: Long) {
+ toolbar.animate().alpha(alpha).setDuration(duration)
+ }
+
+ override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
+ if (restarter.isVisible) {
+ restarter.performClick()
+ return true
+ }
+ return false
+ }
+
+ override fun onVideoComplete() {
+ video.jumpToStart()
+ restarter.fadeIn()
+ }
+
+ val container: ViewGroup by bindView(R.id.video_container)
+ val toolbar: Toolbar by bindView(R.id.video_toolbar)
+ val background: View by bindView(R.id.video_background)
+ val video: FrostVideoView by bindView(R.id.video)
+ val restarter: ImageView by bindView(R.id.video_restart)
+
+
+ companion object {
+ /**
+ * Simplified binding to add video to layout, and remove it when finished
+ * This is under the assumption that the container allows for overlays,
+ * such as a FrameLayout
+ */
+ inline fun showVideo(container: ViewGroup, url: String, crossinline onFinish: () -> Unit): FrostVideoViewer {
+ val videoViewer = FrostVideoViewer(container.context)
+ container.addView(videoViewer)
+ videoViewer.bringToFront()
+ L.d("Create video view", url)
+ videoViewer.setVideo(url)
+ videoViewer.video.onFinishedListener = { container.removeView(videoViewer); onFinish() }
+ return videoViewer
+ }
+ }
+
+ init {
+ inflate(R.layout.view_video, true)
+ alpha = 0f
+ background.setBackgroundColor(if (Prefs.bgColor.isColorDark) Prefs.bgColor.withMinAlpha(200) else Color.BLACK)
+ video.backgroundView = background
+ video.viewerContract = this
+ video.pause()
+ toolbar.inflateMenu(R.menu.menu_video)
+ toolbar.setBackgroundColor(Prefs.headerColor)
+ context.setMenuIcons(toolbar.menu, Prefs.iconColor,
+ R.id.action_pip to GoogleMaterial.Icon.gmd_picture_in_picture_alt,
+ R.id.action_download to GoogleMaterial.Icon.gmd_file_download
+ )
+ toolbar.setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.action_pip -> video.isExpanded = false
+ R.id.action_download -> context.frostDownload(video.videoUri)
+ }
+ true
+ }
+ restarter.gone().setIcon(GoogleMaterial.Icon.gmd_replay, 64)
+ restarter.setOnClickListener {
+ video.restart()
+ restarter.fadeOut { restarter.gone() }
+ }
+ }
+
+ fun setVideo(url: String) {
+ animate().alpha(1f).setDuration(FrostVideoView.ANIMATION_DURATION).start()
+ video.setVideoURI(Uri.parse(url.formattedFbUrl))
+ }
+
+ /**
+ * Handle back presses
+ * returns true if consumed, false otherwise
+ */
+ fun onBackPressed(): Boolean {
+ if (video.isExpanded) {
+ video.isExpanded = false
+ return true
+ }
+ return false
+ }
+
+ fun pause() = video.pause()
+
+}
+
+interface FrostVideoViewerContract {
+ fun onSingleTapConfirmed(event: MotionEvent): Boolean
+ fun onFade(alpha: Float, duration: Long)
+ fun onVideoComplete()
+} \ No newline at end of file