Photo viewer & video player UI
This commit is contained in:
@@ -1,16 +1,34 @@
|
||||
package org.joinmastodon.android.ui.photoviewer;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
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.Rect;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.MediaScannerConnection;
|
||||
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.view.DisplayCutout;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Surface;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
@@ -19,27 +37,47 @@ import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
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.api.MastodonAPIController;
|
||||
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.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
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{
|
||||
private static final String TAG="PhotoViewer";
|
||||
public static final int PERMISSION_REQUEST=926;
|
||||
|
||||
private Activity activity;
|
||||
private List<Attachment> attachments;
|
||||
@@ -48,10 +86,28 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
private Listener listener;
|
||||
|
||||
private FrameLayout windowView;
|
||||
private FragmentRootLinearLayout uiOverlay;
|
||||
private ViewPager2 pager;
|
||||
private ColorDrawable background=new ColorDrawable(0xff000000);
|
||||
private ArrayList<MediaPlayer> players=new ArrayList<>();
|
||||
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){
|
||||
this.activity=activity;
|
||||
@@ -75,7 +131,26 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
|
||||
@Override
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -84,15 +159,47 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
pager=new ViewPager2(activity);
|
||||
pager.setAdapter(new PhotoViewAdapter());
|
||||
pager.setCurrentItem(index, false);
|
||||
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
|
||||
@Override
|
||||
public void onPageSelected(int position){
|
||||
onPageChanged(position);
|
||||
}
|
||||
});
|
||||
windowView.addView(pager);
|
||||
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);
|
||||
wlp.type=WindowManager.LayoutParams.TYPE_APPLICATION;
|
||||
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
|
||||
| WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
|
||||
wlp.format=PixelFormat.TRANSLUCENT;
|
||||
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);
|
||||
wm.addView(windowView, wlp);
|
||||
|
||||
@@ -112,6 +219,44 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
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
|
||||
@@ -127,15 +272,22 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
@Override
|
||||
public void onSetBackgroundAlpha(float alpha){
|
||||
background.setAlpha(Math.round(alpha*255f));
|
||||
uiOverlay.setAlpha(Math.max(0f, alpha*2f-1f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartSwipeToDismiss(){
|
||||
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
|
||||
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
|
||||
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
||||
wlp.flags|=WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||||
@@ -163,17 +315,74 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
@Override
|
||||
public void onSwipeToDismissCanceled(){
|
||||
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
|
||||
public void onDismissed(){
|
||||
for(MediaPlayer player:players)
|
||||
player.release();
|
||||
if(!players.isEmpty()){
|
||||
activity.getSystemService(AudioManager.class).abandonAudioFocus(audioFocusListener);
|
||||
}
|
||||
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
|
||||
wm.removeView(windowView);
|
||||
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
|
||||
* @param x
|
||||
@@ -189,6 +398,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
||||
wlp.flags|=WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
||||
wm.updateViewLayout(windowView, wlp);
|
||||
activity.getSystemService(AudioManager.class).requestAudioFocus(audioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
|
||||
}
|
||||
screenOnRefCount++;
|
||||
}
|
||||
@@ -201,6 +411,185 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
|
||||
wlp.flags&=~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
||||
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);
|
||||
|
||||
void photoViewerDismissed();
|
||||
void onRequestPermissions(String[] permissions);
|
||||
}
|
||||
|
||||
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 FrameLayout wrap;
|
||||
public MediaPlayer player;
|
||||
private Surface surface;
|
||||
private boolean playerReady;
|
||||
private boolean keepingScreenOn;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
public GifVViewHolder(){
|
||||
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));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -359,6 +755,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
params.width=item.getWidth();
|
||||
params.height=item.getHeight();
|
||||
wrap.setBackground(listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()));
|
||||
progressBar.setVisibility(item.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
|
||||
if(itemView.isAttachedToWindow()){
|
||||
reset();
|
||||
prepareAndStartPlayer();
|
||||
@@ -369,6 +766,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
public void onPrepared(MediaPlayer mp){
|
||||
Log.d(TAG, "onPrepared() called with: mp = ["+mp+"]");
|
||||
playerReady=true;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
if(surface!=null)
|
||||
startPlayer();
|
||||
}
|
||||
@@ -398,19 +796,24 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
|
||||
private void startPlayer(){
|
||||
player.setSurface(surface);
|
||||
player.setLooping(true);
|
||||
player.start();
|
||||
if(item.type==Attachment.Type.VIDEO){
|
||||
incKeepScreenOn();
|
||||
keepingScreenOn=true;
|
||||
if(getAbsoluteAdapterPosition()==currentIndex){
|
||||
player.start();
|
||||
startUpdatingVideoPosition(player);
|
||||
hideUiDelayed();
|
||||
}
|
||||
}else{
|
||||
keepingScreenOn=false;
|
||||
player.setLooping(true);
|
||||
player.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -420,6 +823,13 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
players.add(player);
|
||||
player.setOnPreparedListener(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{
|
||||
player.setDataSource(activity, Uri.parse(item.url));
|
||||
player.prepareAsync();
|
||||
@@ -438,5 +848,53 @@ public class PhotoViewer implements ZoomPanView.Listener{
|
||||
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 lastFlingVelocityY;
|
||||
private float backgroundAlphaForTransition=1f;
|
||||
private boolean forceUpdateLayout;
|
||||
|
||||
private static final String TAG="ZoomPanView";
|
||||
|
||||
@@ -106,6 +107,8 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
if(!changed && child!=null && !forceUpdateLayout)
|
||||
return;
|
||||
child=getChildAt(0);
|
||||
if(child==null)
|
||||
return;
|
||||
@@ -120,6 +123,13 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
updateViewTransform(false);
|
||||
updateLimits(scale);
|
||||
transX=transY=0;
|
||||
if(forceUpdateLayout)
|
||||
forceUpdateLayout=false;
|
||||
}
|
||||
|
||||
public void updateLayout(){
|
||||
forceUpdateLayout=true;
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private float interpolate(float a, float b, float k){
|
||||
@@ -445,7 +455,8 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e){
|
||||
return false;
|
||||
listener.onSingleTap();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -589,5 +600,6 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
|
||||
void onStartSwipeToDismissTransition(float velocityY);
|
||||
void onSwipeToDismissCanceled();
|
||||
void onDismissed();
|
||||
void onSingleTap();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user