Gifv player

This commit is contained in:
Grishka
2022-02-08 23:37:48 +03:00
parent 990f364bf9
commit 90bd7baa94
10 changed files with 362 additions and 81 deletions

View File

@@ -15,6 +15,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PhotoStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
@@ -122,18 +123,18 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex){
final Status status=_status.reblog!=null ? _status.reblog : _status;
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
private PhotoStatusDisplayItem.Holder transitioningHolder;
private ImageStatusDisplayItem.Holder transitioningHolder;
@Override
public void setPhotoViewVisibility(int index, boolean visible){
PhotoStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
if(holder!=null)
holder.photo.setAlpha(visible ? 1f : 0f);
}
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
PhotoStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
if(holder!=null){
transitioningHolder=holder;
View view=transitioningHolder.photo;
@@ -170,7 +171,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public Drawable getPhotoViewCurrentDrawable(int index){
PhotoStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
ImageStatusDisplayItem.Holder holder=findPhotoViewHolder(index);
if(holder!=null)
return holder.photo.getDrawable();
return null;
@@ -181,14 +182,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentPhotoViewer=null;
}
private PhotoStatusDisplayItem.Holder findPhotoViewHolder(int index){
private ImageStatusDisplayItem.Holder findPhotoViewHolder(int index){
int offset=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(parentID)){
if(item instanceof PhotoStatusDisplayItem){
if(item instanceof ImageStatusDisplayItem){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(getMainAdapterOffset()+offset+index);
if(holder instanceof PhotoStatusDisplayItem.Holder){
return (PhotoStatusDisplayItem.Holder) holder;
if(holder instanceof ImageStatusDisplayItem.Holder){
return (ImageStatusDisplayItem.Holder) holder;
}
return null;
}
@@ -264,10 +265,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
position-=getMainAdapterOffset();
if(position>=0 && position<displayItems.size()){
StatusDisplayItem item=displayItems.get(position);
if(item instanceof PhotoStatusDisplayItem){
int total=((PhotoStatusDisplayItem) item).totalPhotos;
if(item instanceof ImageStatusDisplayItem){
int total=((ImageStatusDisplayItem) item).totalPhotos;
if(total>1){
int index=((PhotoStatusDisplayItem) item).index;
int index=((ImageStatusDisplayItem) item).index;
return 1;
}
}

View File

@@ -0,0 +1,41 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.graphics.Outline;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public class GifVStatusDisplayItem extends ImageStatusDisplayItem{
public GifVStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos){
super(parentID, parentFragment, photo, status, index, totalPhotos);
request=new UrlImageLoaderRequest(photo.previewUrl, 1000, 1000);
}
@Override
public Type getType(){
return Type.GIFV;
}
public static class Holder extends ImageStatusDisplayItem.Holder<GifVStatusDisplayItem>{
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_gifv, parent);
View play=findViewById(R.id.play_button);
play.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setOval(0, 0, view.getWidth(), view.getHeight());
outline.setAlpha(.99f); // fixes shadow rendering
}
});
}
}
}

View File

@@ -0,0 +1,79 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.app.Fragment;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import androidx.annotation.LayoutRes;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
public final int index;
public final int totalPhotos;
protected Attachment attachment;
protected ImageLoaderRequest request;
protected Fragment parentFragment;
protected Status status;
public ImageStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Attachment photo, Status status, int index, int totalPhotos){
super(parentID, parentFragment);
this.attachment=photo;
this.parentFragment=parentFragment;
this.status=status;
this.index=index;
this.totalPhotos=totalPhotos;
}
@Override
public int getImageCount(){
return 1;
}
@Override
public ImageLoaderRequest getImageRequest(int index){
return request;
}
public static abstract class Holder<T extends ImageStatusDisplayItem> extends StatusDisplayItem.Holder<T> implements ImageLoaderViewHolder{
public final ImageView photo;
public Holder(Activity activity, @LayoutRes int layout, ViewGroup parent){
super(activity, layout, parent);
photo=findViewById(R.id.photo);
photo.setOnClickListener(this::onViewClick);
}
@Override
public void onBind(ImageStatusDisplayItem item){
}
@Override
public void setImage(int index, Drawable drawable){
photo.setImageDrawable(drawable);
}
@Override
public void clearImage(int index){
photo.setImageDrawable(item.attachment.blurhashPlaceholder);
}
private void onViewClick(View v){
if(item.parentFragment instanceof PhotoViewerHost){
Status contentStatus=item.status.reblog!=null ? item.status.reblog : item.status;
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, contentStatus.mediaAttachments.indexOf(item.attachment));
}
}
}
}

View File

@@ -1,37 +1,19 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.app.Fragment;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
public class PhotoStatusDisplayItem extends StatusDisplayItem{
private Attachment attachment;
private ImageLoaderRequest request;
private Fragment parentFragment;
private Status status;
public final int index, totalPhotos;
public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos){
super(parentID, parentFragment);
this.status=status;
this.attachment=photo;
super(parentID, parentFragment, photo, status, index, totalPhotos);
request=new UrlImageLoaderRequest(photo.url, 1000, 1000);
this.parentFragment=parentFragment;
this.index=index;
this.totalPhotos=totalPhotos;
}
@Override
@@ -39,44 +21,10 @@ public class PhotoStatusDisplayItem extends StatusDisplayItem{
return Type.PHOTO;
}
@Override
public int getImageCount(){
return 1;
}
public static class Holder extends ImageStatusDisplayItem.Holder<PhotoStatusDisplayItem>{
@Override
public ImageLoaderRequest getImageRequest(int index){
return request;
}
public static class Holder extends StatusDisplayItem.Holder<PhotoStatusDisplayItem> implements ImageLoaderViewHolder{
public final ImageView photo;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_photo, parent);
photo=findViewById(R.id.photo);
photo.setOnClickListener(this::onViewClick);
}
@Override
public void onBind(PhotoStatusDisplayItem item){
}
@Override
public void setImage(int index, Drawable drawable){
photo.setImageDrawable(drawable);
}
@Override
public void clearImage(int index){
photo.setImageDrawable(item.attachment.blurhashPlaceholder);
}
private void onViewClick(View v){
if(item.parentFragment instanceof PhotoViewerHost){
Status contentStatus=item.status.reblog!=null ? item.status.reblog : item.status;
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, contentStatus.mediaAttachments.indexOf(item.attachment));
}
}
}
}

View File

@@ -45,6 +45,7 @@ public abstract class StatusDisplayItem{
case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent);
case TEXT -> new TextStatusDisplayItem.Holder(activity, parent);
case PHOTO -> new PhotoStatusDisplayItem.Holder(activity, parent);
case GIFV -> new GifVStatusDisplayItem.Holder(activity, parent);
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
default -> throw new UnsupportedOperationException();
};
@@ -63,7 +64,7 @@ public abstract class StatusDisplayItem{
int photoIndex=0;
int totalPhotos=0;
for(Attachment attachment:statusForContent.mediaAttachments){
if(attachment.type==Attachment.Type.IMAGE){
if(attachment.type==Attachment.Type.IMAGE || attachment.type==Attachment.Type.GIFV){
totalPhotos++;
}
}
@@ -71,6 +72,9 @@ public abstract class StatusDisplayItem{
if(attachment.type==Attachment.Type.IMAGE){
items.add(new PhotoStatusDisplayItem(parentID, status, attachment, fragment, photoIndex, totalPhotos));
photoIndex++;
}else if(attachment.type==Attachment.Type.GIFV){
items.add(new GifVStatusDisplayItem(parentID, status, attachment, fragment, photoIndex, totalPhotos));
photoIndex++;
}
}
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));

View File

@@ -3,10 +3,16 @@ package org.joinmastodon.android.ui.photoviewer;
import android.app.Activity;
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.MediaPlayer;
import android.net.Uri;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
@@ -16,6 +22,8 @@ import android.widget.ImageView;
import org.joinmastodon.android.model.Attachment;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
@@ -28,6 +36,8 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator;
public class PhotoViewer implements ZoomPanView.Listener{
private static final String TAG="PhotoViewer";
private Activity activity;
private List<Attachment> attachments;
private int currentIndex;
@@ -37,6 +47,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
private FrameLayout windowView;
private ViewPager2 pager;
private ColorDrawable background=new ColorDrawable(0xff000000);
private ArrayList<MediaPlayer> players=new ArrayList<>();
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Listener listener){
this.activity=activity;
@@ -79,7 +90,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
int[] radius=new int[4];
if(listener.startPhotoViewTransition(index, rect, radius)){
RecyclerView rv=(RecyclerView) pager.getChildAt(0);
PhotoViewHolder holder=(PhotoViewHolder) rv.findViewHolderForAdapterPosition(index);
BaseHolder holder=(BaseHolder) rv.findViewHolderForAdapterPosition(index);
holder.zoomPanView.animateIn(rect, radius);
}
@@ -121,7 +132,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
int[] radius=new int[4];
if(listener.startPhotoViewTransition(index, rect, radius)){
RecyclerView rv=(RecyclerView) pager.getChildAt(0);
PhotoViewHolder holder=(PhotoViewHolder) rv.findViewHolderForAdapterPosition(index);
BaseHolder holder=(BaseHolder) rv.findViewHolderForAdapterPosition(index);
holder.zoomPanView.animateOut(rect, radius, velocityY);
}else{
windowView.animate()
@@ -140,6 +151,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
@Override
public void onDismissed(){
for(MediaPlayer player:players)
player.release();
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
wm.removeView(windowView);
listener.photoViewerDismissed();
@@ -193,16 +206,20 @@ public class PhotoViewer implements ZoomPanView.Listener{
void photoViewerDismissed();
}
private class PhotoViewAdapter extends RecyclerView.Adapter<PhotoViewHolder>{
private class PhotoViewAdapter extends RecyclerView.Adapter<BaseHolder>{
@NonNull
@Override
public PhotoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new PhotoViewHolder();
public BaseHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return switch(viewType){
case 0 -> new PhotoViewHolder();
case 1 -> new GifVViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@Override
public void onBindViewHolder(@NonNull PhotoViewHolder holder, int position){
public void onBindViewHolder(@NonNull BaseHolder holder, int position){
holder.bind(attachments.get(position));
}
@@ -210,27 +227,63 @@ public class PhotoViewer implements ZoomPanView.Listener{
public int getItemCount(){
return attachments.size();
}
@Override
public int getItemViewType(int position){
Attachment att=attachments.get(position);
return switch(att.type){
case IMAGE -> 0;
case GIFV -> 1;
default -> throw new IllegalStateException("Unexpected value: "+att.type);
};
}
@Override
public void onViewDetachedFromWindow(@NonNull BaseHolder holder){
super.onViewDetachedFromWindow(holder);
if(holder instanceof GifVViewHolder){
((GifVViewHolder) holder).reset();
}
}
@Override
public void onViewAttachedToWindow(@NonNull BaseHolder holder){
super.onViewAttachedToWindow(holder);
if(holder instanceof GifVViewHolder){
((GifVViewHolder) holder).prepareAndStartPlayer();
}
}
}
private class PhotoViewHolder extends BindableViewHolder<Attachment> implements ViewImageLoader.Target{
public ImageView imageView;
private abstract class BaseHolder extends BindableViewHolder<Attachment>{
public ZoomPanView zoomPanView;
public PhotoViewHolder(){
public BaseHolder(){
super(new ZoomPanView(activity));
zoomPanView=(ZoomPanView) itemView;
zoomPanView.setListener(PhotoViewer.this);
itemView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
imageView=new ImageView(activity);
((FrameLayout)itemView).addView(imageView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
zoomPanView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
@Override
public void onBind(Attachment item){
zoomPanView.setScrollDirections(getAbsoluteAdapterPosition()>0, getAbsoluteAdapterPosition()<attachments.size()-1);
}
}
private class PhotoViewHolder extends BaseHolder implements ViewImageLoader.Target{
public ImageView imageView;
public PhotoViewHolder(){
imageView=new ImageView(activity);
zoomPanView.addView(imageView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
}
@Override
public void onBind(Attachment item){
super.onBind(item);
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) imageView.getLayoutParams();
params.width=item.getWidth();
params.height=item.getHeight();
zoomPanView.setScrollDirections(getAbsoluteAdapterPosition()>0, getAbsoluteAdapterPosition()<attachments.size()-1);
ViewImageLoader.load(this, listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()), new UrlImageLoaderRequest(item.url), false);
}
@@ -244,4 +297,98 @@ public class PhotoViewer implements ZoomPanView.Listener{
return imageView;
}
}
private class GifVViewHolder extends BaseHolder implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, TextureView.SurfaceTextureListener{
public TextureView textureView;
public FrameLayout wrap;
public MediaPlayer player;
private Surface surface;
private boolean playerReady;
public GifVViewHolder(){
textureView=new TextureView(activity);
wrap=new FrameLayout(activity);
zoomPanView.addView(wrap, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
wrap.addView(textureView);
textureView.setSurfaceTextureListener(this);
}
@Override
public void onBind(Attachment item){
super.onBind(item);
playerReady=false;
FrameLayout.LayoutParams params=(FrameLayout.LayoutParams) wrap.getLayoutParams();
params.width=item.getWidth();
params.height=item.getHeight();
wrap.setBackground(listener.getPhotoViewCurrentDrawable(getAbsoluteAdapterPosition()));
if(itemView.isAttachedToWindow()){
prepareAndStartPlayer();
}
}
@Override
public void onPrepared(MediaPlayer mp){
Log.d(TAG, "onPrepared() called with: mp = ["+mp+"]");
playerReady=true;
if(surface!=null)
startPlayer();
}
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height){
this.surface=new Surface(surface);
if(playerReady)
startPlayer();
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height){
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface){
this.surface=null;
return true;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface){
}
private void startPlayer(){
player.setSurface(surface);
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+"]");
return false;
}
public void prepareAndStartPlayer(){
playerReady=false;
player=new MediaPlayer();
players.add(player);
player.setOnPreparedListener(this);
player.setOnErrorListener(this);
try{
player.setDataSource(activity, Uri.parse(item.url));
player.prepareAsync();
}catch(IOException x){
Log.w(TAG, "Error initializing gif player", x);
}
}
public void reset(){
playerReady=false;
player.release();
players.remove(player);
player=null;
}
}
}