Photo viewer & video player UI
This commit is contained in:
parent
2e1f08a096
commit
94c864c8ac
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET"/>
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
|
||||||
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
|
||||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,8 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||||
.setCallback(new SimpleCallback<>(this){
|
.setCallback(new SimpleCallback<>(this){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Status> result){
|
public void onSuccess(List<Status> result){
|
||||||
|
if(getActivity()==null)
|
||||||
|
return;
|
||||||
onDataLoaded(result, !result.isEmpty());
|
onDataLoaded(result, !result.isEmpty());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -234,6 +234,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||||
currentPhotoViewer=null;
|
currentPhotoViewer=null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissions(String[] permissions){
|
||||||
|
requestPermissions(permissions, PhotoViewer.PERMISSION_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
private ImageStatusDisplayItem.Holder<?> findPhotoViewHolder(int index){
|
private ImageStatusDisplayItem.Holder<?> findPhotoViewHolder(int index){
|
||||||
int offset=0;
|
int offset=0;
|
||||||
for(StatusDisplayItem item:displayItems){
|
for(StatusDisplayItem item:displayItems){
|
||||||
|
@ -562,6 +567,20 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||||
super.onApplyWindowInsets(insets);
|
super.onApplyWindowInsets(insets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults){
|
||||||
|
if(requestCode==PhotoViewer.PERMISSION_REQUEST && currentPhotoViewer!=null){
|
||||||
|
currentPhotoViewer.onRequestPermissionsResult(permissions, grantResults);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause(){
|
||||||
|
super.onPause();
|
||||||
|
if(currentPhotoViewer!=null)
|
||||||
|
currentPhotoViewer.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||||
|
|
||||||
public DisplayItemsAdapter(){
|
public DisplayItemsAdapter(){
|
||||||
|
|
|
@ -1,16 +1,34 @@
|
||||||
package org.joinmastodon.android.ui.photoviewer;
|
package org.joinmastodon.android.ui.photoviewer;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.DownloadManager;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
|
import android.graphics.Insets;
|
||||||
import android.graphics.PixelFormat;
|
import android.graphics.PixelFormat;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.media.AudioManager;
|
||||||
import android.media.MediaPlayer;
|
import android.media.MediaPlayer;
|
||||||
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.view.DisplayCutout;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
import android.view.TextureView;
|
import android.view.TextureView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -19,27 +37,47 @@ import android.view.ViewTreeObserver;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
import org.joinmastodon.android.model.Attachment;
|
import org.joinmastodon.android.model.Attachment;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewpager2.widget.ViewPager2;
|
import androidx.viewpager2.widget.ViewPager2;
|
||||||
|
import me.grishka.appkit.imageloader.ImageCache;
|
||||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||||
import me.grishka.appkit.utils.BindableViewHolder;
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||||
|
import okio.BufferedSink;
|
||||||
|
import okio.Okio;
|
||||||
|
import okio.Sink;
|
||||||
|
import okio.Source;
|
||||||
|
|
||||||
public class PhotoViewer implements ZoomPanView.Listener{
|
public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
private static final String TAG="PhotoViewer";
|
private static final String TAG="PhotoViewer";
|
||||||
|
public static final int PERMISSION_REQUEST=926;
|
||||||
|
|
||||||
private Activity activity;
|
private Activity activity;
|
||||||
private List<Attachment> attachments;
|
private List<Attachment> attachments;
|
||||||
|
@ -48,10 +86,28 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
private Listener listener;
|
private Listener listener;
|
||||||
|
|
||||||
private FrameLayout windowView;
|
private FrameLayout windowView;
|
||||||
|
private FragmentRootLinearLayout uiOverlay;
|
||||||
private ViewPager2 pager;
|
private ViewPager2 pager;
|
||||||
private ColorDrawable background=new ColorDrawable(0xff000000);
|
private ColorDrawable background=new ColorDrawable(0xff000000);
|
||||||
private ArrayList<MediaPlayer> players=new ArrayList<>();
|
private ArrayList<MediaPlayer> players=new ArrayList<>();
|
||||||
private int screenOnRefCount=0;
|
private int screenOnRefCount=0;
|
||||||
|
private Toolbar toolbar;
|
||||||
|
private View toolbarWrap;
|
||||||
|
private SeekBar videoSeekBar;
|
||||||
|
private TextView videoTimeView;
|
||||||
|
private ImageButton videoPlayPauseButton;
|
||||||
|
private View videoControls;
|
||||||
|
private boolean uiVisible=true;
|
||||||
|
private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged;
|
||||||
|
private Runnable uiAutoHider=()->{
|
||||||
|
if(uiVisible)
|
||||||
|
toggleUI();
|
||||||
|
};
|
||||||
|
|
||||||
|
private boolean videoPositionNeedsUpdating;
|
||||||
|
private Runnable videoPositionUpdater=this::updateVideoPosition;
|
||||||
|
private int videoDuration, videoInitialPosition, videoLastTimeUpdatePosition;
|
||||||
|
private long videoInitialPositionTime;
|
||||||
|
|
||||||
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Listener listener){
|
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Listener listener){
|
||||||
this.activity=activity;
|
this.activity=activity;
|
||||||
|
@ -75,7 +131,26 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
|
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
|
||||||
Log.w(TAG, "dispatchApplyWindowInsets() called with: insets = ["+insets+"]");
|
if(Build.VERSION.SDK_INT>=29){
|
||||||
|
DisplayCutout cutout=insets.getDisplayCutout();
|
||||||
|
if(cutout!=null){
|
||||||
|
// Make controls extend beneath the cutout, and replace insets to avoid cutout insets being filled with "navigation bar color"
|
||||||
|
Insets tappable=insets.getTappableElementInsets();
|
||||||
|
int leftInset=Math.max(0, cutout.getSafeInsetLeft()-tappable.left);
|
||||||
|
int rightInset=Math.max(0, cutout.getSafeInsetRight()-tappable.right);
|
||||||
|
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, tappable.bottom);
|
||||||
|
toolbarWrap.setPadding(leftInset, 0, rightInset, 0);
|
||||||
|
videoControls.setPadding(leftInset, 0, rightInset, 0);
|
||||||
|
}else{
|
||||||
|
toolbarWrap.setPadding(0, 0, 0, 0);
|
||||||
|
videoControls.setPadding(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiOverlay.dispatchApplyWindowInsets(insets);
|
||||||
|
int bottomInset=insets.getSystemWindowInsetBottom();
|
||||||
|
if(bottomInset>0 && bottomInset<V.dp(36)){
|
||||||
|
uiOverlay.setPadding(uiOverlay.getPaddingLeft(), uiOverlay.getPaddingTop(), uiOverlay.getPaddingRight(), V.dp(36));
|
||||||
|
}
|
||||||
return insets.consumeSystemWindowInsets();
|
return insets.consumeSystemWindowInsets();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -84,15 +159,47 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
pager=new ViewPager2(activity);
|
pager=new ViewPager2(activity);
|
||||||
pager.setAdapter(new PhotoViewAdapter());
|
pager.setAdapter(new PhotoViewAdapter());
|
||||||
pager.setCurrentItem(index, false);
|
pager.setCurrentItem(index, false);
|
||||||
|
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||||
|
@Override
|
||||||
|
public void onPageSelected(int position){
|
||||||
|
onPageChanged(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
windowView.addView(pager);
|
windowView.addView(pager);
|
||||||
pager.setMotionEventSplittingEnabled(false);
|
pager.setMotionEventSplittingEnabled(false);
|
||||||
|
|
||||||
|
uiOverlay=activity.getLayoutInflater().inflate(R.layout.photo_viewer_ui, windowView).findViewById(R.id.photo_viewer_overlay);
|
||||||
|
uiOverlay.setStatusBarColor(0x80000000);
|
||||||
|
uiOverlay.setNavigationBarColor(0x80000000);
|
||||||
|
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
|
||||||
|
toolbar=uiOverlay.findViewById(R.id.toolbar);
|
||||||
|
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
|
||||||
|
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_fluent_arrow_download_24_regular).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||||
|
toolbar.setOnMenuItemClickListener(item->{
|
||||||
|
saveCurrentFile();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
uiOverlay.setAlpha(0f);
|
||||||
|
videoControls=uiOverlay.findViewById(R.id.video_player_controls);
|
||||||
|
videoSeekBar=uiOverlay.findViewById(R.id.seekbar);
|
||||||
|
videoTimeView=uiOverlay.findViewById(R.id.time);
|
||||||
|
videoPlayPauseButton=uiOverlay.findViewById(R.id.play_pause_btn);
|
||||||
|
if(attachments.get(index).type!=Attachment.Type.VIDEO){
|
||||||
|
videoControls.setVisibility(View.GONE);
|
||||||
|
}else{
|
||||||
|
videoDuration=(int)Math.round(attachments.get(index).getDuration()*1000);
|
||||||
|
videoLastTimeUpdatePosition=-1;
|
||||||
|
updateVideoTimeText(0);
|
||||||
|
}
|
||||||
|
|
||||||
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
|
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
|
||||||
wlp.type=WindowManager.LayoutParams.TYPE_APPLICATION;
|
wlp.type=WindowManager.LayoutParams.TYPE_APPLICATION;
|
||||||
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
|
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
|
||||||
| WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
| WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
||||||
wlp.format=PixelFormat.TRANSLUCENT;
|
wlp.format=PixelFormat.TRANSLUCENT;
|
||||||
wlp.setTitle(activity.getString(R.string.media_viewer));
|
wlp.setTitle(activity.getString(R.string.media_viewer));
|
||||||
|
if(Build.VERSION.SDK_INT>=28)
|
||||||
|
wlp.layoutInDisplayCutoutMode=Build.VERSION.SDK_INT>=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||||
wm.addView(windowView, wlp);
|
wm.addView(windowView, wlp);
|
||||||
|
|
||||||
|
@ -112,6 +219,44 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
videoPlayPauseButton.setOnClickListener(v->{
|
||||||
|
MediaPlayer player=findCurrentVideoPlayer();
|
||||||
|
if(player!=null){
|
||||||
|
if(player.isPlaying())
|
||||||
|
pauseVideo();
|
||||||
|
else
|
||||||
|
resumeVideo();
|
||||||
|
hideUiDelayed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
videoSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener(){
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser){
|
||||||
|
if(fromUser){
|
||||||
|
float p=progress/10000f;
|
||||||
|
updateVideoTimeText(Math.round(p*videoDuration));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar){
|
||||||
|
stopUpdatingVideoPosition();
|
||||||
|
if(!uiVisible) // If dragging started during hide animation
|
||||||
|
toggleUI();
|
||||||
|
windowView.removeCallbacks(uiAutoHider);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar){
|
||||||
|
MediaPlayer player=findCurrentVideoPlayer();
|
||||||
|
if(player!=null){
|
||||||
|
float progress=seekBar.getProgress()/10000f;
|
||||||
|
player.seekTo(Math.round(progress*player.getDuration()));
|
||||||
|
}
|
||||||
|
hideUiDelayed();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -127,15 +272,22 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
@Override
|
@Override
|
||||||
public void onSetBackgroundAlpha(float alpha){
|
public void onSetBackgroundAlpha(float alpha){
|
||||||
background.setAlpha(Math.round(alpha*255f));
|
background.setAlpha(Math.round(alpha*255f));
|
||||||
|
uiOverlay.setAlpha(Math.max(0f, alpha*2f-1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartSwipeToDismiss(){
|
public void onStartSwipeToDismiss(){
|
||||||
listener.setPhotoViewVisibility(pager.getCurrentItem(), false);
|
listener.setPhotoViewVisibility(pager.getCurrentItem(), false);
|
||||||
|
if(!uiVisible){
|
||||||
|
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() & ~(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN));
|
||||||
|
}else{
|
||||||
|
windowView.removeCallbacks(uiAutoHider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartSwipeToDismissTransition(float velocityY){
|
public void onStartSwipeToDismissTransition(float velocityY){
|
||||||
|
pauseVideo();
|
||||||
// stop receiving input events to allow the user to interact with the underlying UI while the animation is still running
|
// stop receiving input events to allow the user to interact with the underlying UI while the animation is still running
|
||||||
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
||||||
wlp.flags|=WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
wlp.flags|=WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||||||
|
@ -163,17 +315,74 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
@Override
|
@Override
|
||||||
public void onSwipeToDismissCanceled(){
|
public void onSwipeToDismissCanceled(){
|
||||||
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
||||||
|
if(!uiVisible){
|
||||||
|
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}else if(attachments.get(currentIndex).type==Attachment.Type.VIDEO){
|
||||||
|
hideUiDelayed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDismissed(){
|
public void onDismissed(){
|
||||||
for(MediaPlayer player:players)
|
for(MediaPlayer player:players)
|
||||||
player.release();
|
player.release();
|
||||||
|
if(!players.isEmpty()){
|
||||||
|
activity.getSystemService(AudioManager.class).abandonAudioFocus(audioFocusListener);
|
||||||
|
}
|
||||||
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
||||||
wm.removeView(windowView);
|
wm.removeView(windowView);
|
||||||
listener.photoViewerDismissed();
|
listener.photoViewerDismissed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSingleTap(){
|
||||||
|
toggleUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleUI(){
|
||||||
|
if(uiVisible){
|
||||||
|
uiOverlay.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(250)
|
||||||
|
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||||
|
.withEndAction(()->uiOverlay.setVisibility(View.GONE))
|
||||||
|
.start();
|
||||||
|
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN);
|
||||||
|
}else{
|
||||||
|
uiOverlay.setVisibility(View.VISIBLE);
|
||||||
|
uiOverlay.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(300)
|
||||||
|
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||||
|
.start();
|
||||||
|
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() & ~(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN));
|
||||||
|
if(attachments.get(currentIndex).type==Attachment.Type.VIDEO)
|
||||||
|
hideUiDelayed(5000);
|
||||||
|
}
|
||||||
|
uiVisible=!uiVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideUiDelayed(){
|
||||||
|
hideUiDelayed(2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideUiDelayed(long delay){
|
||||||
|
windowView.removeCallbacks(uiAutoHider);
|
||||||
|
windowView.postDelayed(uiAutoHider, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPageChanged(int index){
|
||||||
|
currentIndex=index;
|
||||||
|
Attachment att=attachments.get(index);
|
||||||
|
V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
||||||
|
if(att.type==Attachment.Type.VIDEO){
|
||||||
|
videoSeekBar.setSecondaryProgress(0);
|
||||||
|
videoDuration=(int)Math.round(att.getDuration()*1000);
|
||||||
|
videoLastTimeUpdatePosition=-1;
|
||||||
|
updateVideoTimeText(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To be called when the list containing photo views is scrolled
|
* To be called when the list containing photo views is scrolled
|
||||||
* @param x
|
* @param x
|
||||||
|
@ -189,6 +398,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
||||||
wlp.flags|=WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
wlp.flags|=WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
||||||
wm.updateViewLayout(windowView, wlp);
|
wm.updateViewLayout(windowView, wlp);
|
||||||
|
activity.getSystemService(AudioManager.class).requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||||
}
|
}
|
||||||
screenOnRefCount++;
|
screenOnRefCount++;
|
||||||
}
|
}
|
||||||
|
@ -201,6 +411,185 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
||||||
wlp.flags&=~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
wlp.flags&=~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
||||||
wm.updateViewLayout(windowView, wlp);
|
wm.updateViewLayout(windowView, wlp);
|
||||||
|
activity.getSystemService(AudioManager.class).abandonAudioFocus(audioFocusListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPause(){
|
||||||
|
pauseVideo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCurrentFile(){
|
||||||
|
if(Build.VERSION.SDK_INT>=29){
|
||||||
|
doSaveCurrentFile();
|
||||||
|
}else{
|
||||||
|
if(activity.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){
|
||||||
|
listener.onRequestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE});
|
||||||
|
}else{
|
||||||
|
doSaveCurrentFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRequestPermissionsResult(String[] permissions, int[] results){
|
||||||
|
if(results[0]==PackageManager.PERMISSION_GRANTED){
|
||||||
|
doSaveCurrentFile();
|
||||||
|
}else if(!activity.shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)){
|
||||||
|
new M3AlertDialogBuilder(activity)
|
||||||
|
.setTitle(R.string.permission_required)
|
||||||
|
.setMessage(R.string.storage_permission_to_download)
|
||||||
|
.setPositiveButton(R.string.open_settings, (dialog, which)->activity.startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", activity.getPackageName(), null))))
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String mimeTypeForFileName(String fileName){
|
||||||
|
int extOffset=fileName.lastIndexOf('.');
|
||||||
|
if(extOffset>0){
|
||||||
|
return switch(fileName.substring(extOffset+1).toLowerCase()){
|
||||||
|
case "jpg", "jpeg" -> "image/jpeg";
|
||||||
|
case "png" -> "image/png";
|
||||||
|
case "gif" -> "image/gif";
|
||||||
|
case "webp" -> "image/webp";
|
||||||
|
case "mp4" -> "video/mp4";
|
||||||
|
case "webm" -> "video/webm";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OutputStream destinationStreamForFile(Attachment att) throws IOException{
|
||||||
|
String fileName=Uri.parse(att.url).getLastPathSegment();
|
||||||
|
if(Build.VERSION.SDK_INT>=29){
|
||||||
|
ContentValues values=new ContentValues();
|
||||||
|
// values.put(MediaStore.Downloads.DOWNLOAD_URI, att.url);
|
||||||
|
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
|
||||||
|
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
|
||||||
|
String mime=mimeTypeForFileName(fileName);
|
||||||
|
if(mime!=null)
|
||||||
|
values.put(MediaStore.MediaColumns.MIME_TYPE, mime);
|
||||||
|
ContentResolver cr=activity.getContentResolver();
|
||||||
|
Uri itemUri=cr.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
|
||||||
|
return cr.openOutputStream(itemUri);
|
||||||
|
}else{
|
||||||
|
return new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doSaveCurrentFile(){
|
||||||
|
Attachment att=attachments.get(pager.getCurrentItem());
|
||||||
|
if(att.type==Attachment.Type.IMAGE){
|
||||||
|
UrlImageLoaderRequest req=new UrlImageLoaderRequest(att.url);
|
||||||
|
try{
|
||||||
|
File file=ImageCache.getInstance(activity).getFile(req);
|
||||||
|
if(file==null){
|
||||||
|
saveViaDownloadManager(att);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MastodonAPIController.runInBackground(()->{
|
||||||
|
try(Source src=Okio.source(file); Sink sink=Okio.sink(destinationStreamForFile(att))){
|
||||||
|
BufferedSink buf=Okio.buffer(sink);
|
||||||
|
buf.writeAll(src);
|
||||||
|
buf.flush();
|
||||||
|
activity.runOnUiThread(()->Toast.makeText(activity, R.string.file_saved, Toast.LENGTH_SHORT).show());
|
||||||
|
if(Build.VERSION.SDK_INT<29){
|
||||||
|
String fileName=Uri.parse(att.url).getLastPathSegment();
|
||||||
|
File dstFile=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
|
||||||
|
MediaScannerConnection.scanFile(activity, new String[]{dstFile.getAbsolutePath()}, new String[]{mimeTypeForFileName(fileName)}, null);
|
||||||
|
}
|
||||||
|
}catch(IOException x){
|
||||||
|
Log.w(TAG, "doSaveCurrentFile: ", x);
|
||||||
|
activity.runOnUiThread(()->Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}catch(IOException x){
|
||||||
|
Log.w(TAG, "doSaveCurrentFile: ", x);
|
||||||
|
Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
saveViaDownloadManager(att);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveViaDownloadManager(Attachment att){
|
||||||
|
DownloadManager.Request req=new DownloadManager.Request(Uri.parse(att.url));
|
||||||
|
activity.getSystemService(DownloadManager.class).enqueue(req);
|
||||||
|
Toast.makeText(activity, R.string.downloading, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onAudioFocusChanged(int change){
|
||||||
|
if(change==AudioManager.AUDIOFOCUS_LOSS || change==AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || change==AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK){
|
||||||
|
pauseVideo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaPlayer findCurrentVideoPlayer(){
|
||||||
|
RecyclerView rv=(RecyclerView) pager.getChildAt(0);
|
||||||
|
if(rv.findViewHolderForAdapterPosition(pager.getCurrentItem()) instanceof GifVViewHolder vvh && vvh.playerReady){
|
||||||
|
return vvh.player;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pauseVideo(){
|
||||||
|
MediaPlayer player=findCurrentVideoPlayer();
|
||||||
|
if(player==null || !player.isPlaying())
|
||||||
|
return;
|
||||||
|
player.pause();
|
||||||
|
videoPlayPauseButton.setImageResource(R.drawable.ic_play_24);
|
||||||
|
videoPlayPauseButton.setContentDescription(activity.getString(R.string.play));
|
||||||
|
stopUpdatingVideoPosition();
|
||||||
|
windowView.removeCallbacks(uiAutoHider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resumeVideo(){
|
||||||
|
MediaPlayer player=findCurrentVideoPlayer();
|
||||||
|
if(player==null || player.isPlaying())
|
||||||
|
return;
|
||||||
|
player.start();
|
||||||
|
videoPlayPauseButton.setImageResource(R.drawable.ic_pause_24);
|
||||||
|
videoPlayPauseButton.setContentDescription(activity.getString(R.string.pause));
|
||||||
|
startUpdatingVideoPosition(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startUpdatingVideoPosition(MediaPlayer player){
|
||||||
|
videoInitialPosition=player.getCurrentPosition();
|
||||||
|
videoInitialPositionTime=SystemClock.uptimeMillis();
|
||||||
|
videoDuration=player.getDuration();
|
||||||
|
videoPositionNeedsUpdating=true;
|
||||||
|
windowView.postOnAnimation(videoPositionUpdater);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopUpdatingVideoPosition(){
|
||||||
|
videoPositionNeedsUpdating=false;
|
||||||
|
windowView.removeCallbacks(videoPositionUpdater);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatTime(int timeSec, boolean includeHours){
|
||||||
|
if(includeHours)
|
||||||
|
return String.format(Locale.getDefault(), "%d:%02d:%02d", timeSec/3600, timeSec%3600/60, timeSec%60);
|
||||||
|
else
|
||||||
|
return String.format(Locale.getDefault(), "%d:%02d", timeSec/60, timeSec%60);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateVideoPosition(){
|
||||||
|
if(videoPositionNeedsUpdating){
|
||||||
|
int currentPosition=videoInitialPosition+(int)(SystemClock.uptimeMillis()-videoInitialPositionTime);
|
||||||
|
videoSeekBar.setProgress(Math.round((float)currentPosition/videoDuration*10000f));
|
||||||
|
updateVideoTimeText(currentPosition);
|
||||||
|
windowView.postOnAnimation(videoPositionUpdater);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
private void updateVideoTimeText(int currentPosition){
|
||||||
|
int currentPositionSec=currentPosition/1000;
|
||||||
|
if(currentPositionSec!=videoLastTimeUpdatePosition){
|
||||||
|
videoLastTimeUpdatePosition=currentPositionSec;
|
||||||
|
boolean includeHours=videoDuration>=3600_000;
|
||||||
|
videoTimeView.setText(formatTime(currentPositionSec, includeHours)+" / "+formatTime(videoDuration/1000, includeHours));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +629,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
Drawable getPhotoViewCurrentDrawable(int index);
|
Drawable getPhotoViewCurrentDrawable(int index);
|
||||||
|
|
||||||
void photoViewerDismissed();
|
void photoViewerDismissed();
|
||||||
|
void onRequestPermissions(String[] permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PhotoViewAdapter extends RecyclerView.Adapter<BaseHolder>{
|
private class PhotoViewAdapter extends RecyclerView.Adapter<BaseHolder>{
|
||||||
|
@ -334,13 +724,15 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class GifVViewHolder extends BaseHolder implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, TextureView.SurfaceTextureListener{
|
private class GifVViewHolder extends BaseHolder implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener,
|
||||||
|
MediaPlayer.OnVideoSizeChangedListener, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnInfoListener, MediaPlayer.OnSeekCompleteListener, TextureView.SurfaceTextureListener{
|
||||||
public TextureView textureView;
|
public TextureView textureView;
|
||||||
public FrameLayout wrap;
|
public FrameLayout wrap;
|
||||||
public MediaPlayer player;
|
public MediaPlayer player;
|
||||||
private Surface surface;
|
private Surface surface;
|
||||||
private boolean playerReady;
|
private boolean playerReady;
|
||||||
private boolean keepingScreenOn;
|
private boolean keepingScreenOn;
|
||||||
|
private ProgressBar progressBar;
|
||||||
|
|
||||||
public GifVViewHolder(){
|
public GifVViewHolder(){
|
||||||
textureView=new TextureView(activity);
|
textureView=new TextureView(activity);
|
||||||
|
@ -348,6 +740,10 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
zoomPanView.addView(wrap, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
zoomPanView.addView(wrap, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||||
wrap.addView(textureView);
|
wrap.addView(textureView);
|
||||||
|
|
||||||
|
progressBar=new ProgressBar(activity);
|
||||||
|
progressBar.setIndeterminateTintList(ColorStateList.valueOf(0xffffffff));
|
||||||
|
zoomPanView.addView(progressBar, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
|
||||||
|
|
||||||
textureView.setSurfaceTextureListener(this);
|
textureView.setSurfaceTextureListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,6 +755,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
params.width=item.getWidth();
|
params.width=item.getWidth();
|
||||||
params.height=item.getHeight();
|
params.height=item.getHeight();
|
||||||
wrap.setBackground(listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()));
|
wrap.setBackground(listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()));
|
||||||
|
progressBar.setVisibility(item.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
||||||
if(itemView.isAttachedToWindow()){
|
if(itemView.isAttachedToWindow()){
|
||||||
reset();
|
reset();
|
||||||
prepareAndStartPlayer();
|
prepareAndStartPlayer();
|
||||||
|
@ -369,6 +766,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
public void onPrepared(MediaPlayer mp){
|
public void onPrepared(MediaPlayer mp){
|
||||||
Log.d(TAG, "onPrepared() called with: mp = ["+mp+"]");
|
Log.d(TAG, "onPrepared() called with: mp = ["+mp+"]");
|
||||||
playerReady=true;
|
playerReady=true;
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
if(surface!=null)
|
if(surface!=null)
|
||||||
startPlayer();
|
startPlayer();
|
||||||
}
|
}
|
||||||
|
@ -398,19 +796,24 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
|
|
||||||
private void startPlayer(){
|
private void startPlayer(){
|
||||||
player.setSurface(surface);
|
player.setSurface(surface);
|
||||||
player.setLooping(true);
|
|
||||||
player.start();
|
|
||||||
if(item.type==Attachment.Type.VIDEO){
|
if(item.type==Attachment.Type.VIDEO){
|
||||||
incKeepScreenOn();
|
incKeepScreenOn();
|
||||||
keepingScreenOn=true;
|
keepingScreenOn=true;
|
||||||
|
if(getAbsoluteAdapterPosition()==currentIndex){
|
||||||
|
player.start();
|
||||||
|
startUpdatingVideoPosition(player);
|
||||||
|
hideUiDelayed();
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
keepingScreenOn=false;
|
keepingScreenOn=false;
|
||||||
|
player.setLooping(true);
|
||||||
|
player.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onError(MediaPlayer mp, int what, int extra){
|
public boolean onError(MediaPlayer mp, int what, int extra){
|
||||||
Log.e(TAG, "gif player onError() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
Log.e(TAG, "video player onError() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,6 +823,13 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
players.add(player);
|
players.add(player);
|
||||||
player.setOnPreparedListener(this);
|
player.setOnPreparedListener(this);
|
||||||
player.setOnErrorListener(this);
|
player.setOnErrorListener(this);
|
||||||
|
player.setOnVideoSizeChangedListener(this);
|
||||||
|
if(item.type==Attachment.Type.VIDEO){
|
||||||
|
player.setOnBufferingUpdateListener(this);
|
||||||
|
player.setOnInfoListener(this);
|
||||||
|
player.setOnSeekCompleteListener(this);
|
||||||
|
player.setOnCompletionListener(this);
|
||||||
|
}
|
||||||
try{
|
try{
|
||||||
player.setDataSource(activity, Uri.parse(item.url));
|
player.setDataSource(activity, Uri.parse(item.url));
|
||||||
player.prepareAsync();
|
player.prepareAsync();
|
||||||
|
@ -438,5 +848,53 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||||
keepingScreenOn=false;
|
keepingScreenOn=false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onVideoSizeChanged(MediaPlayer mp, int width, int height){
|
||||||
|
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) wrap.getLayoutParams();
|
||||||
|
params.width=width;
|
||||||
|
params.height=height;
|
||||||
|
zoomPanView.updateLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBufferingUpdate(MediaPlayer mp, int percent){
|
||||||
|
if(getAbsoluteAdapterPosition()==currentIndex){
|
||||||
|
videoSeekBar.setSecondaryProgress(percent*100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onInfo(MediaPlayer mp, int what, int extra){
|
||||||
|
return switch(what){
|
||||||
|
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
|
||||||
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
stopUpdatingVideoPosition();
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
startUpdatingVideoPosition(player);
|
||||||
|
yield true;
|
||||||
|
}
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSeekComplete(MediaPlayer mp){
|
||||||
|
if(getAbsoluteAdapterPosition()==currentIndex && player.isPlaying())
|
||||||
|
startUpdatingVideoPosition(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCompletion(MediaPlayer mp){
|
||||||
|
videoPlayPauseButton.setImageResource(R.drawable.ic_play_24);
|
||||||
|
videoPlayPauseButton.setContentDescription(activity.getString(R.string.play));
|
||||||
|
stopUpdatingVideoPosition();
|
||||||
|
if(!uiVisible)
|
||||||
|
toggleUI();
|
||||||
|
windowView.removeCallbacks(uiAutoHider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||||
private float cropAnimationValue, rawCropAndFadeValue;
|
private float cropAnimationValue, rawCropAndFadeValue;
|
||||||
private float lastFlingVelocityY;
|
private float lastFlingVelocityY;
|
||||||
private float backgroundAlphaForTransition=1f;
|
private float backgroundAlphaForTransition=1f;
|
||||||
|
private boolean forceUpdateLayout;
|
||||||
|
|
||||||
private static final String TAG="ZoomPanView";
|
private static final String TAG="ZoomPanView";
|
||||||
|
|
||||||
|
@ -106,6 +107,8 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||||
@Override
|
@Override
|
||||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
|
||||||
super.onLayout(changed, left, top, right, bottom);
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
if(!changed && child!=null && !forceUpdateLayout)
|
||||||
|
return;
|
||||||
child=getChildAt(0);
|
child=getChildAt(0);
|
||||||
if(child==null)
|
if(child==null)
|
||||||
return;
|
return;
|
||||||
|
@ -120,6 +123,13 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||||
updateViewTransform(false);
|
updateViewTransform(false);
|
||||||
updateLimits(scale);
|
updateLimits(scale);
|
||||||
transX=transY=0;
|
transX=transY=0;
|
||||||
|
if(forceUpdateLayout)
|
||||||
|
forceUpdateLayout=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLayout(){
|
||||||
|
forceUpdateLayout=true;
|
||||||
|
requestLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
private float interpolate(float a, float b, float k){
|
private float interpolate(float a, float b, float k){
|
||||||
|
@ -445,7 +455,8 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onSingleTapConfirmed(MotionEvent e){
|
public boolean onSingleTapConfirmed(MotionEvent e){
|
||||||
return false;
|
listener.onSingleTap();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -589,5 +600,6 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||||
void onStartSwipeToDismissTransition(float velocityY);
|
void onStartSwipeToDismissTransition(float velocityY);
|
||||||
void onSwipeToDismissCanceled();
|
void onSwipeToDismissCanceled();
|
||||||
void onDismissed();
|
void onDismissed();
|
||||||
|
void onSingleTap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:pathData="M18.25 20.5c0.414 0 0.75 0.337 0.75 0.75 0 0.415-0.336 0.75-0.75 0.75l-13 0.005c-0.414 0-0.75-0.336-0.75-0.75s0.336-0.75 0.75-0.75l13-0.004zM11.648 2.014l0.102-0.007c0.38 0 0.694 0.282 0.743 0.648L12.5 2.756 12.499 16.44l3.722-3.72c0.266-0.267 0.683-0.29 0.976-0.073l0.085 0.073c0.266 0.266 0.29 0.683 0.072 0.976l-0.073 0.084-4.997 4.997c-0.266 0.266-0.683 0.29-0.976 0.073l-0.085-0.073-5.003-4.996c-0.293-0.293-0.293-0.768 0-1.061 0.265-0.267 0.682-0.291 0.976-0.073L7.28 12.72l3.719 3.714L11 2.756c0-0.38 0.282-0.694 0.648-0.743l0.102-0.007-0.102 0.007z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:gravity="center_vertical">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/gray_500"/>
|
||||||
|
<corners android:radius="1dp"/>
|
||||||
|
<size android:height="2dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:gravity="center_vertical" android:id="@android:id/secondaryProgress">
|
||||||
|
<clip>
|
||||||
|
<shape android:tint="@color/gray_50">
|
||||||
|
<solid android:color="#40000000"/>
|
||||||
|
<corners android:radius="1dp"/>
|
||||||
|
<size android:height="2dp"/>
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
<item android:gravity="center_vertical" android:id="@android:id/progress">
|
||||||
|
<clip>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/gray_50"/>
|
||||||
|
<corners android:radius="1dp"/>
|
||||||
|
<size android:height="2dp"/>
|
||||||
|
</shape>
|
||||||
|
</clip>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
|
||||||
|
<solid android:color="@color/gray_25"/>
|
||||||
|
<size android:width="18dp" android:height="18dp"/>
|
||||||
|
</shape>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<me.grishka.appkit.views.FragmentRootLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/photo_viewer_overlay"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:theme="@style/Theme.Mastodon.Dark">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/toolbar_wrap"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:background="#80000000">
|
||||||
|
|
||||||
|
<Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?android:attr/actionBarSize"
|
||||||
|
android:elevation="0dp"
|
||||||
|
android:navigationIcon="@drawable/ic_fluent_arrow_left_24_regular"
|
||||||
|
android:navigationContentDescription="@string/back"
|
||||||
|
android:theme="@style/Theme.Mastodon.Toolbar.Profile"
|
||||||
|
android:background="@null"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/video_player_controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="#80000000">
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/seekbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="34dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:max="10000"
|
||||||
|
android:progressDrawable="@drawable/seekbar_video_player"
|
||||||
|
android:thumb="@drawable/seekbar_video_player_thumb"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/play_pause_btn"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_below="@id/seekbar"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:src="@drawable/ic_pause_24"
|
||||||
|
android:tint="@color/gray_50"
|
||||||
|
android:contentDescription="@string/pause"
|
||||||
|
android:background="?android:selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/time"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_below="@id/seekbar"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textAppearance="@style/m3_body_large"
|
||||||
|
android:textColor="#fff"
|
||||||
|
tools:text="1:23 / 4:56"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</me.grishka.appkit.views.FragmentRootLinearLayout>
|
|
@ -298,4 +298,11 @@
|
||||||
<string name="profile_header">Header image</string>
|
<string name="profile_header">Header image</string>
|
||||||
<string name="profile_picture">Profile picture</string>
|
<string name="profile_picture">Profile picture</string>
|
||||||
<string name="reorder">Reorder</string>
|
<string name="reorder">Reorder</string>
|
||||||
|
<string name="download">Download</string>
|
||||||
|
<string name="permission_required">Permission required</string>
|
||||||
|
<string name="storage_permission_to_download">The app needs access to your storage to save this file.</string>
|
||||||
|
<string name="open_settings">Open settings</string>
|
||||||
|
<string name="error_saving_file">Error saving file</string>
|
||||||
|
<string name="file_saved">File saved</string>
|
||||||
|
<string name="downloading">Downloading…</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue