Media layout
This commit is contained in:
parent
bb463aa10a
commit
6cc6fe195b
|
@ -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(){
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -147,6 +147,8 @@ public class Account extends BaseModel{
|
|||
}
|
||||
if(moved!=null)
|
||||
moved.postprocess();
|
||||
if(TextUtils.isEmpty(displayName))
|
||||
displayName=username;
|
||||
}
|
||||
|
||||
public boolean isLocal(){
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue