mirror of https://codeberg.org/tom79/Fedilab
parent
596c546cce
commit
c778c71306
@ -0,0 +1 @@
|
||||
/build
|
@ -0,0 +1,38 @@
|
||||
apply plugin: 'com.android.library'
|
||||
android {
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
consumerProguardFiles 'consumer-rules.pro'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
namespace 'com.github.vkay94.dtpv'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation "androidx.core:core-ktx:1.12.0"
|
||||
|
||||
implementation "androidx.media3:media3-exoplayer:1.2.1"
|
||||
implementation "androidx.media3:media3-exoplayer-dash:1.2.1"
|
||||
implementation "androidx.media3:media3-ui:1.2.1"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.vkay94.dtpv" />
|
@ -0,0 +1,222 @@
|
||||
package com.github.vkay94.dtpv
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.media3.ui.PlayerView
|
||||
|
||||
|
||||
/**
|
||||
* Custom player class for Double-Tapping listening
|
||||
*/
|
||||
open class DoubleTapPlayerView @JvmOverloads constructor(
|
||||
context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : PlayerView(context!!, attrs, defStyleAttr) {
|
||||
|
||||
private val gestureDetector: GestureDetectorCompat
|
||||
private val gestureListener: DoubleTapGestureListener = DoubleTapGestureListener(rootView)
|
||||
|
||||
private var controller: PlayerDoubleTapListener? = null
|
||||
get() = gestureListener.controls
|
||||
set(value) {
|
||||
gestureListener.controls = value
|
||||
field = value
|
||||
}
|
||||
|
||||
private var controllerRef: Int = -1
|
||||
|
||||
init {
|
||||
gestureDetector = GestureDetectorCompat(context, gestureListener)
|
||||
|
||||
// Check whether controller is set through XML
|
||||
attrs?.let {
|
||||
val a = context?.obtainStyledAttributes(attrs, R.styleable.DoubleTapPlayerView, 0,0)
|
||||
controllerRef = a?.getResourceId(R.styleable.DoubleTapPlayerView_dtpv_controller, -1) ?: -1
|
||||
|
||||
a?.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this field is set to `true` this view will handle double tapping, otherwise it will
|
||||
* handle touches the same way as the original [PlayerView][com.google.android.exoplayer2.ui.PlayerView] does
|
||||
*/
|
||||
var isDoubleTapEnabled = true
|
||||
|
||||
/**
|
||||
* Time window a double tap is active, so a followed tap is calling a gesture detector
|
||||
* method instead of normal tap (see [PlayerView.onTouchEvent])
|
||||
*/
|
||||
var doubleTapDelay: Long = 700
|
||||
get() = gestureListener.doubleTapDelay
|
||||
set(value) {
|
||||
gestureListener.doubleTapDelay = value
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the [PlayerDoubleTapListener] which handles the gesture callbacks.
|
||||
*
|
||||
* Primarily used for [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay]
|
||||
*/
|
||||
fun controller(controller: PlayerDoubleTapListener) = apply { this.controller = controller }
|
||||
|
||||
/**
|
||||
* Returns the current state of double tapping.
|
||||
*/
|
||||
fun isInDoubleTapMode(): Boolean = gestureListener.isDoubleTapping
|
||||
|
||||
/**
|
||||
* Resets the timeout to keep in double tap mode.
|
||||
*
|
||||
* Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called
|
||||
* from outside if the double tap is customized / overridden to detect ongoing taps
|
||||
*/
|
||||
fun keepInDoubleTapMode() {
|
||||
gestureListener.keepInDoubleTapMode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished]
|
||||
*/
|
||||
fun cancelInDoubleTapMode() {
|
||||
gestureListener.cancelInDoubleTapMode()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
if (isDoubleTapEnabled) {
|
||||
gestureDetector.onTouchEvent(ev)
|
||||
|
||||
// Do not trigger original behavior when double tapping
|
||||
// otherwise the controller would show/hide - it would flack
|
||||
return true
|
||||
}
|
||||
return super.onTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
// If the PlayerView is set by XML then call the corresponding setter method
|
||||
if (controllerRef != -1) {
|
||||
try {
|
||||
val view = (this.parent as View).findViewById(controllerRef) as View
|
||||
if (view is PlayerDoubleTapListener) {
|
||||
controller(view)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.e("DoubleTapPlayerView",
|
||||
"controllerRef is either invalid or not PlayerDoubleTapListener: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gesture Listener for double tapping
|
||||
*
|
||||
* For more information which methods are called in certain situations look for
|
||||
* [GestureDetector.onTouchEvent][android.view.GestureDetector.onTouchEvent],
|
||||
* especially for ACTION_DOWN and ACTION_UP
|
||||
*/
|
||||
private class DoubleTapGestureListener(private val rootView: View) : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
private val mHandler = Handler(Looper.getMainLooper())
|
||||
private val mRunnable = Runnable {
|
||||
if (DEBUG) Log.d(TAG, "Runnable called")
|
||||
isDoubleTapping = false
|
||||
controls?.onDoubleTapFinished()
|
||||
}
|
||||
|
||||
var controls: PlayerDoubleTapListener? = null
|
||||
var isDoubleTapping = false
|
||||
var doubleTapDelay: Long = 650
|
||||
|
||||
/**
|
||||
* Resets the timeout to keep in double tap mode.
|
||||
*
|
||||
* Called once in [PlayerDoubleTapListener.onDoubleTapStarted]. Needs to be called
|
||||
* from outside if the double tap is customized / overridden to detect ongoing taps
|
||||
*/
|
||||
fun keepInDoubleTapMode() {
|
||||
isDoubleTapping = true
|
||||
mHandler.removeCallbacks(mRunnable)
|
||||
mHandler.postDelayed(mRunnable, doubleTapDelay)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels double tap mode instantly by calling [PlayerDoubleTapListener.onDoubleTapFinished]
|
||||
*/
|
||||
fun cancelInDoubleTapMode() {
|
||||
mHandler.removeCallbacks(mRunnable)
|
||||
isDoubleTapping = false
|
||||
controls?.onDoubleTapFinished()
|
||||
}
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
// Used to override the other methods
|
||||
if (isDoubleTapping) {
|
||||
controls?.onDoubleTapProgressDown(e.x, e.y)
|
||||
return true
|
||||
}
|
||||
return super.onDown(e)
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
if (isDoubleTapping) {
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapUp: isDoubleTapping = true")
|
||||
controls?.onDoubleTapProgressUp(e.x, e.y)
|
||||
return true
|
||||
}
|
||||
return super.onSingleTapUp(e)
|
||||
}
|
||||
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
// Ignore this event if double tapping is still active
|
||||
// Return true needed because this method is also called if you tap e.g. three times
|
||||
// in a row, therefore the controller would appear since the original behavior is
|
||||
// to hide and show on single tap
|
||||
if (isDoubleTapping) return true
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed: isDoubleTap = false")
|
||||
return rootView.performClick()
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||
// First tap (ACTION_DOWN) of both taps
|
||||
if (DEBUG) Log.d(TAG, "onDoubleTap")
|
||||
if (!isDoubleTapping) {
|
||||
isDoubleTapping = true
|
||||
keepInDoubleTapMode()
|
||||
controls?.onDoubleTapStarted(e.x, e.y)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
|
||||
// Second tap (ACTION_UP) of both taps
|
||||
if (e.actionMasked == MotionEvent.ACTION_UP && isDoubleTapping) {
|
||||
if (DEBUG) Log.d(
|
||||
TAG,
|
||||
"onDoubleTapEvent, ACTION_UP"
|
||||
)
|
||||
controls?.onDoubleTapProgressUp(e.x, e.y)
|
||||
return true
|
||||
}
|
||||
return super.onDoubleTapEvent(e)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = ".DTGListener"
|
||||
private var DEBUG = true
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.github.vkay94.dtpv;
|
||||
|
||||
public interface PlayerDoubleTapListener {
|
||||
|
||||
/**
|
||||
* Called when double tapping starts, after double tap gesture
|
||||
*
|
||||
* @param posX x tap position on the root view
|
||||
* @param posY y tap position on the root view
|
||||
*/
|
||||
default void onDoubleTapStarted(float posX, float posY) { }
|
||||
|
||||
/**
|
||||
* Called for each ongoing tap (also single tap) (MotionEvent#ACTION_DOWN)
|
||||
* when double tap started and still in double tap mode defined
|
||||
* by {@link DoubleTapPlayerView#getDoubleTapDelay()}
|
||||
*
|
||||
* @param posX x tap position on the root view
|
||||
* @param posY y tap position on the root view
|
||||
*/
|
||||
default void onDoubleTapProgressDown(float posX, float posY) { }
|
||||
|
||||
/**
|
||||
* Called for each ongoing tap (also single tap) (MotionEvent#ACTION_UP}
|
||||
* when double tap started and still in double tap mode defined
|
||||
* by {@link DoubleTapPlayerView#getDoubleTapDelay()}
|
||||
*
|
||||
* @param posX x tap position on the root view
|
||||
* @param posY y tap position on the root view
|
||||
*/
|
||||
default void onDoubleTapProgressUp(float posX, float posY) { }
|
||||
|
||||
/**
|
||||
* Called when {@link DoubleTapPlayerView#getDoubleTapDelay()} is over
|
||||
*/
|
||||
default void onDoubleTapFinished() { }
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.github.vkay94.dtpv
|
||||
|
||||
interface SeekListener {
|
||||
/**
|
||||
* Called when video start reached during rewinding
|
||||
*/
|
||||
fun onVideoStartReached() {}
|
||||
|
||||
/**
|
||||
* Called when video end reached during forwarding
|
||||
*/
|
||||
fun onVideoEndReached() {}
|
||||
}
|
@ -0,0 +1,509 @@
|
||||
package com.github.vkay94.dtpv.youtube
|
||||
|
||||
import android.content.Context
|
||||
import android.media.session.PlaybackState
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.*
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import androidx.media3.common.Player
|
||||
import com.github.vkay94.dtpv.DoubleTapPlayerView
|
||||
import com.github.vkay94.dtpv.PlayerDoubleTapListener
|
||||
import com.github.vkay94.dtpv.R
|
||||
import com.github.vkay94.dtpv.SeekListener
|
||||
import com.github.vkay94.dtpv.youtube.views.CircleClipTapView
|
||||
import com.github.vkay94.dtpv.youtube.views.SecondsView
|
||||
|
||||
|
||||
/**
|
||||
* Overlay for [DoubleTapPlayerView] to create a similar UI/UX experience like the official
|
||||
* YouTube Android app.
|
||||
*
|
||||
* The overlay has the typical YouTube scaling circle animation and provides some configurations
|
||||
* which can't be accomplished with the regular Android Ripple (I didn't find any options in the
|
||||
* documentation ...).
|
||||
*/
|
||||
class YouTubeOverlay(context: Context, private val attrs: AttributeSet?) :
|
||||
ConstraintLayout(context, attrs), PlayerDoubleTapListener {
|
||||
|
||||
private var rootLayout: ConstraintLayout
|
||||
private var secondsView: SecondsView
|
||||
private var circleClipTapView: CircleClipTapView
|
||||
|
||||
constructor(context: Context) : this(context, null) {
|
||||
// Hide overlay initially when added programmatically
|
||||
this.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
private var playerViewRef: Int = -1
|
||||
|
||||
// Player behaviors
|
||||
private var playerView: DoubleTapPlayerView? = null
|
||||
private var player: Player? = null
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.yt_overlay, this, true)
|
||||
|
||||
rootLayout = findViewById(R.id.root_constraint_layout)
|
||||
secondsView = findViewById(R.id.seconds_view)
|
||||
circleClipTapView = findViewById(R.id.circle_clip_tap_view)
|
||||
|
||||
// Initialize UI components
|
||||
initializeAttributes()
|
||||
secondsView.isForward = true
|
||||
changeConstraints(true)
|
||||
|
||||
// This code snippet is executed when the circle scale animation is finished
|
||||
circleClipTapView.performAtEnd = {
|
||||
performListener?.onAnimationEnd()
|
||||
|
||||
secondsView.visibility = View.INVISIBLE
|
||||
secondsView.seconds = 0
|
||||
secondsView.stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all optional XML attributes and defaults
|
||||
*/
|
||||
private fun initializeAttributes() {
|
||||
if (attrs != null) {
|
||||
val a = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.YouTubeOverlay, 0, 0)
|
||||
|
||||
// PlayerView => see onAttachToWindow
|
||||
playerViewRef = a.getResourceId(R.styleable.YouTubeOverlay_yt_playerView, -1)
|
||||
|
||||
// Durations
|
||||
animationDuration = a.getInt(
|
||||
R.styleable.YouTubeOverlay_yt_animationDuration, 650).toLong()
|
||||
|
||||
seekSeconds = a.getInt(
|
||||
R.styleable.YouTubeOverlay_yt_seekSeconds, 10)
|
||||
|
||||
iconAnimationDuration = a.getInt(
|
||||
R.styleable.YouTubeOverlay_yt_iconAnimationDuration, 750).toLong()
|
||||
|
||||
// Arc size
|
||||
arcSize = a.getDimensionPixelSize(
|
||||
R.styleable.YouTubeOverlay_yt_arcSize,
|
||||
context.resources.getDimensionPixelSize(R.dimen.dtpv_yt_arc_size)).toFloat()
|
||||
|
||||
// Colors
|
||||
tapCircleColor = a.getColor(
|
||||
R.styleable.YouTubeOverlay_yt_tapCircleColor,
|
||||
ContextCompat.getColor(context, R.color.dtpv_yt_tap_circle_color)
|
||||
)
|
||||
|
||||
circleBackgroundColor = a.getColor(
|
||||
R.styleable.YouTubeOverlay_yt_backgroundCircleColor,
|
||||
ContextCompat.getColor(context, R.color.dtpv_yt_background_circle_color)
|
||||
)
|
||||
|
||||
// Seconds TextAppearance
|
||||
textAppearance = a.getResourceId(
|
||||
R.styleable.YouTubeOverlay_yt_textAppearance,
|
||||
R.style.YTOSecondsTextAppearance)
|
||||
|
||||
// Seconds icon
|
||||
icon = a.getResourceId(
|
||||
R.styleable.YouTubeOverlay_yt_icon,
|
||||
R.drawable.ic_play_triangle
|
||||
)
|
||||
|
||||
a.recycle()
|
||||
|
||||
} else {
|
||||
// Set defaults
|
||||
arcSize = context.resources.getDimensionPixelSize(R.dimen.dtpv_yt_arc_size).toFloat()
|
||||
tapCircleColor = ContextCompat.getColor(context, R.color.dtpv_yt_tap_circle_color)
|
||||
circleBackgroundColor = ContextCompat.getColor(context, R.color.dtpv_yt_background_circle_color)
|
||||
animationDuration = 650
|
||||
iconAnimationDuration = 750
|
||||
seekSeconds = 10
|
||||
textAppearance = R.style.YTOSecondsTextAppearance
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
|
||||
// If the PlayerView is set by XML then call the corresponding setter method
|
||||
if (playerViewRef != -1)
|
||||
playerView((this.parent as View).findViewById(playerViewRef) as DoubleTapPlayerView)
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligatory call if playerView is not set via XML!
|
||||
*
|
||||
* Links the DoubleTapPlayerView to this view for recognizing the tapped position.
|
||||
*
|
||||
* @param playerView PlayerView which triggers the event
|
||||
*/
|
||||
fun playerView(playerView: DoubleTapPlayerView) = apply {
|
||||
this.playerView = playerView
|
||||
}
|
||||
|
||||
/**
|
||||
* Obligatory call! Needs to be called whenever the Player changes.
|
||||
*
|
||||
* Performs seekTo-calls on the ExoPlayer's Player instance.
|
||||
*
|
||||
* @param player PlayerView which triggers the event
|
||||
*/
|
||||
fun player(player: Player) = apply {
|
||||
this.player = player
|
||||
}
|
||||
|
||||
/*
|
||||
Properties
|
||||
*/
|
||||
|
||||
private var seekListener: SeekListener? = null
|
||||
|
||||
/**
|
||||
* Optional: Sets a listener to observe whether double tap reached the start / end of the video
|
||||
*/
|
||||
fun seekListener(listener: SeekListener) = apply {
|
||||
seekListener = listener
|
||||
}
|
||||
|
||||
private var performListener: PerformListener? = null
|
||||
|
||||
/**
|
||||
* Sets a listener to execute some code before and after the animation
|
||||
* (for example UI changes (hide and show views etc.))
|
||||
*/
|
||||
fun performListener(listener: PerformListener) = apply {
|
||||
performListener = listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward / rewind duration on a tap in seconds.
|
||||
*/
|
||||
var seekSeconds: Int = 0
|
||||
private set
|
||||
|
||||
fun seekSeconds(seconds: Int) = apply {
|
||||
seekSeconds = seconds
|
||||
}
|
||||
|
||||
/**
|
||||
* Color of the scaling circle on touch feedback.
|
||||
*/
|
||||
var tapCircleColor: Int
|
||||
get() = circleClipTapView.circleColor
|
||||
private set(value) {
|
||||
circleClipTapView.circleColor = value
|
||||
}
|
||||
|
||||
fun tapCircleColorRes(@ColorRes resId: Int) = apply {
|
||||
tapCircleColor = ContextCompat.getColor(context, resId)
|
||||
}
|
||||
|
||||
fun tapCircleColorInt(@ColorInt color: Int) = apply {
|
||||
tapCircleColor = color
|
||||
}
|
||||
|
||||
/**
|
||||
* Color of the clipped background circle
|
||||
*/
|
||||
var circleBackgroundColor: Int
|
||||
get() = circleClipTapView.circleBackgroundColor
|
||||
private set(value) {
|
||||
circleClipTapView.circleBackgroundColor = value
|
||||
}
|
||||
|
||||
fun circleBackgroundColorRes(@ColorRes resId: Int) = apply {
|
||||
circleBackgroundColor = ContextCompat.getColor(context, resId)
|
||||
}
|
||||
|
||||
fun circleBackgroundColorInt(@ColorInt color: Int) = apply {
|
||||
circleBackgroundColor = color
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration of the circle scaling animation / speed in milliseconds.
|
||||
* The overlay keeps visible until the animation finishes.
|
||||
*/
|
||||
var animationDuration: Long
|
||||
get() = circleClipTapView.animationDuration
|
||||
private set(value) {
|
||||
circleClipTapView.animationDuration = value
|
||||
}
|
||||
|
||||
fun animationDuration(duration: Long) = apply {
|
||||
animationDuration = duration
|
||||
}
|
||||
|
||||
/**
|
||||
* Size of the arc which will be clipped from the background circle.
|
||||
* The greater the value the more roundish the shape becomes
|
||||
*/
|
||||
var arcSize: Float
|
||||
get() = circleClipTapView.arcSize
|
||||
internal set(value) {
|
||||
circleClipTapView.arcSize = value
|
||||
}
|
||||
|
||||
fun arcSize(@DimenRes resId: Int) = apply {
|
||||
arcSize = context.resources.getDimension(resId)
|
||||
}
|
||||
|
||||
fun arcSize(px: Float) = apply {
|
||||
arcSize = px
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration the icon animation (fade in + fade out) for a full cycle in milliseconds.
|
||||
*/
|
||||
var iconAnimationDuration: Long = 750
|
||||
get() = secondsView.cycleDuration
|
||||
private set(value) {
|
||||
secondsView.cycleDuration = value
|
||||
field = value
|
||||
}
|
||||
|
||||
fun iconAnimationDuration(duration: Long) = apply {
|
||||
iconAnimationDuration = duration
|
||||
}
|
||||
|
||||
/**
|
||||
* One of the three forward icons which will be animated above the seconds indicator.
|
||||
* The rewind icon will be the 180° mirrored version.
|
||||
*
|
||||
* Keep in mind that padding on the left and right of the drawable will be rendered which
|
||||
* could result in additional space between the three icons.
|
||||
*/
|
||||
@DrawableRes
|
||||
var icon: Int = 0
|
||||
get() = secondsView.icon
|
||||
private set(value) {
|
||||
secondsView.stop()
|
||||
secondsView.icon = value
|
||||
field = value
|
||||
}
|
||||
|
||||
fun icon(@DrawableRes resId: Int) = apply {
|
||||
icon = resId
|
||||
}
|
||||
|
||||
/**
|
||||
* Text appearance of the *xx seconds* text.
|
||||
*/
|
||||
@StyleRes
|
||||
var textAppearance: Int = 0
|
||||
private set(value) {
|
||||
TextViewCompat.setTextAppearance(secondsView.textView, value)
|
||||
field = value
|
||||
}
|
||||
|
||||
fun textAppearance(@StyleRes resId: Int) = apply {
|
||||
textAppearance = resId
|
||||
}
|
||||
|
||||
/**
|
||||
* TextView view for *xx seconds*.
|
||||
*
|
||||
* In case of you'd like to change some specific attributes of the TextView in runtime.
|
||||
*/
|
||||
val secondsTextView: TextView
|
||||
get() = secondsView.textView
|
||||
|
||||
override fun onDoubleTapStarted(posX: Float, posY: Float) {
|
||||
if (player == null || playerView == null)
|
||||
return
|
||||
|
||||
if (performListener?.shouldForward(player!!, playerView!!, posX) == null)
|
||||
return
|
||||
}
|
||||
|
||||
override fun onDoubleTapProgressUp(posX: Float, posY: Float) {
|
||||
|
||||
// Check first whether forwarding/rewinding is "valid"
|
||||
if (player == null || playerView == null) return
|
||||
|
||||
val shouldForward = performListener?.shouldForward(player!!, playerView!!, posX)
|
||||
|
||||
// YouTube behavior: show overlay on MOTION_UP
|
||||
// But check whether the first double tap is in invalid area
|
||||
if (this.visibility != View.VISIBLE) {
|
||||
if (shouldForward != null) {
|
||||
performListener?.onAnimationStart()
|
||||
secondsView.visibility = View.VISIBLE
|
||||
secondsView.start()
|
||||
} else
|
||||
return
|
||||
}
|
||||
|
||||
when (shouldForward) {
|
||||
false -> {
|
||||
|
||||
// First time tap or switched
|
||||
if (secondsView.isForward) {
|
||||
changeConstraints(false)
|
||||
secondsView.apply {
|
||||
isForward = false
|
||||
seconds = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel ripple and start new without triggering overlay disappearance
|
||||
// (resetting instead of ending)
|
||||
circleClipTapView.resetAnimation {
|
||||
circleClipTapView.updatePosition(posX, posY)
|
||||
}
|
||||
rewinding()
|
||||
}
|
||||
true -> {
|
||||
|
||||
// First time tap or switched
|
||||
if (!secondsView.isForward) {
|
||||
changeConstraints(true)
|
||||
secondsView.apply {
|
||||
isForward = true
|
||||
seconds = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel ripple and start new without triggering overlay disappearance
|
||||
// (resetting instead of ending)
|
||||
circleClipTapView.resetAnimation {
|
||||
circleClipTapView.updatePosition(posX, posY)
|
||||
}
|
||||
forwarding()
|
||||
}
|
||||
else -> {
|
||||
// Middle area tapped: do nothing
|
||||
//
|
||||
// playerView?.cancelInDoubleTapMode()
|
||||
// circle_clip_tap_view.endAnimation()
|
||||
// triangle_seconds_view.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeks the video to desired position.
|
||||
* Calls interface functions when start reached ([SeekListener.onVideoStartReached])
|
||||
* or when end reached ([SeekListener.onVideoEndReached])
|
||||
*
|
||||
* @param newPosition desired position
|
||||
*/
|
||||
private fun seekToPosition(newPosition: Long?) {
|
||||
if (newPosition == null) return
|
||||
|
||||
// Start of the video reached
|
||||
if (newPosition <= 0) {
|
||||
player?.seekTo(0)
|
||||
|
||||
seekListener?.onVideoStartReached()
|
||||
return
|
||||
}
|
||||
|
||||
// End of the video reached
|
||||
player?.duration?.let { total ->
|
||||
if (newPosition >= total) {
|
||||
player?.seekTo(total)
|
||||
|
||||
seekListener?.onVideoEndReached()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise
|
||||
playerView?.keepInDoubleTapMode()
|
||||
player?.seekTo(newPosition)
|
||||
}
|
||||
|
||||
private fun forwarding() {
|
||||
secondsView.seconds += seekSeconds
|
||||
seekToPosition(player?.currentPosition?.plus(seekSeconds * 1000))
|
||||
}
|
||||
|
||||
private fun rewinding() {
|
||||
secondsView.seconds += seekSeconds
|
||||
seekToPosition(player?.currentPosition?.minus(seekSeconds * 1000))
|
||||
}
|
||||
|
||||
private fun changeConstraints(forward: Boolean) {
|
||||
val constraintSet = ConstraintSet()
|
||||
with(constraintSet) {
|
||||
clone(rootLayout)
|
||||
if (forward) {
|
||||
clear(secondsView.id, ConstraintSet.START)
|
||||
connect(secondsView.id, ConstraintSet.END,
|
||||
ConstraintSet.PARENT_ID, ConstraintSet.END)
|
||||
} else {
|
||||
clear(secondsView.id, ConstraintSet.END)
|
||||
connect(secondsView.id, ConstraintSet.START,
|
||||
ConstraintSet.PARENT_ID, ConstraintSet.START)
|
||||
}
|
||||
secondsView.start()
|
||||
applyTo(rootLayout)
|
||||
}
|
||||
}
|
||||
|
||||
interface PerformListener {
|
||||
/**
|
||||
* Called when the overlay is not visible and onDoubleTapProgressUp event occurred.
|
||||
* Visibility of the overlay should be set to VISIBLE within this interface method.
|
||||
*/
|
||||
fun onAnimationStart()
|
||||
|
||||
/**
|
||||
* Called when the circle animation is finished.
|
||||
* Visibility of the overlay should be set to GONE within this interface method.
|
||||
*/
|
||||
fun onAnimationEnd()
|
||||
|
||||
/**
|
||||
* Determines whether the player should forward, rewind or skip this tap by doing
|
||||
* nothing / ignoring. Is called for each tap.
|
||||
*
|
||||
* By overriding this method you can check for self-defined conditions whether showing the
|
||||
* overlay and rewinding/forwarding (e.g. if the media source valid) or skip it.
|
||||
*
|
||||
* In the following you see the default conditions for each action (if there is no media
|
||||
* to play ([PlaybackState.STATE_NONE]), an error occurred ([PlaybackState.STATE_ERROR])
|
||||
* or the media is stopped ([PlaybackState.STATE_STOPPED]) the tap will be ignored in any
|
||||
* case):
|
||||
*
|
||||
*
|
||||
* | Action | Current position | Screen width portion |
|
||||
* |---------|---------------------------|----------------------|
|
||||
* | rewind | greater than 500 ms | 0% to 35% |
|
||||
* | forward | less than total duration | 65% to 100% |
|
||||
* | ignore | ------------ | between 35% and 65% |
|
||||
*
|
||||
* @param player Current [Player]
|
||||
* @param playerView [PlayerView] which accepts the taps
|
||||
* @param posX Position of the tap on the x-axis
|
||||
*
|
||||
* @return `true` to forward, `false` to rewind or `null` to ignore.
|
||||
*/
|
||||
fun shouldForward(player: Player, playerView: DoubleTapPlayerView, posX: Float): Boolean? {
|
||||
|
||||
if (player.playbackState == PlaybackState.STATE_ERROR ||
|
||||
player.playbackState == PlaybackState.STATE_NONE ||
|
||||
player.playbackState == PlaybackState.STATE_STOPPED) {
|
||||
|
||||
playerView.cancelInDoubleTapMode()
|
||||
return null
|
||||
}
|
||||
|
||||
if (player.currentPosition > 500 && posX < playerView.width * 0.35)
|
||||
return false
|
||||
|
||||
if (player.currentPosition < player.duration && posX > playerView.width * 0.65)
|
||||
return true
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,221 @@
|
||||
package com.github.vkay94.dtpv.youtube.views
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.vkay94.dtpv.R
|
||||
|
||||
/**
|
||||
* View class
|
||||
*
|
||||
* Draws a arc shape and provides a circle scaling animation.
|
||||
* Used by [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay].
|
||||
*/
|
||||
internal class CircleClipTapView(context: Context?, attrs: AttributeSet) :
|
||||
View(context, attrs) {
|
||||
|
||||
private var backgroundPaint = Paint()
|
||||
private var circlePaint = Paint()
|
||||
|
||||
private var widthPx = 0
|
||||
private var heightPx = 0
|
||||
|
||||
// Background
|
||||
|
||||
private var shapePath = Path()
|
||||
private var isLeft = true
|
||||
|
||||
// Circle
|
||||
|
||||
private var cX = 0f
|
||||
private var cY = 0f
|
||||
|
||||
private var currentRadius = 0f
|
||||
private var minRadius: Int = 0
|
||||
private var maxRadius: Int = 0
|
||||
|
||||
// Animation
|
||||
|
||||
private var valueAnimator: ValueAnimator? = null
|
||||
private var forceReset = false
|
||||
|
||||
init {
|
||||
requireNotNull(context) { "Context is null." }
|
||||
|
||||
backgroundPaint.apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
color = ContextCompat.getColor(context, R.color.dtpv_yt_background_circle_color)
|
||||
}
|
||||
|
||||
circlePaint.apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
color = ContextCompat.getColor(context, R.color.dtpv_yt_tap_circle_color)
|
||||
}
|
||||
|
||||
// Pre-configuations depending on device display metrics
|
||||
val dm = context.resources.displayMetrics
|
||||
|
||||
widthPx = dm.widthPixels
|
||||
heightPx = dm.heightPixels
|
||||
|
||||
minRadius = (30f * dm.density).toInt()
|
||||
maxRadius = (400f * dm.density).toInt()
|
||||
|
||||
updatePathShape()
|
||||
|
||||
valueAnimator = getCircleAnimator()
|
||||
}
|
||||
|
||||
var performAtEnd: () -> Unit = { }
|
||||
|
||||
/*
|
||||
Getter and setter
|
||||
*/
|
||||
|
||||
var arcSize: Float = 80f
|
||||
set(value) {
|
||||
field = value
|
||||
updatePathShape()
|
||||
}
|
||||
|
||||
var circleBackgroundColor: Int
|
||||
get() = backgroundPaint.color
|
||||
set(value) {
|
||||
backgroundPaint.color = value
|
||||
}
|
||||
|
||||
var circleColor: Int
|
||||
get() = circlePaint.color
|
||||
set(value) {
|
||||
circlePaint.color = value
|
||||
}
|
||||
|
||||
var animationDuration: Long
|
||||
get() = valueAnimator?.duration ?: 650
|
||||
set(value) {
|
||||
getCircleAnimator().duration = value
|
||||
}
|
||||
|
||||
/*
|
||||
Methods
|
||||
*/
|
||||
|
||||
/*
|
||||
Circle
|
||||
*/
|
||||
|
||||
fun updatePosition(x: Float, y: Float) {
|
||||
cX = x
|
||||
cY = y
|
||||
|
||||
val newIsLeft = x <= resources.displayMetrics.widthPixels / 2
|
||||
if (isLeft != newIsLeft) {
|
||||
isLeft = newIsLeft
|
||||
updatePathShape()
|
||||
}
|
||||
}
|
||||
|
||||
private fun invalidateWithCurrentRadius(factor: Float) {
|
||||
currentRadius = minRadius + ((maxRadius - minRadius) * factor)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/*
|
||||
Background
|
||||
*/
|
||||
|
||||
private fun updatePathShape() {
|
||||
val halfWidth = widthPx * 0.5f
|
||||
|
||||
shapePath.reset()
|
||||
// shapePath.fillType = Path.FillType.WINDING
|
||||
|
||||
val w = if (isLeft) 0f else widthPx.toFloat()
|
||||
val f = if (isLeft) 1 else -1
|
||||
|
||||
shapePath.moveTo(w, 0f)
|
||||
shapePath.lineTo(f * (halfWidth - arcSize) + w, 0f)
|
||||
shapePath.quadTo(
|
||||
f * (halfWidth + arcSize) + w,
|
||||
heightPx.toFloat() / 2,
|
||||
f * (halfWidth - arcSize) + w,
|
||||
heightPx.toFloat()
|
||||
)
|
||||
shapePath.lineTo(w, heightPx.toFloat())
|
||||
|
||||
shapePath.close()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/*
|
||||
Animation
|
||||
*/
|
||||
|
||||
private fun getCircleAnimator(): ValueAnimator {
|
||||
if (valueAnimator == null) {
|
||||
valueAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = animationDuration
|
||||
// interpolator = LinearInterpolator()
|
||||
addUpdateListener {
|
||||
invalidateWithCurrentRadius(it.animatedValue as Float)
|
||||
}
|
||||
|
||||
addListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
visibility = VISIBLE
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
if (!forceReset) performAtEnd()
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animator?) {}
|
||||
override fun onAnimationCancel(animation: Animator?) {}
|
||||
})
|
||||
}
|
||||
}
|
||||
return valueAnimator!!
|
||||
}
|
||||
|
||||
fun resetAnimation(body: () -> Unit) {
|
||||
forceReset = true
|
||||
getCircleAnimator().end()
|
||||
body()
|
||||
forceReset = false
|
||||
getCircleAnimator().start()
|
||||
}
|
||||
|
||||
fun endAnimation() {
|
||||
getCircleAnimator().end()
|
||||
}
|
||||
|
||||
/*
|
||||
Others: Drawing and Measurements
|
||||
*/
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
widthPx = w
|
||||
heightPx = h
|
||||
updatePathShape()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
// Background
|
||||
canvas?.clipPath(shapePath)
|
||||
canvas?.drawPath(shapePath, backgroundPaint)
|
||||
|
||||
// Circle
|
||||
canvas?.drawCircle(cX, cY, currentRadius, circlePaint)
|
||||
}
|
||||
}
|
@ -0,0 +1,201 @@
|
||||
package com.github.vkay94.dtpv.youtube.views
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.animation.doOnStart
|
||||
import com.github.vkay94.dtpv.R
|
||||
|
||||
/**
|
||||
* Layout group which handles the icon animation while forwarding and rewinding.
|
||||
*
|
||||
* Since it's based on view's alpha the fading effect is more fluid (more YouTube-like) than
|
||||
* using static drawables, especially when [cycleDuration] is low.
|
||||
*
|
||||
* Used by [YouTubeOverlay][com.github.vkay94.dtpv.youtube.YouTubeOverlay].
|
||||
*/
|
||||
class SecondsView(context: Context, attrs: AttributeSet?) :
|
||||
ConstraintLayout(context, attrs) {
|
||||
|
||||
private var trianglesContainer: LinearLayout
|
||||
private var secondsTextView: TextView
|
||||
private var icon1: ImageView
|
||||
private var icon2: ImageView
|
||||
private var icon3: ImageView
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.yt_seconds_view, this, true)
|
||||
|
||||
trianglesContainer = findViewById(R.id.triangle_container)
|
||||
secondsTextView = findViewById(R.id.tv_seconds)
|
||||
icon1 = findViewById(R.id.icon_1)
|
||||
icon2 = findViewById(R.id.icon_2)
|
||||
icon3 = findViewById(R.id.icon_3)
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the duration for a full cycle of the triangle animation.
|
||||
* Each animation step takes 20% of it.
|
||||
*/
|
||||
var cycleDuration: Long = 750L
|
||||
set(value) {
|
||||
firstAnimator.duration = value / 5
|
||||
secondAnimator.duration = value / 5
|
||||
thirdAnimator.duration = value / 5
|
||||
fourthAnimator.duration = value / 5
|
||||
fifthAnimator.duration = value / 5
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the `TextView`'s seconds text according to the device`s language.
|
||||
*/
|
||||
var seconds: Int = 0
|
||||
set(value) {
|
||||
secondsTextView.text = context.resources.getQuantityString(
|
||||
R.plurals.quick_seek_x_second, value, value
|
||||
)
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors the triangles depending on what kind of type should be used (forward/rewind).
|
||||
*/
|
||||
var isForward: Boolean = true
|
||||
set(value) {
|
||||
trianglesContainer.rotation = if (value) 0f else 180f
|
||||
field = value
|
||||
}
|
||||
|
||||
val textView: TextView
|
||||
get() = secondsTextView
|
||||
|
||||
@DrawableRes
|
||||
var icon: Int = R.drawable.ic_play_triangle
|
||||
set(value) {
|
||||
if (value > 0) {
|
||||
icon1.setImageResource(value)
|
||||
icon2.setImageResource(value)
|
||||
icon3.setImageResource(value)
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the triangle animation
|
||||
*/
|
||||
fun start() {
|
||||
stop()
|
||||
firstAnimator.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the triangle animation
|
||||
*/
|
||||
fun stop() {
|
||||
firstAnimator.cancel()
|
||||
secondAnimator.cancel()
|
||||
thirdAnimator.cancel()
|
||||
fourthAnimator.cancel()
|
||||
fifthAnimator.cancel()
|
||||
reset()
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
icon1.alpha = 0f
|
||||
icon2.alpha = 0f
|
||||
icon3.alpha = 0f
|
||||
}
|
||||
|
||||
private val firstAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {
|
||||
doOnStart {
|
||||
icon1.alpha = 0f
|
||||
icon2.alpha = 0f
|
||||
icon3.alpha = 0f
|
||||
}
|
||||
addUpdateListener {
|
||||
icon1.alpha = (it.animatedValue as Float)
|
||||
}
|
||||
|
||||
doOnEnd {
|
||||
secondAnimator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val secondAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {
|
||||
doOnStart {
|
||||
icon1.alpha = 1f
|
||||
icon2.alpha = 0f
|
||||
icon3.alpha = 0f
|
||||
}
|
||||
addUpdateListener {
|
||||
icon2.alpha = (it.animatedValue as Float)
|
||||
}
|
||||
doOnEnd {
|
||||
thirdAnimator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val thirdAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {
|
||||
doOnStart {
|
||||
icon1.alpha = 1f
|
||||
icon2.alpha = 1f
|
||||
icon3.alpha = 0f
|
||||
}
|
||||
addUpdateListener {
|
||||
icon1.alpha =
|
||||
1f - icon3.alpha // or 1f - it (t3.alpha => all three stay a little longer together)
|
||||
icon3.alpha = (it.animatedValue as Float)
|
||||
}
|
||||
doOnEnd {
|
||||
fourthAnimator.start()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val fourthAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {
|
||||
doOnStart {
|
||||
icon1.alpha = 0f
|
||||
icon2.alpha = 1f
|
||||
icon3.alpha = 1f
|
||||
}
|
||||
addUpdateListener {
|
||||
icon2.alpha = 1f - (it.animatedValue as Float)
|
||||
}
|
||||
doOnEnd {
|
||||
fifthAnimator.start()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private val fifthAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(0f, 1f).setDuration(cycleDuration / 5).apply {
|
||||
doOnStart {
|
||||
icon1.alpha = 0f
|
||||
icon2.alpha = 0f
|
||||
icon3.alpha = 1f
|
||||
}
|
||||
addUpdateListener {
|
||||
icon3.alpha = 1f - (it.animatedValue as Float)
|
||||
}
|
||||
doOnEnd {
|
||||
firstAnimator.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M3,2 L22,12 L3,22 Z" />
|
||||
|
||||
</vector>
|
@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/root_constraint_layout"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.github.vkay94.dtpv.youtube.views.CircleClipTapView
|
||||
android:id="@+id/circle_clip_tap_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
|
||||
<com.github.vkay94.dtpv.youtube.views.SecondsView
|
||||
android:id="@+id/seconds_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintWidth_percent="0.5"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:ignore="ContentDescription">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/triangle_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_play_triangle"
|
||||
tools:alpha="0.18" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_play_triangle"
|
||||
tools:alpha="0.5" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/icon_3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:srcCompat="@drawable/ic_play_triangle"
|
||||
tools:alpha="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/tv_seconds"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:padding="4dp"
|
||||
tools:text="20 Sekunden" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekondes</item>
|
||||
<item quantity="one">%d sekonde</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d ሰከንዶች</item>
|
||||
<item quantity="one">%d ሰከንድ</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,10 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d ثانية</item>
|
||||
<item quantity="zero">%d ثانية</item>
|
||||
<item quantity="one">ثانية واحدة (%d)</item>
|
||||
<item quantity="two">ثانيتان (%d)</item>
|
||||
<item quantity="few">%d ثوانٍ</item>
|
||||
<item quantity="many">%d ثانية</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d saniyə</item>
|
||||
<item quantity="one">%d saniyə</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekundi</item>
|
||||
<item quantity="one">%d sekunda</item>
|
||||
<item quantity="few">%d sekunde</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d секунди</item>
|
||||
<item quantity="one">%d секунда</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d সেকেন্ড</item>
|
||||
<item quantity="one">%d সেকেন্ড</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d s</item>
|
||||
<item quantity="one">%d s</item>
|
||||
<item quantity="few">%d s</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekunder</item>
|
||||
<item quantity="one">%d sekund</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d δευτερόλεπτα</item>
|
||||
<item quantity="one">%d δευτερόλεπτο</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d seconds</item>
|
||||
<item quantity="one">%d second</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d seconds</item>
|
||||
<item quantity="one">%d second</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekundit</item>
|
||||
<item quantity="one">%d sekund</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d ثانیه</item>
|
||||
<item quantity="one">%d ثانیه</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d સેકન્ડ</item>
|
||||
<item quantity="one">%d સેકન્ડ</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d सेकंड</item>
|
||||
<item quantity="one">%d सेकंड</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekundi</item>
|
||||
<item quantity="one">%d sekunda</item>
|
||||
<item quantity="few">%d sekunde</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d másodperc</item>
|
||||
<item quantity="one">%d másodperc</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d վայրկյան</item>
|
||||
<item quantity="one">%d վայրկյան</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d detik</item>
|
||||
<item quantity="one">%d detik</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekúndur</item>
|
||||
<item quantity="one">%d sekúnda</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d secondi</item>
|
||||
<item quantity="one">%d secondo</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d שניות</item>
|
||||
<item quantity="one">שנייה אחת (%d)</item>
|
||||
<item quantity="two">%d שניות</item>
|
||||
<item quantity="many">%d שניות</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d秒</item>
|
||||
<item quantity="one">%d秒</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d წამი</item>
|
||||
<item quantity="one">%d წამი</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d секунд</item>
|
||||
<item quantity="one">%d секунд</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d វិនាទី</item>
|
||||
<item quantity="one">%d វិនាទី</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d ಸೆಕೆಂಡ್ಗಳು</item>
|
||||
<item quantity="one">%d ಸೆಕೆಂಡ್ಗಳು</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d초</item>
|
||||
<item quantity="one">%d초</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d секунд</item>
|
||||
<item quantity="one">%d секунд</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d ວິນາທີ</item>
|
||||
<item quantity="one">%d ວິນາທີ</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekundžių</item>
|
||||
<item quantity="one">%d sekundė</item>
|
||||
<item quantity="few">%d sekundės</item>
|
||||
<item quantity="many">%d sekundės</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d секунди</item>
|
||||
<item quantity="one">%d секунда</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d സെക്കൻഡ്</item>
|
||||
<item quantity="one">%d സെക്കൻഡ്</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d секунд</item>
|
||||
<item quantity="one">%d секунд</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d सेकंद</item>
|
||||
<item quantity="one">%d सेकंद</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d saat</item>
|
||||
<item quantity="one">%d saat</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d စက္ကန့်</item>
|
||||
<item quantity="one">%d စက္ကန့်</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekunder</item>
|
||||
<item quantity="one">%d sekund</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d सेकेन्ड</item>
|
||||
<item quantity="one">%d सेकेन्ड</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d seconden</item>
|
||||
<item quantity="one">%d seconde</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d ਸਕਿੰਟ</item>
|
||||
<item quantity="one">%d ਸਕਿੰਟ</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d segundos</item>
|
||||
<item quantity="one">%d segundo</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d segundos</item>
|
||||
<item quantity="one">%d segundo</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d de secunde</item>
|
||||
<item quantity="one">%d secundă</item>
|
||||
<item quantity="few">%d secunde</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d секунды</item>
|
||||
<item quantity="one">%d секунда</item>
|
||||
<item quantity="few">%d секунды</item>
|
||||
<item quantity="many">%d секунд</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">තත්පර %dක්</item>
|
||||
<item quantity="one">තත්පර %dක්</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,8 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekund</item>
|
||||
<item quantity="one">%d sekunda</item>
|
||||
<item quantity="two">%d sekundi</item>
|
||||
<item quantity="few">%d sekunde</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekonda</item>
|
||||
<item quantity="one">%d sekondë</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,7 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d секунди</item>
|
||||
<item quantity="one">%d секунда</item>
|
||||
<item quantity="few">%d секунде</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d sekunder</item>
|
||||
<item quantity="one">%d sekund</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">Sekunde %d</item>
|
||||
<item quantity="one">Sekunde %d</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d விநாடிகள்</item>
|
||||
<item quantity="one">%d விநாடி</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d సెకన్లు</item>
|
||||
<item quantity="one">%d సెకను</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d วินาที</item>
|
||||
<item quantity="one">%d วินาที</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d na segundo</item>
|
||||
<item quantity="one">%d segundo</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d saniye</item>
|
||||
<item quantity="one">%d saniye</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d سیکنڈز</item>
|
||||
<item quantity="one">%d سیکنڈ</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d soniya</item>
|
||||
<item quantity="one">%d soniya</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d giây</item>
|
||||
<item quantity="one">%d giây</item>
|
||||
</plurals>
|
||||
</resources>
|
@ -0,0 +1,6 @@
|
||||
<resources>
|
||||
<plurals name="quick_seek_x_second">
|
||||
<item quantity="other">%d 秒</item>
|
||||
<item quantity="one">%d 秒</item>
|
||||
</plurals>
|
||||
</resources>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue