Media layout

This commit is contained in:
Grishka 2022-02-19 02:32:08 +03:00
parent bb463aa10a
commit 6cc6fe195b
15 changed files with 554 additions and 42 deletions

View File

@ -22,6 +22,8 @@ import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.TileGridLayoutManager;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem;
@ -32,6 +34,7 @@ import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout;
import java.util.ArrayList;
import java.util.Collections;
@ -193,6 +196,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void endPhotoViewTransition(){
// fix drawable callback
Drawable d=transitioningHolder.photo.getDrawable();
transitioningHolder.photo.setImageDrawable(null);
transitioningHolder.photo.setImageDrawable(d);
View view=transitioningHolder.photo;
view.setTranslationX(0f);
view.setTranslationY(0f);
@ -265,6 +273,46 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
}
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof ImageStatusDisplayItem.Holder){
int width=Math.min(parent.getWidth(), V.dp(ImageAttachmentFrameLayout.MAX_WIDTH));
PhotoLayoutHelper.TiledLayoutResult layout=((ImageStatusDisplayItem.Holder<?>) holder).getItem().tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=((ImageStatusDisplayItem.Holder<?>) holder).getItem().thisTile;
if(tile.startCol+tile.colSpan<layout.columnSizes.length){
outRect.right=V.dp(1);
}
if(tile.startRow+tile.rowSpan<layout.rowSizes.length){
outRect.bottom=V.dp(1);
}
// For a view that spans rows, compensate its additional height so the row it's in stays the right height
if(tile.rowSpan>1){
outRect.bottom=-(Math.round(tile.height/1000f*width)-Math.round(layout.rowSizes[tile.startRow]/1000f*width));
}
// ...and for its siblings, offset those on rows below first to the right where they belong
if(tile.startCol>0 && layout.tiles[0].rowSpan>1 && tile.startRow>layout.tiles[0].startRow){
int xOffset=Math.round(layout.tiles[0].width/1000f*parent.getWidth());
outRect.left=xOffset;
outRect.right=-xOffset;
}
// If the width of the media block is smaller than that of the RecyclerView, offset the views horizontally to center them
if(parent.getWidth()>width){
outRect.left+=(parent.getWidth()-V.dp(ImageAttachmentFrameLayout.MAX_WIDTH))/2;
if(tile.startCol>0){
int spanOffset=0;
for(int i=0;i<tile.startCol;i++){
spanOffset+=layout.columnSizes[i];
}
outRect.left-=Math.round(spanOffset/1000f*parent.getWidth());
outRect.left+=Math.round(spanOffset/1000f*width);
}
}
}
}
});
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
@Override
@ -295,7 +343,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
protected RecyclerView.LayoutManager onCreateLayoutManager(){
GridLayoutManager lm=new GridLayoutManager(getActivity(), 2);
GridLayoutManager lm=new TileGridLayoutManager(getActivity(), 1000);
lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
@Override
public int getSpanSize(int position){
@ -303,14 +351,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if(position>=0 && position<displayItems.size()){
StatusDisplayItem item=displayItems.get(position);
if(item instanceof ImageStatusDisplayItem){
int total=((ImageStatusDisplayItem) item).totalPhotos;
if(total>1){
int index=((ImageStatusDisplayItem) item).index;
return 1;
PhotoLayoutHelper.TiledLayoutResult layout=((ImageStatusDisplayItem) item).tiledLayout;
PhotoLayoutHelper.TiledLayoutResult.Tile tile=((ImageStatusDisplayItem) item).thisTile;
int spans=0;
for(int i=0;i<tile.colSpan;i++){
spans+=layout.columnSizes[tile.startCol+i];
}
return spans;
}
}
return 2;
return 1000;
}
});
return lm;
@ -320,6 +370,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
updateToolbar();
list.invalidateItemDecorations();
}
private void updateToolbar(){

View File

@ -1,18 +1,22 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class NotificationsFragment extends BaseStatusListFragment<Notification>{
@ -71,7 +75,16 @@ public class NotificationsFragment extends BaseStatusListFragment<Notification>{
@Override
public void onItemClick(String id){
Notification n=getNotificationByID(id);
if(n.status!=null){
Status status=n.status;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(status));
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
Nav.go(getActivity(), ThreadFragment.class, args);
}
}
@Override

View File

@ -147,6 +147,8 @@ public class Account extends BaseModel{
}
if(moved!=null)
moved.postprocess();
if(TextUtils.isEmpty(displayName))
displayName=username;
}
public boolean isLocal(){

View File

@ -11,6 +11,8 @@ import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.ui.utils.BlurHashDecoder;
import org.joinmastodon.android.ui.utils.BlurHashDrawable;
import org.parceler.Parcel;
import org.parceler.ParcelConstructor;
import org.parceler.ParcelProperty;
@Parcel
public class Attachment extends BaseModel{
@ -23,11 +25,24 @@ public class Attachment extends BaseModel{
public String previewUrl;
public String remoteUrl;
public String description;
@ParcelProperty("blurhash")
public String blurhash;
public Metadata meta;
public transient Drawable blurhashPlaceholder;
public Attachment(){}
@ParcelConstructor
public Attachment(@ParcelProperty("blurhash") String blurhash){
this.blurhash=blurhash;
if(blurhash!=null){
Bitmap placeholder=BlurHashDecoder.decode(blurhash, 16, 16);
if(placeholder!=null)
blurhashPlaceholder=new BlurHashDrawable(placeholder, getWidth(), getHeight());
}
}
public int getWidth(){
if(meta==null)
return 0;
@ -86,7 +101,11 @@ public class Attachment extends BaseModel{
@SerializedName("audio")
AUDIO,
@SerializedName("unknown")
UNKNOWN
UNKNOWN;
public boolean isImage(){
return this==IMAGE || this==GIFV || this==VIDEO;
}
}
@Parcel

View File

@ -0,0 +1,338 @@
package org.joinmastodon.android.ui;
import org.joinmastodon.android.model.Attachment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import androidx.annotation.NonNull;
public class PhotoLayoutHelper{
@NonNull
public static TiledLayoutResult processThumbs(int _maxW, int _maxH, List<Attachment> thumbs){
TiledLayoutResult result=new TiledLayoutResult();
if(thumbs.size()==1){
Attachment att=thumbs.get(0);
result.rowSizes=result.columnSizes=new int[]{1};
if(att.getWidth()>att.getHeight()){
result.width=_maxW;
result.height=Math.round(att.getHeight()/(float)att.getWidth()*_maxW);
}else{
result.height=_maxH;
result.width=Math.round(att.getWidth()/(float)att.getHeight()*_maxH);
}
result.tiles=new TiledLayoutResult.Tile[]{new TiledLayoutResult.Tile(1, 1, result.width, result.height, 0, 0)};
}else if(thumbs.size()==0){
throw new IllegalArgumentException("Empty thumbs array");
}
String orients="";
ArrayList<Float> ratios=new ArrayList<Float>();
int cnt=thumbs.size();
for(Attachment thumb : thumbs){
// float ratio=thumb.isSizeKnown() ? thumb.getWidth()/(float) thumb.getHeight() : 1f;
float ratio=thumb.getWidth()/(float) thumb.getHeight();
char orient=ratio>1.2 ? 'w' : (ratio<0.8 ? 'n' : 'q');
orients+=orient;
ratios.add(ratio);
}
float avgRatio=!ratios.isEmpty() ? sum(ratios)/ratios.size() : 1.0f;
float maxW, maxH, marginW=0, marginH=0;
if(_maxW>0){
maxW=_maxW;
maxH=_maxH;
}else{
maxW=510;
maxH=510;
}
float maxRatio=maxW/maxH;
if(cnt==2){
if(orients.equals("ww") && avgRatio>1.4*maxRatio && (ratios.get(1)-ratios.get(0))<0.2){ // two wide photos, one above the other
float h=Math.min(maxW/ratios.get(0), Math.min(maxW/ratios.get(1), (maxH-marginH)/2.0f));
result.width=Math.round(maxW);
result.height=Math.round(h*2+marginH);
result.columnSizes=new int[]{result.width};
result.rowSizes=new int[]{Math.round(h), Math.round(h)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 1, maxW, h, 0, 0),
new TiledLayoutResult.Tile(1, 1, maxW, h, 0, 1)
};
}else if(orients.equals("ww") || orients.equals("qq")){ // next to each other, same ratio
float w=((maxW-marginW)/2);
float h=Math.min(w/ratios.get(0), Math.min(w/ratios.get(1), maxH));
result.width=Math.round(maxW);
result.height=Math.round(h);
result.columnSizes=new int[]{Math.round(w), _maxW-Math.round(w)};
result.rowSizes=new int[]{Math.round(h)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 1, w, h, 0, 0),
new TiledLayoutResult.Tile(1, 1, w, h, 1, 0)
};
}else{ // next to each other, different ratios
float w0=((maxW-marginW)/ratios.get(1)/(1/ratios.get(0)+1/ratios.get(1)));
float w1=(maxW-w0-marginW);
float h=Math.min(maxH, Math.min(w0/ratios.get(0), w1/ratios.get(1)));
result.columnSizes=new int[]{Math.round(w0), Math.round(w1)};
result.rowSizes=new int[]{Math.round(h)};
result.width=Math.round(w0+w1+marginW);
result.height=Math.round(h);
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 1, w0, h, 0, 0),
new TiledLayoutResult.Tile(1, 1, w1, h, 1, 0)
};
}
}else if(cnt==3){
if(/*(ratios.get(0) > 1.2 * maxRatio || avgRatio > 1.5 * maxRatio) &&*/ orients.equals("www")){ // 2nd and 3rd photos are on the next line
float hCover=Math.min(maxW/ratios.get(0), (maxH-marginH)*0.66f);
float w2=((maxW-marginW)/2);
float h=Math.min(maxH-hCover-marginH, Math.min(w2/ratios.get(1), w2/ratios.get(2)));
result.width=Math.round(maxW);
result.height=Math.round(hCover+h+marginH);
result.columnSizes=new int[]{Math.round(w2), _maxW-Math.round(w2)};
result.rowSizes=new int[]{Math.round(hCover), Math.round(h)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(2, 1, maxW, hCover, 0, 0),
new TiledLayoutResult.Tile(1, 1, w2, h, 0, 1),
new TiledLayoutResult.Tile(1, 1, w2, h, 1, 1)
};
}else{ // 2nd and 3rd photos are on the right part
float wCover=Math.min(maxH*ratios.get(0), (maxW-marginW)*0.75f);
float h1=(ratios.get(1)*(maxH-marginH)/(ratios.get(2)+ratios.get(1)));
float h0=(maxH-h1-marginH);
float w=Math.min(maxW-wCover-marginW, Math.min(h1*ratios.get(2), h0*ratios.get(1)));
result.width=Math.round(wCover+w+marginW);
result.height=Math.round(maxH);
result.columnSizes=new int[]{Math.round(wCover), Math.round(w)};
result.rowSizes=new int[]{Math.round(h0), Math.round(h1)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 2, wCover, maxH, 0, 0),
new TiledLayoutResult.Tile(1, 1, w, h0, 1, 0),
new TiledLayoutResult.Tile(1, 1, w, h1, 1, 1)
};
}
}else if(cnt==4){
if(/*(ratios.get(0) > 1.2 * maxRatio || avgRatio > 1.5 * maxRatio) &&*/ orients.equals("wwww")){ // 2nd, 3rd and 4th photos are on the next line
float hCover=Math.min(maxW/ratios.get(0), (maxH-marginH)*0.66f);
float h=(maxW-2*marginW)/(ratios.get(1)+ratios.get(2)+ratios.get(3));
float w0=h*ratios.get(1);
float w1=h*ratios.get(2);
float w2=h*ratios.get(3);
h=Math.min(maxH-hCover-marginH, h);
result.width=Math.round(maxW);
result.height=Math.round(hCover+h+marginH);
result.columnSizes=new int[]{Math.round(w0), Math.round(w1), _maxW-Math.round(w0)-Math.round(w1)};
result.rowSizes=new int[]{Math.round(hCover), Math.round(h)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(3, 1, maxW, hCover, 0, 0),
new TiledLayoutResult.Tile(1, 1, w0, h, 0, 1),
new TiledLayoutResult.Tile(1, 1, w1, h, 1, 1),
new TiledLayoutResult.Tile(1, 1, w2, h, 2, 1),
};
}else{ // 2nd, 3rd and 4th photos are on the right part
float wCover= Math.min(maxH*ratios.get(0), (maxW-marginW)*0.66f);
float w=(maxH-2*marginH)/(1/ratios.get(1)+1/ratios.get(2)+1/ratios.get(3));
float h0=w/ratios.get(1);
float h1=w/ratios.get(2);
float h2=w/ratios.get(3)+marginH;
w=Math.min(maxW-wCover-marginW, w);
result.width=Math.round(wCover+marginW+w);
result.height=Math.round(maxH);
result.columnSizes=new int[]{Math.round(wCover), Math.round(w)};
result.rowSizes=new int[]{Math.round(h0), Math.round(h1), Math.round(h2)};
result.tiles=new TiledLayoutResult.Tile[]{
new TiledLayoutResult.Tile(1, 3, wCover, maxH, 0, 0),
new TiledLayoutResult.Tile(1, 1, w, h0, 1, 0),
new TiledLayoutResult.Tile(1, 1, w, h1, 1, 1),
new TiledLayoutResult.Tile(1, 1, w, h2, 1, 2),
};
}
}else{
ArrayList<Float> ratiosCropped=new ArrayList<Float>();
if(avgRatio>1.1){
for(float ratio : ratios){
ratiosCropped.add(Math.max(1.0f, ratio));
}
}else{
for(float ratio : ratios){
ratiosCropped.add(Math.min(1.0f, ratio));
}
}
HashMap<int[], float[]> tries=new HashMap<>();
// One line
int firstLine, secondLine, thirdLine;
tries.put(new int[]{firstLine=cnt}, new float[]{calculateMultiThumbsHeight(ratiosCropped, maxW, marginW)});
// Two lines
for(firstLine=1; firstLine<=cnt-1; firstLine++){
tries.put(new int[]{firstLine, secondLine=cnt-firstLine}, new float[]{
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), maxW, marginW),
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, ratiosCropped.size()), maxW, marginW)
}
);
}
// Three lines
for(firstLine=1; firstLine<=cnt-2; firstLine++){
for(secondLine=1; secondLine<=cnt-firstLine-1; secondLine++){
tries.put(new int[]{firstLine, secondLine, thirdLine=cnt-firstLine-secondLine}, new float[]{
calculateMultiThumbsHeight(ratiosCropped.subList(0, firstLine), maxW, marginW),
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine, firstLine+secondLine), maxW, marginW),
calculateMultiThumbsHeight(ratiosCropped.subList(firstLine+secondLine, ratiosCropped.size()), maxW, marginW)
}
);
}
}
// Looking for minimum difference between thumbs block height and maxH (may probably be little over)
int[] optConf=null;
float optDiff=0;
for(int[] conf : tries.keySet()){
float[] heights=tries.get(conf);
float confH=marginH*(heights.length-1);
for(float h : heights) confH+=h;
float confDiff=Math.abs(confH-maxH);
if(conf.length>1){
if(conf[0]>conf[1] || conf.length>2 && conf[1]>conf[2]){
confDiff*=1.1;
}
}
if(optConf==null || confDiff<optDiff){
optConf=conf;
optDiff=confDiff;
}
}
ArrayList<Attachment> thumbsRemain=new ArrayList<>(thumbs);
ArrayList<Float> ratiosRemain=new ArrayList<>(ratiosCropped);
float[] optHeights=tries.get(optConf);
int k=0;
result.width=Math.round(maxW);
result.rowSizes=new int[optHeights.length];
result.tiles=new TiledLayoutResult.Tile[thumbs.size()];
float totalHeight=0f;
ArrayList<Integer> gridLineOffsets=new ArrayList<>();
ArrayList<ArrayList<TiledLayoutResult.Tile>> rowTiles=new ArrayList<>(optHeights.length);
for(int i=0; i<optConf.length; i++){
int lineChunksNum=optConf[i];
ArrayList<Attachment> lineThumbs=new ArrayList<>();
for(int j=0; j<lineChunksNum; j++) lineThumbs.add(thumbsRemain.remove(0));
float lineHeight=optHeights[i];
totalHeight+=lineHeight;
result.rowSizes[i]=Math.round(lineHeight);
int totalWidth=0;
ArrayList<TiledLayoutResult.Tile> row=new ArrayList<>();
for(int j=0; j<lineThumbs.size(); j++){
float thumb_ratio=ratiosRemain.remove(0);
float w=j==lineThumbs.size()-1 ? (maxW-totalWidth) : (thumb_ratio*lineHeight);
totalWidth+=Math.round(w);
if(j<lineThumbs.size()-1 && !gridLineOffsets.contains(totalWidth))
gridLineOffsets.add(totalWidth);
TiledLayoutResult.Tile tile=new TiledLayoutResult.Tile(1, 1, w, lineHeight, 0, i);
result.tiles[k]=tile;
row.add(tile);
k++;
}
rowTiles.add(row);
}
Collections.sort(gridLineOffsets);
gridLineOffsets.add(Math.round(maxW));
result.columnSizes=new int[gridLineOffsets.size()];
result.columnSizes[0]=gridLineOffsets.get(0);
for(int i=gridLineOffsets.size()-1; i>0; i--){
result.columnSizes[i]=gridLineOffsets.get(i)-gridLineOffsets.get(i-1);
}
for(ArrayList<TiledLayoutResult.Tile> row : rowTiles){
int columnOffset=0;
for(TiledLayoutResult.Tile tile : row){
int startColumn=columnOffset;
tile.startCol=startColumn;
int width=0;
tile.colSpan=0;
for(int i=startColumn; i<result.columnSizes.length; i++){
width+=result.columnSizes[i];
tile.colSpan++;
if(width==tile.width){
break;
}
}
columnOffset+=tile.colSpan;
}
}
result.height=Math.round(totalHeight+marginH*(optHeights.length-1));
}
return result;
}
private static float sum(List<Float> a){
float sum=0;
for(float f:a) sum+=f;
return sum;
}
private static float calculateMultiThumbsHeight(List<Float> ratios, float width, float margin){
return (width-(ratios.size()-1)*margin)/sum(ratios);
}
public static class TiledLayoutResult{
public int[] columnSizes, rowSizes; // sizes in grid fractions
public Tile[] tiles;
public int width, height; // in pixels (510x510 max)
@Override
public String toString(){
return "TiledLayoutResult{"+
"columnSizes="+Arrays.toString(columnSizes)+
", rowSizes="+Arrays.toString(rowSizes)+
", tiles="+Arrays.toString(tiles)+
", width="+width+
", height="+height+
'}';
}
public static class Tile{
public int colSpan, rowSpan, width, height, startCol, startRow;
public Tile(int colSpan, int rowSpan, int width, int height, int startCol, int startRow){
this.colSpan=colSpan;
this.rowSpan=rowSpan;
this.width=width;
this.height=height;
this.startCol=startCol;
this.startRow=startRow;
}
public Tile(int colSpan, int rowSpan, float width, float height, int startCol, int startRow){
this(colSpan, rowSpan, Math.round(width), Math.round(height), startCol, startRow);
}
@Override
public String toString(){
return "Tile{"+
"colSpan="+colSpan+
", rowSpan="+rowSpan+
", width="+width+
", height="+height+
'}';
}
}
}
}

View File

@ -0,0 +1,27 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import android.util.AttributeSet;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class TileGridLayoutManager extends GridLayoutManager{
private static final String TAG="TileGridLayoutManager";
public TileGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
super(context, attrs, defStyleAttr, defStyleRes);
}
public TileGridLayoutManager(Context context, int spanCount){
super(context, spanCount);
}
public TileGridLayoutManager(Context context, int spanCount, int orientation, boolean reverseLayout){
super(context, spanCount, orientation, reverseLayout);
}
@Override
public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state){
return 1;
}
}

View File

@ -10,12 +10,13 @@ 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.PhotoLayoutHelper;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public class GifVStatusDisplayItem extends ImageStatusDisplayItem{
public GifVStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos){
super(parentID, parentFragment, attachment, status, index, totalPhotos);
public GifVStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
super(parentID, parentFragment, attachment, status, index, totalPhotos, tiledLayout, thisTile);
request=new UrlImageLoaderRequest(attachment.previewUrl, 1000, 1000);
}

View File

@ -1,7 +1,6 @@
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;
@ -11,13 +10,14 @@ 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.PhotoLayoutHelper;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout;
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;
@ -25,13 +25,17 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
protected Attachment attachment;
protected ImageLoaderRequest request;
public final Status status;
public final PhotoLayoutHelper.TiledLayoutResult tiledLayout;
public final PhotoLayoutHelper.TiledLayoutResult.Tile thisTile;
public ImageStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Attachment photo, Status status, int index, int totalPhotos){
public ImageStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Attachment photo, Status status, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
super(parentID, parentFragment);
this.attachment=photo;
this.status=status;
this.index=index;
this.totalPhotos=totalPhotos;
this.tiledLayout=tiledLayout;
this.thisTile=thisTile;
}
@Override
@ -46,6 +50,7 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
public static abstract class Holder<T extends ImageStatusDisplayItem> extends StatusDisplayItem.Holder<T> implements ImageLoaderViewHolder{
public final ImageView photo;
private ImageAttachmentFrameLayout layout;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
@ -53,10 +58,12 @@ public abstract class ImageStatusDisplayItem extends StatusDisplayItem{
super(activity, layout, parent);
photo=findViewById(R.id.photo);
photo.setOnClickListener(this::onViewClick);
this.layout=(ImageAttachmentFrameLayout)itemView;
}
@Override
public void onBind(ImageStatusDisplayItem item){
layout.setLayout(item.tiledLayout, item.thisTile);
crossfadeDrawable.setSize(item.attachment.getWidth(), item.attachment.getHeight());
crossfadeDrawable.setBlurhashDrawable(item.attachment.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(item.status.spoilerRevealed ? 0f : 1f);

View File

@ -7,12 +7,13 @@ 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.PhotoLayoutHelper;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos){
super(parentID, parentFragment, photo, status, index, totalPhotos);
public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
super(parentID, parentFragment, photo, status, index, totalPhotos, tiledLayout, thisTile);
request=new UrlImageLoaderRequest(photo.url, 1000, 1000);
}

View File

@ -13,12 +13,14 @@ import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Poll;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.text.HtmlParser;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
@ -71,22 +73,20 @@ public abstract class StatusDisplayItem{
items.add(new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent));
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, accountID), fragment, statusForContent));
int photoIndex=0;
int totalPhotos=0;
for(Attachment attachment:statusForContent.mediaAttachments){
if(attachment.type==Attachment.Type.IMAGE || attachment.type==Attachment.Type.GIFV || attachment.type==Attachment.Type.VIDEO){
totalPhotos++;
}
}
for(Attachment attachment:statusForContent.mediaAttachments){
if(attachment.type==Attachment.Type.IMAGE){
items.add(new PhotoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, totalPhotos));
photoIndex++;
}else if(attachment.type==Attachment.Type.GIFV){
items.add(new GifVStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, totalPhotos));
photoIndex++;
}else if(attachment.type==Attachment.Type.VIDEO){
items.add(new VideoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, totalPhotos));
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
if(!imageAttachments.isEmpty()){
int photoIndex=0;
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(1000, 1910, imageAttachments);
for(Attachment attachment:imageAttachments){
if(attachment.type==Attachment.Type.IMAGE){
items.add(new PhotoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex]));
}else if(attachment.type==Attachment.Type.GIFV){
items.add(new GifVStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex]));
}else if(attachment.type==Attachment.Type.VIDEO){
items.add(new VideoStatusDisplayItem(parentID, statusForContent, attachment, fragment, photoIndex, imageAttachments.size(), layout, layout.tiles[photoIndex]));
}else{
throw new IllegalStateException("This isn't supposed to happen, type is "+attachment.type);
}
photoIndex++;
}
}

View File

@ -10,12 +10,13 @@ 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.PhotoLayoutHelper;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
public class VideoStatusDisplayItem extends ImageStatusDisplayItem{
public VideoStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos){
super(parentID, parentFragment, attachment, status, index, totalPhotos);
public VideoStatusDisplayItem(String parentID, Status status, Attachment attachment, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
super(parentID, parentFragment, attachment, status, index, totalPhotos, tiledLayout, thisTile);
request=new UrlImageLoaderRequest(attachment.previewUrl, 1000, 1000);
}

View File

@ -0,0 +1,52 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.V;
public class ImageAttachmentFrameLayout extends FrameLayout{
public static final int MAX_WIDTH=400; // dp
private PhotoLayoutHelper.TiledLayoutResult tileLayout;
private PhotoLayoutHelper.TiledLayoutResult.Tile tile;
public ImageAttachmentFrameLayout(@NonNull Context context){
super(context);
}
public ImageAttachmentFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs){
super(context, attrs);
}
public ImageAttachmentFrameLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
if(isInEditMode()){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
int w=Math.min(((View)getParent()).getMeasuredWidth(), V.dp(MAX_WIDTH));
int actualHeight=Math.round(tile.height/1000f*w)+V.dp(1)*(tile.rowSpan-1);
int actualWidth=Math.round(tile.width/1000f*w);
if(tile.startCol+tile.colSpan<tileLayout.columnSizes.length)
actualWidth-=V.dp(1);
heightMeasureSpec=actualHeight | MeasureSpec.EXACTLY;
widthMeasureSpec=actualWidth | MeasureSpec.EXACTLY;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void setLayout(PhotoLayoutHelper.TiledLayoutResult layout, PhotoLayoutHelper.TiledLayoutResult.Tile tile){
tileLayout=layout;
this.tile=tile;
}
}

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"/>
@ -25,4 +25,4 @@
android:layout_margin="8dp"
android:background="@drawable/ic_gif"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout>

View File

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout>

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"/>
@ -18,4 +18,4 @@
android:elevation="3dp"
android:background="@drawable/play_button"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout>