Prepare Media3

This commit is contained in:
Thomas 2024-01-20 18:10:05 +01:00
parent 596c546cce
commit c778c71306
108 changed files with 1993 additions and 73 deletions

View File

@ -132,14 +132,18 @@ dependencies {
implementation project(path: ':sparkbutton')
implementation project(path: ':colorPicker')
implementation project(path: ':mathjaxandroid')
implementation project(path: ':doubletapplayerview')
implementation 'com.burhanrashid52:photoeditor:1.5.1'
implementation("com.vanniktech:android-image-cropper:4.3.3")
annotationProcessor "com.github.bumptech.glide:compiler:4.12.0"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.23.0'
implementation 'com.google.android.exoplayer:exoplayer:2.19.1'
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.viewpager2:viewpager2:1.0.0"
implementation 'com.github.piasy:rxandroidaudio:1.7.0'
implementation 'com.github.piasy:AudioProcessor:1.7.0'
@ -177,14 +181,13 @@ dependencies {
implementation 'androidx.browser:browser:1.7.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.github.amoskorir:avatarimagegenerator:1.5.0'
implementation 'com.google.android.exoplayer:extension-mediasession:2.19.1'
implementation "com.github.mabbas007:TagsEditText:1.0.5"
implementation "net.gotev:uploadservice:4.9.2"
implementation "net.gotev:uploadservice-okhttp:4.9.2"
implementation 'androidx.media:media:1.7.0'
implementation 'com.github.mancj:MaterialSearchBar:0.8.5'
implementation 'com.github.vkay94:DoubleTapPlayerView:1.0.0'
implementation 'io.noties.markwon:core:4.6.2'

View File

@ -28,9 +28,9 @@ import android.view.View;
import android.webkit.MimeTypeMap;
import androidx.appcompat.app.AlertDialog;
import androidx.media3.common.Player;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.jetbrains.annotations.NotNull;
@ -53,7 +53,7 @@ public class BasePeertubeActivity extends BaseBarActivity {
protected ActivityPeertubeBinding binding;
protected VideoData.Video peertube;
protected ExoPlayer player;
protected Player player;
protected String videoURL;
protected String subtitlesStr;

View File

@ -18,24 +18,24 @@ import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.media3.database.ExoDatabaseProvider;
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.database.ExoDatabaseProvider;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSourceFactory;
import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.datasource.FileDataSource;
import androidx.media3.datasource.cache.CacheDataSink;
import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor;
import androidx.media3.datasource.cache.SimpleCache;
import java.io.File;
import app.fedilab.android.R;
@androidx.annotation.OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public class CacheDataSourceFactory implements DataSource.Factory {
private static SimpleCache sDownloadCache;

View File

@ -76,6 +76,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.LinearLayoutCompat;
@ -88,6 +89,14 @@ import androidx.fragment.app.Fragment;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.ui.PlayerView;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -97,13 +106,6 @@ import com.bumptech.glide.ListPreloader;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.request.RequestOptions;
import com.github.stom79.mytransl.MyTransL;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.smarteist.autoimageslider.SliderAnimations;
import com.smarteist.autoimageslider.SliderView;
@ -390,6 +392,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
* @param status {@link Status}
*/
@SuppressLint("ClickableViewAccessibility")
@androidx.annotation.OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static void statusManagement(Context context,
StatusesVM statusesVM,
SearchVM searchVM,

View File

@ -49,17 +49,18 @@ import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.ui.PlayerView;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.RequestBuilder;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import org.jetbrains.annotations.NotNull;

View File

@ -33,18 +33,18 @@ import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.preference.PreferenceManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.r0adkll.slidr.Slidr;
import com.r0adkll.slidr.model.SlidrConfig;
import com.r0adkll.slidr.model.SlidrInterface;
@ -248,7 +248,7 @@ public class FragmentMedia extends Fragment {
}
}
@androidx.annotation.OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private void loadVideo(String url, String type) {
if (binding == null || !isAdded() || getActivity() == null || url == null) {
return;

View File

@ -14,7 +14,8 @@ package app.fedilab.android.peertube.activities;
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
* see <http://www.gnu.org/licenses>. */
import static com.google.android.exoplayer2.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO;
import static androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO;
import static app.fedilab.android.BaseMainActivity.currentAccount;
import static app.fedilab.android.BaseMainActivity.currentInstance;
import static app.fedilab.android.BaseMainActivity.currentToken;
@ -85,34 +86,30 @@ import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.VideoSize;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.source.MergingMediaSource;
import androidx.media3.exoplayer.source.ProgressiveMediaSource;
import androidx.media3.exoplayer.source.SingleSampleMediaSource;
import androidx.media3.exoplayer.trackselection.AdaptiveTrackSelection;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.trackselection.TrackSelector;
import androidx.media3.ui.AspectRatioFrameLayout;
import androidx.media3.ui.PlayerControlView;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.github.vkay94.dtpv.youtube.YouTubeOverlay;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.DefaultTimeBar;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.video.VideoSize;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
@ -168,7 +165,7 @@ import app.fedilab.android.peertube.webview.MastalabWebChromeClient;
import app.fedilab.android.peertube.webview.MastalabWebViewClient;
import es.dmoral.toasty.Toasty;
@androidx.annotation.OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public class PeertubeActivity extends BasePeertubeActivity implements CommentListAdapter.AllCommentRemoved, MenuAdapter.ItemClicked, MenuItemAdapter.ItemAction, Player.Listener {
public static String video_id;

View File

@ -14,14 +14,14 @@ import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.media3.common.C;
import androidx.media3.common.Player;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.ui.TrackSelectionView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.ui.TrackSelectionView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.common.collect.ImmutableList;
@ -31,12 +31,14 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import app.fedilab.android.R;
import app.fedilab.android.databinding.TrackSelectionDialogBinding;
/**
* Dialog to select tracks.
*/
@androidx.annotation.OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public final class TrackSelectionDialog extends DialogFragment {
public static final ImmutableList<Integer> SUPPORTED_TRACK_TYPES =

View File

@ -65,14 +65,14 @@
android:layout_height="match_parent"
android:visibility="gone">
<com.google.android.exoplayer2.ui.PlayerView
<androidx.media3.ui.PlayerView
android:id="@+id/media_video"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:use_controller="false" />
<com.google.android.exoplayer2.ui.PlayerControlView
<androidx.media3.ui.PlayerControlView
android:id="@+id/controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -18,7 +18,7 @@
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<com.google.android.exoplayer2.ui.PlayerView
<androidx.media3.ui.PlayerView
android:id="@+id/media_video"
app:shutter_background_color="@color/transparent"
app:surface_type="texture_view"

1
doubletapplayerview/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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"
}

View File

21
doubletapplayerview/proguard-rules.pro vendored Normal file
View File

@ -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

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.vkay94.dtpv" />

View File

@ -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
}
}
}

View File

@ -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() { }
}

View File

@ -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() {}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d ሰከንዶች</item>
<item quantity="one">%d ሰከንድ</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d секунди</item>
<item quantity="one">%d секунда</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d সেকেন্ড</item>
<item quantity="one">%d সেকেন্ড</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d segons</item>
<item quantity="one">%d segon</item>
</plurals>
</resources>

View File

@ -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="few">%d sekundy</item>
<item quantity="many">%d sekundy</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d Sekunden</item>
<item quantity="one">%d Sekunde</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d δευτερόλεπτα</item>
<item quantity="one">%d δευτερόλεπτο</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d segundo</item>
<item quantity="one">%d segundo</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d ثانیه</item>
<item quantity="one">%d ثانیه</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d sekuntia</item>
<item quantity="one">%d sekunti</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d secondes</item>
<item quantity="one">%d seconde</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d secondes</item>
<item quantity="one">%d seconde</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d સેકન્ડ</item>
<item quantity="one">%d સેકન્ડ</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d सेकंड</item>
<item quantity="one">%d सेकंड</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d վայրկյան</item>
<item quantity="one">%d վայրկյան</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d秒</item>
<item quantity="one">%d秒</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d წამი</item>
<item quantity="one">%d წამი</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d секунд</item>
<item quantity="one">%d секунд</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d វិនាទី</item>
<item quantity="one">%d វិនាទី</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d ಸೆಕೆಂಡ್‌ಗಳು</item>
<item quantity="one">%d ಸೆಕೆಂಡ್‌ಗಳು</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d초</item>
<item quantity="one">%d초</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d секунд</item>
<item quantity="one">%d секунд</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d ວິນາທີ</item>
<item quantity="one">%d ວິນາທີ</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -0,0 +1,7 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d sekundes</item>
<item quantity="zero">%d sekundes</item>
<item quantity="one">%d sekunde</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d секунди</item>
<item quantity="one">%d секунда</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d സെക്കൻഡ്</item>
<item quantity="one">%d സെക്കൻഡ്</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d секунд</item>
<item quantity="one">%d секунд</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d सेकंद</item>
<item quantity="one">%d सेकंद</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d စက္ကန့်</item>
<item quantity="one">%d စက္ကန့်</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d सेकेन्ड</item>
<item quantity="one">%d सेकेन्ड</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d ਸਕਿੰਟ</item>
<item quantity="one">%d ਸਕਿੰਟ</item>
</plurals>
</resources>

View File

@ -0,0 +1,8 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d sekundy</item>
<item quantity="one">%d sekunda</item>
<item quantity="few">%d sekundy</item>
<item quantity="many">%d sekund</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">තත්පර %dක්</item>
<item quantity="one">තත්පර %dක්</item>
</plurals>
</resources>

View File

@ -0,0 +1,8 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d sekúnd</item>
<item quantity="one">%d sekunda</item>
<item quantity="few">%d sekundy</item>
<item quantity="many">%d sekundy</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d விநாடிகள்</item>
<item quantity="one">%d விநாடி</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d సెకన్లు</item>
<item quantity="one">%d సెకను</item>
</plurals>
</resources>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d วินาที</item>
<item quantity="one">%d วินาที</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,6 @@
<resources>
<plurals name="quick_seek_x_second">
<item quantity="other">%d سیکنڈز</item>
<item quantity="one">%d سیکنڈ</item>
</plurals>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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