Posts redesign wip
This commit is contained in:
@@ -31,7 +31,6 @@ import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -57,6 +56,7 @@ public class AudioPlayerService extends Service{
|
||||
private static HashSet<Callback> callbacks=new HashSet<>();
|
||||
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener=this::onAudioFocusChanged;
|
||||
private boolean resumeAfterAudioFocusGain;
|
||||
private boolean isBuffering=true;
|
||||
|
||||
private BroadcastReceiver receiver=new BroadcastReceiver(){
|
||||
@Override
|
||||
@@ -176,6 +176,7 @@ public class AudioPlayerService extends Service{
|
||||
player.setOnErrorListener(this::onPlayerError);
|
||||
player.setOnCompletionListener(this::onPlayerCompletion);
|
||||
player.setOnSeekCompleteListener(this::onPlayerSeekCompleted);
|
||||
player.setOnInfoListener(this::onPlayerInfo);
|
||||
try{
|
||||
player.setDataSource(this, Uri.parse(attachment.url));
|
||||
player.prepareAsync();
|
||||
@@ -187,7 +188,9 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
private void onPlayerPrepared(MediaPlayer mp){
|
||||
Log.i(TAG, "onPlayerPrepared");
|
||||
playerReady=true;
|
||||
isBuffering=false;
|
||||
player.start();
|
||||
updateSessionState(false);
|
||||
}
|
||||
@@ -205,6 +208,21 @@ public class AudioPlayerService extends Service{
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
private boolean onPlayerInfo(MediaPlayer mp, int what, int extra){
|
||||
switch(what){
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_START -> {
|
||||
isBuffering=true;
|
||||
updateSessionState(false);
|
||||
}
|
||||
case MediaPlayer.MEDIA_INFO_BUFFERING_END -> {
|
||||
isBuffering=false;
|
||||
updateSessionState(false);
|
||||
}
|
||||
default -> Log.i(TAG, "onPlayerInfo() called with: mp = ["+mp+"], what = ["+what+"], extra = ["+extra+"]");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onAudioFocusChanged(int change){
|
||||
switch(change){
|
||||
case AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
@@ -232,12 +250,16 @@ public class AudioPlayerService extends Service{
|
||||
|
||||
private void updateSessionState(boolean removeNotification){
|
||||
session.setPlaybackState(new PlaybackState.Builder()
|
||||
.setState(player.isPlaying() ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED, player.getCurrentPosition(), 1f)
|
||||
.setState(switch(getPlayState()){
|
||||
case PLAYING -> PlaybackState.STATE_PLAYING;
|
||||
case PAUSED -> PlaybackState.STATE_PAUSED;
|
||||
case BUFFERING -> PlaybackState.STATE_BUFFERING;
|
||||
}, player.getCurrentPosition(), 1f)
|
||||
.setActions(PlaybackState.ACTION_STOP | PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_SEEK_TO)
|
||||
.build());
|
||||
updateNotification(!player.isPlaying(), removeNotification);
|
||||
for(Callback cb:callbacks)
|
||||
cb.onPlayStateChanged(attachment.id, player.isPlaying(), player.getCurrentPosition());
|
||||
cb.onPlayStateChanged(attachment.id, getPlayState(), player.getCurrentPosition());
|
||||
}
|
||||
|
||||
private void updateNotification(boolean dismissable, boolean removeNotification){
|
||||
@@ -310,6 +332,12 @@ public class AudioPlayerService extends Service{
|
||||
return attachment.id;
|
||||
}
|
||||
|
||||
public PlayState getPlayState(){
|
||||
if(isBuffering)
|
||||
return PlayState.BUFFERING;
|
||||
return player.isPlaying() ? PlayState.PLAYING : PlayState.PAUSED;
|
||||
}
|
||||
|
||||
public static void registerCallback(Callback cb){
|
||||
callbacks.add(cb);
|
||||
}
|
||||
@@ -333,7 +361,13 @@ public class AudioPlayerService extends Service{
|
||||
}
|
||||
|
||||
public interface Callback{
|
||||
void onPlayStateChanged(String attachmentID, boolean playing, int position);
|
||||
void onPlayStateChanged(String attachmentID, PlayState state, int position);
|
||||
void onPlaybackStopped(String attachmentID);
|
||||
}
|
||||
|
||||
public enum PlayState{
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
BUFFERING
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
@@ -48,6 +49,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -405,25 +407,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
public void onRevealSpoilerClick(TextStatusDisplayItem.Holder holder){
|
||||
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
revealSpoiler(status, holder.getItemID());
|
||||
toggleSpoiler(status, holder.getItemID());
|
||||
}
|
||||
|
||||
public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
revealSpoiler(status, holder.getItemID());
|
||||
}
|
||||
protected void toggleSpoiler(Status status, String itemID){
|
||||
status.spoilerRevealed=!status.spoilerRevealed;
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null)
|
||||
spoiler.rebind();
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
|
||||
|
||||
protected void revealSpoiler(Status status, String itemID){
|
||||
status.spoilerRevealed=true;
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null)
|
||||
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
|
||||
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
|
||||
if(header!=null)
|
||||
header.rebind();
|
||||
updateImagesSpoilerState(status, itemID);
|
||||
int index=displayItems.indexOf(spoilerItem);
|
||||
if(status.spoilerRevealed){
|
||||
displayItems.addAll(index+1, spoilerItem.contentItems);
|
||||
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
|
||||
}else{
|
||||
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
|
||||
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
|
||||
}
|
||||
}
|
||||
|
||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
|
||||
|
||||
@@ -327,8 +327,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
spoilerEdit=view.findViewById(R.id.content_warning);
|
||||
LayerDrawable spoilerBg=(LayerDrawable) spoilerEdit.getBackground().mutate();
|
||||
spoilerBg.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable());
|
||||
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable());
|
||||
spoilerBg.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(false));
|
||||
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(false));
|
||||
spoilerEdit.setBackground(spoilerBg);
|
||||
if((savedInstanceState!=null && savedInstanceState.getBoolean("hasSpoiler", false)) || hasSpoiler){
|
||||
hasSpoiler=true;
|
||||
|
||||
@@ -336,6 +336,8 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
||||
}
|
||||
|
||||
private void setActionProgressVisible(boolean visible){
|
||||
if(visible)
|
||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||
actionButton.setTextVisible(!visible);
|
||||
actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
actionButton.setClickable(!visible);
|
||||
|
||||
@@ -127,6 +127,7 @@ public class Attachment extends BaseModel{
|
||||
public PointF focus;
|
||||
public SizeMetadata original;
|
||||
public SizeMetadata small;
|
||||
public ColorsMetadata colors;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
@@ -138,6 +139,7 @@ public class Attachment extends BaseModel{
|
||||
", focus="+focus+
|
||||
", original="+original+
|
||||
", small="+small+
|
||||
", colors="+colors+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -161,4 +163,20 @@ public class Attachment extends BaseModel{
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@Parcel
|
||||
public static class ColorsMetadata{
|
||||
public String background;
|
||||
public String foreground;
|
||||
public String accent;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "ColorsMetadata{"+
|
||||
"background='"+background+'\''+
|
||||
", foreground='"+foreground+'\''+
|
||||
", accent='"+accent+'\''+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ public class OutlineProviders{
|
||||
outline.setAlpha(view.getAlpha());
|
||||
}
|
||||
};
|
||||
public static final ViewOutlineProvider OVAL=new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setOval(0, 0, view.getWidth(), view.getHeight());
|
||||
}
|
||||
};
|
||||
|
||||
public static ViewOutlineProvider roundedRect(int dp){
|
||||
ViewOutlineProvider provider=roundedRects.get(dp);
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -13,16 +23,27 @@ 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.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.AudioAttachmentBackgroundDrawable;
|
||||
import org.joinmastodon.android.ui.drawables.SeekBarThumbDrawable;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.palette.graphics.Palette;
|
||||
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.V;
|
||||
|
||||
public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final Attachment attachment;
|
||||
private final ImageLoaderRequest imageRequest;
|
||||
|
||||
public AudioStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, Attachment attachment){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.attachment=attachment;
|
||||
imageRequest=new UrlImageLoaderRequest(TextUtils.isEmpty(attachment.previewUrl) ? status.account.avatarStatic : attachment.previewUrl, V.dp(100), V.dp(100));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,25 +51,38 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
return Type.AUDIO;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<AudioStatusDisplayItem> implements AudioPlayerService.Callback{
|
||||
private final ImageButton playPauseBtn;
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return imageRequest;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<AudioStatusDisplayItem> implements AudioPlayerService.Callback, ImageLoaderViewHolder{
|
||||
private final ImageButton playPauseBtn, forwardBtn, rewindBtn;
|
||||
private final TextView time;
|
||||
private final SeekBar seekBar;
|
||||
private final ImageView image;
|
||||
private final FrameLayout content;
|
||||
private final AudioAttachmentBackgroundDrawable bgDrawable;
|
||||
|
||||
private int lastKnownPosition;
|
||||
private long lastKnownPositionTime;
|
||||
private boolean playing;
|
||||
private int lastRemainingSeconds=-1;
|
||||
private boolean seekbarBeingDragged;
|
||||
private int lastPosSeconds=-1;
|
||||
private AudioPlayerService.PlayState state;
|
||||
|
||||
private Runnable positionUpdater=this::updatePosition;
|
||||
private final Runnable positionUpdater=this::updatePosition;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_audio, parent);
|
||||
playPauseBtn=findViewById(R.id.play_pause_btn);
|
||||
time=findViewById(R.id.time);
|
||||
seekBar=findViewById(R.id.seekbar);
|
||||
seekBar.setThumb(new SeekBarThumbDrawable(context));
|
||||
image=findViewById(R.id.image);
|
||||
content=findViewById(R.id.content);
|
||||
forwardBtn=findViewById(R.id.forward_btn);
|
||||
rewindBtn=findViewById(R.id.rewind_btn);
|
||||
playPauseBtn.setOnClickListener(this::onPlayPauseClick);
|
||||
itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener(){
|
||||
@Override
|
||||
@@ -61,76 +95,71 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
AudioPlayerService.unregisterCallback(Holder.this);
|
||||
}
|
||||
});
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener(){
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser){
|
||||
if(fromUser){
|
||||
int seconds=(int)(seekBar.getProgress()/10000.0*item.attachment.getDuration());
|
||||
time.setText(formatDuration(seconds));
|
||||
}
|
||||
}
|
||||
forwardBtn.setOnClickListener(this::onSeekButtonClick);
|
||||
rewindBtn.setOnClickListener(this::onSeekButtonClick);
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar){
|
||||
seekbarBeingDragged=true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar){
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
service.seekTo((int)(seekBar.getProgress()/10000.0*item.attachment.getDuration()*1000.0));
|
||||
}
|
||||
seekbarBeingDragged=false;
|
||||
if(playing)
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
}
|
||||
});
|
||||
image.setOutlineProvider(OutlineProviders.OVAL);
|
||||
image.setClipToOutline(true);
|
||||
content.setBackground(bgDrawable=new AudioAttachmentBackgroundDrawable());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AudioStatusDisplayItem item){
|
||||
int seconds=(int)item.attachment.getDuration();
|
||||
String duration=formatDuration(seconds);
|
||||
// Some fonts (not Roboto) have different-width digits. 0 is supposedly the widest.
|
||||
time.getLayoutParams().width=(int)Math.ceil(Math.max(time.getPaint().measureText("-"+duration),
|
||||
time.getPaint().measureText("-"+duration.replaceAll("\\d", "0"))));
|
||||
time.setText(duration);
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
seekBar.setEnabled(true);
|
||||
onPlayStateChanged(item.attachment.id, service.isPlaying(), service.getPosition());
|
||||
forwardBtn.setVisibility(View.VISIBLE);
|
||||
rewindBtn.setVisibility(View.VISIBLE);
|
||||
onPlayStateChanged(item.attachment.id, service.getPlayState(), service.getPosition());
|
||||
actuallyUpdatePosition();
|
||||
}else{
|
||||
seekBar.setEnabled(false);
|
||||
state=null;
|
||||
time.setText(duration);
|
||||
forwardBtn.setVisibility(View.INVISIBLE);
|
||||
rewindBtn.setVisibility(View.INVISIBLE);
|
||||
setPlayButtonPlaying(false, false);
|
||||
}
|
||||
|
||||
int mainColor;
|
||||
if(item.attachment.meta!=null && item.attachment.meta.colors!=null){
|
||||
try{
|
||||
mainColor=Color.parseColor(item.attachment.meta.colors.background);
|
||||
}catch(IllegalArgumentException x){
|
||||
mainColor=0xff808080;
|
||||
}
|
||||
}else{
|
||||
mainColor=0xff808080;
|
||||
}
|
||||
updateColors(mainColor);
|
||||
}
|
||||
|
||||
private void onPlayPauseClick(View v){
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
if(playing)
|
||||
if(state!=AudioPlayerService.PlayState.PAUSED)
|
||||
service.pause(true);
|
||||
else
|
||||
service.play();
|
||||
}else{
|
||||
AudioPlayerService.start(v.getContext(), item.status, item.attachment);
|
||||
onPlayStateChanged(item.attachment.id, true, 0);
|
||||
seekBar.setEnabled(true);
|
||||
onPlayStateChanged(item.attachment.id, AudioPlayerService.PlayState.BUFFERING, 0);
|
||||
forwardBtn.setVisibility(View.VISIBLE);
|
||||
rewindBtn.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayStateChanged(String attachmentID, boolean playing, int position){
|
||||
public void onPlayStateChanged(String attachmentID, AudioPlayerService.PlayState state, int position){
|
||||
if(attachmentID.equals(item.attachment.id)){
|
||||
this.lastKnownPosition=position;
|
||||
lastKnownPositionTime=SystemClock.uptimeMillis();
|
||||
this.playing=playing;
|
||||
playPauseBtn.setImageResource(playing ? R.drawable.ic_fluent_pause_circle_24_filled : R.drawable.ic_fluent_play_circle_24_filled);
|
||||
if(!playing){
|
||||
lastRemainingSeconds=-1;
|
||||
time.setText(formatDuration((int) item.attachment.getDuration()));
|
||||
}else{
|
||||
this.state=state;
|
||||
setPlayButtonPlaying(state!=AudioPlayerService.PlayState.PAUSED, true);
|
||||
if(state==AudioPlayerService.PlayState.PLAYING){
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
}else if(state==AudioPlayerService.PlayState.BUFFERING){
|
||||
actuallyUpdatePosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,14 +167,15 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
@Override
|
||||
public void onPlaybackStopped(String attachmentID){
|
||||
if(attachmentID.equals(item.attachment.id)){
|
||||
playing=false;
|
||||
playPauseBtn.setImageResource(R.drawable.ic_fluent_play_circle_24_filled);
|
||||
seekBar.setProgress(0);
|
||||
seekBar.setEnabled(false);
|
||||
state=null;
|
||||
setPlayButtonPlaying(false, true);
|
||||
forwardBtn.setVisibility(View.INVISIBLE);
|
||||
rewindBtn.setVisibility(View.INVISIBLE);
|
||||
time.setText(formatDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private String formatDuration(int seconds){
|
||||
if(seconds>=3600)
|
||||
return String.format("%d:%02d:%02d", seconds/3600, seconds%3600/60, seconds%60);
|
||||
@@ -154,16 +184,79 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void updatePosition(){
|
||||
if(!playing || seekbarBeingDragged)
|
||||
if(state!=AudioPlayerService.PlayState.PLAYING)
|
||||
return;
|
||||
double pos=lastKnownPosition/1000.0+(SystemClock.uptimeMillis()-lastKnownPositionTime)/1000.0;
|
||||
seekBar.setProgress((int)Math.round(pos/item.attachment.getDuration()*10000.0));
|
||||
actuallyUpdatePosition();
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
int remainingSeconds=(int)(item.attachment.getDuration()-pos);
|
||||
if(remainingSeconds!=lastRemainingSeconds){
|
||||
lastRemainingSeconds=remainingSeconds;
|
||||
time.setText("-"+formatDuration(remainingSeconds));
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private void actuallyUpdatePosition(){
|
||||
double pos=lastKnownPosition/1000.0;
|
||||
if(state==AudioPlayerService.PlayState.PLAYING)
|
||||
pos+=(SystemClock.uptimeMillis()-lastKnownPositionTime)/1000.0;
|
||||
int posSeconds=(int)pos;
|
||||
if(posSeconds!=lastPosSeconds){
|
||||
lastPosSeconds=posSeconds;
|
||||
time.setText(formatDuration(posSeconds)+"/"+formatDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateColors(int mainColor){
|
||||
float[] hsv={0, 0, 0};
|
||||
float[] hsv2={0, 0, 0};
|
||||
Color.colorToHSV(mainColor, hsv);
|
||||
boolean isGray=hsv[1]<0.2f;
|
||||
boolean isDarkTheme=UiUtils.isDarkTheme();
|
||||
hsv2[0]=hsv[0];
|
||||
hsv2[1]=isGray ? hsv[1] : (isDarkTheme ? 0.6f : 0.4f);
|
||||
hsv2[2]=isDarkTheme ? 0.3f : 0.75f;
|
||||
int bgColor=Color.HSVToColor(hsv2);
|
||||
hsv2[1]=isGray ? hsv[1] : (isDarkTheme ? 0.3f : 0.6f);
|
||||
hsv2[2]=isDarkTheme ? 0.6f : 0.4f;
|
||||
bgDrawable.setColors(bgColor, Color.HSVToColor(128, hsv2));
|
||||
|
||||
hsv2[1]=isGray ? hsv[1] : 0.1f;
|
||||
hsv2[2]=1;
|
||||
int controlsColor=Color.HSVToColor(hsv2);
|
||||
time.setTextColor(controlsColor);
|
||||
forwardBtn.setColorFilter(controlsColor);
|
||||
rewindBtn.setColorFilter(controlsColor);
|
||||
}
|
||||
|
||||
private void setPlayButtonPlaying(boolean playing, boolean animated){
|
||||
playPauseBtn.setImageResource(playing ? R.drawable.ic_pause_48px : R.drawable.ic_play_arrow_48px);
|
||||
playPauseBtn.setContentDescription(item.parentFragment.getString(playing ? R.string.pause : R.string.play));
|
||||
if(playing)
|
||||
bgDrawable.startAnimation();
|
||||
else
|
||||
bgDrawable.stopAnimation(animated);
|
||||
}
|
||||
|
||||
private void onSeekButtonClick(View v){
|
||||
int seekAmount=v.getId()==R.id.forward_btn ? 10_000 : -5_000;
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
int newPos=Math.min(Math.max(0, service.getPosition()+seekAmount), (int)(item.attachment.getDuration()*1000));
|
||||
service.seekTo(newPos);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
this.image.setImageDrawable(image);
|
||||
if((item.attachment.meta==null || item.attachment.meta.colors==null) && image instanceof BitmapDrawable bd){
|
||||
Bitmap bitmap=bd.getBitmap();
|
||||
if(Build.VERSION.SDK_INT>=26 && bitmap.getConfig()==Bitmap.Config.HARDWARE)
|
||||
bitmap=bitmap.copy(Bitmap.Config.ARGB_8888, false);
|
||||
int color=Palette.from(bitmap).maximumColorCount(1).generate().getDominantColor(0xff808080);
|
||||
updateColors(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
@@ -45,6 +47,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
|
||||
private final TextView reply, boost, favorite;
|
||||
private final ImageView share;
|
||||
private final ColorStateList buttonColors;
|
||||
|
||||
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
@@ -61,6 +64,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
boost=findViewById(R.id.boost);
|
||||
favorite=findViewById(R.id.favorite);
|
||||
share=findViewById(R.id.share);
|
||||
|
||||
float[] hsb={0, 0, 0};
|
||||
Color.colorToHSV(UiUtils.getThemeColor(activity, R.attr.colorM3Primary), hsb);
|
||||
hsb[1]+=0.1f;
|
||||
hsb[2]+=0.16f;
|
||||
|
||||
buttonColors=new ColorStateList(new int[][]{
|
||||
{android.R.attr.state_selected},
|
||||
{android.R.attr.state_enabled},
|
||||
{}
|
||||
}, new int[]{
|
||||
Color.HSVToColor(hsb),
|
||||
UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant),
|
||||
UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant) & 0x80FFFFFF
|
||||
});
|
||||
|
||||
boost.setTextColor(buttonColors);
|
||||
boost.setCompoundDrawableTintList(buttonColors);
|
||||
favorite.setTextColor(buttonColors);
|
||||
favorite.setCompoundDrawableTintList(buttonColors);
|
||||
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
|
||||
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
|
||||
@@ -94,7 +118,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
private void bindButton(TextView btn, long count){
|
||||
if(count>0 && !item.hideCounts){
|
||||
btn.setText(UiUtils.abbreviateNumber(count));
|
||||
btn.setCompoundDrawablePadding(V.dp(8));
|
||||
btn.setCompoundDrawablePadding(V.dp(6));
|
||||
}else{
|
||||
btn.setText("");
|
||||
btn.setCompoundDrawablePadding(0);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.graphics.Outline;
|
||||
@@ -32,6 +33,7 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -105,33 +107,23 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView name, username, timestamp, extraText;
|
||||
private final ImageView avatar, more, visibility;
|
||||
private final TextView name, timeAndUsername, extraText;
|
||||
private final ImageView avatar, more;
|
||||
private final PopupMenu optionsMenu;
|
||||
private Relationship relationship;
|
||||
private APIRequest<?> currentRelationshipRequest;
|
||||
|
||||
private static final ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(12));
|
||||
}
|
||||
};
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_header, parent);
|
||||
name=findViewById(R.id.name);
|
||||
username=findViewById(R.id.username);
|
||||
timestamp=findViewById(R.id.timestamp);
|
||||
timeAndUsername=findViewById(R.id.time_and_username);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
more=findViewById(R.id.more);
|
||||
visibility=findViewById(R.id.visibility);
|
||||
extraText=findViewById(R.id.extra_text);
|
||||
avatar.setOnClickListener(this::onAvaClick);
|
||||
avatar.setOutlineProvider(roundCornersOutline);
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
more.setOnClickListener(this::onMoreClick);
|
||||
visibility.setOnClickListener(v->item.parentFragment.onVisibilityIconClick(this));
|
||||
|
||||
optionsMenu=new PopupMenu(activity, more);
|
||||
optionsMenu.inflate(R.menu.post);
|
||||
@@ -200,22 +192,17 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Override
|
||||
public void onBind(HeaderStatusDisplayItem item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText('@'+item.user.acct);
|
||||
String time;
|
||||
if(item.status==null || item.status.editedAt==null)
|
||||
timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt));
|
||||
time=UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt);
|
||||
else
|
||||
timestamp.setText(item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt)));
|
||||
visibility.setVisibility(item.hasVisibilityToggle && !item.inset ? View.VISIBLE : View.GONE);
|
||||
if(item.hasVisibilityToggle){
|
||||
visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility);
|
||||
visibility.setContentDescription(item.parentFragment.getString(item.status.spoilerRevealed ? R.string.hide_content : R.string.reveal_content));
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
||||
visibility.setTooltipText(visibility.getContentDescription());
|
||||
}
|
||||
}
|
||||
time=item.parentFragment.getString(R.string.edited_timestamp, UiUtils.formatRelativeTimestamp(itemView.getContext(), item.status.editedAt));
|
||||
|
||||
timeAndUsername.setText(time+" · @"+item.user.acct);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0);
|
||||
if(TextUtils.isEmpty(item.extraText)){
|
||||
extraText.setVisibility(View.GONE);
|
||||
|
||||
@@ -31,6 +31,7 @@ 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.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private static final String TAG="MediaGridDisplayItem";
|
||||
@@ -97,6 +98,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
wrapper=(FrameLayout)itemView;
|
||||
layout=new MediaGridLayout(activity);
|
||||
wrapper.addView(layout);
|
||||
wrapper.setPadding(0, 0, 0, V.dp(8));
|
||||
|
||||
activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, wrapper);
|
||||
altTextWrapper=findViewById(R.id.alt_text_wrapper);
|
||||
@@ -105,6 +107,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
altTextClose=findViewById(R.id.alt_text_close);
|
||||
altText=findViewById(R.id.alt_text);
|
||||
altTextClose.setOnClickListener(this::onAltTextCloseClick);
|
||||
wrapper.setClipToPadding(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -158,11 +161,7 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
private void onViewClick(View v){
|
||||
int index=(Integer)v.getTag();
|
||||
if(!item.status.spoilerRevealed){
|
||||
item.parentFragment.onRevealSpoilerClick(this);
|
||||
}else if(item.parentFragment instanceof PhotoViewerHost){
|
||||
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, index, this);
|
||||
}
|
||||
((PhotoViewerHost) item.parentFragment).openPhotoViewer(item.parentID, item.status, index, this);
|
||||
}
|
||||
|
||||
private void onAltTextClick(View v){
|
||||
|
||||
@@ -37,9 +37,11 @@ public class PollFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void onBind(PollFooterStatusDisplayItem item){
|
||||
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_voters, item.poll.votersCount, item.poll.votersCount);
|
||||
String text=item.parentFragment.getResources().getQuantityString(R.plurals.x_votes, item.poll.votesCount, item.poll.votesCount);
|
||||
if(item.poll.expiresAt!=null && !item.poll.isExpired()){
|
||||
text+=" · "+UiUtils.formatTimeLeft(itemView.getContext(), item.poll.expiresAt);
|
||||
if(item.poll.multiple)
|
||||
text+=" · "+item.parentFragment.getString(R.string.poll_multiple_choice);
|
||||
}else if(item.poll.isExpired()){
|
||||
text+=" · "+item.parentFragment.getString(R.string.poll_closed);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
@@ -25,11 +26,13 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
private boolean showResults;
|
||||
private float votesFraction; // 0..1
|
||||
private boolean isMostVoted;
|
||||
private final int optionIndex;
|
||||
public final Poll poll;
|
||||
|
||||
public PollOptionStatusDisplayItem(String parentID, Poll poll, Poll.Option option, BaseStatusListFragment parentFragment){
|
||||
public PollOptionStatusDisplayItem(String parentID, Poll poll, int optionIndex, BaseStatusListFragment parentFragment){
|
||||
super(parentID, parentFragment);
|
||||
this.option=option;
|
||||
this.optionIndex=optionIndex;
|
||||
option=poll.options.get(optionIndex);
|
||||
this.poll=poll;
|
||||
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
|
||||
emojiHelper.setText(text);
|
||||
@@ -61,23 +64,24 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<PollOptionStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView text, percent;
|
||||
private final View icon, button;
|
||||
private final View check, button;
|
||||
private final Drawable progressBg;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_poll_option, parent);
|
||||
text=findViewById(R.id.text);
|
||||
percent=findViewById(R.id.percent);
|
||||
icon=findViewById(R.id.icon);
|
||||
check=findViewById(R.id.checkbox);
|
||||
button=findViewById(R.id.button);
|
||||
progressBg=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted, activity.getTheme()).mutate();
|
||||
itemView.setOnClickListener(this::onButtonClick);
|
||||
button.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||
button.setClipToOutline(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(PollOptionStatusDisplayItem item){
|
||||
text.setText(item.text);
|
||||
icon.setVisibility(item.showResults ? View.GONE : View.VISIBLE);
|
||||
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
|
||||
itemView.setClickable(!item.showResults);
|
||||
if(item.showResults){
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final ArrayList<StatusDisplayItem> contentItems=new ArrayList<>();
|
||||
private final CharSequence parsedTitle;
|
||||
private final CustomEmojiHelper emojiHelper;
|
||||
|
||||
public SpoilerStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
parsedTitle=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedTitle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return emojiHelper.getImageRequest(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.SPOILER;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<SpoilerStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView title, action;
|
||||
private final View button;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_spoiler, parent);
|
||||
title=findViewById(R.id.spoiler_title);
|
||||
action=findViewById(R.id.spoiler_action);
|
||||
button=findViewById(R.id.spoiler_button);
|
||||
|
||||
button.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
button.setClipToOutline(true);
|
||||
LayerDrawable spoilerBg=(LayerDrawable) button.getBackground().mutate();
|
||||
spoilerBg.setDrawableByLayerId(R.id.left_drawable, new SpoilerStripesDrawable(true));
|
||||
spoilerBg.setDrawableByLayerId(R.id.right_drawable, new SpoilerStripesDrawable(false));
|
||||
button.setBackground(spoilerBg);
|
||||
button.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(SpoilerStatusDisplayItem item){
|
||||
title.setText(item.parsedTitle);
|
||||
action.setText(item.status.spoilerRevealed ? R.string.spoiler_hide : R.string.spoiler_show);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
item.emojiHelper.setImageDrawable(index, image);
|
||||
title.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
setImage(index, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ public abstract class StatusDisplayItem{
|
||||
case GAP -> new GapStatusDisplayItem.Holder(activity, parent);
|
||||
case EXTENDED_FOOTER -> new ExtendedFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
|
||||
case SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,32 +73,43 @@ public abstract class StatusDisplayItem{
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
Status statusForContent=status.getContentStatus();
|
||||
if(status.reblog!=null){
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_fluent_arrow_repeat_all_20_filled));
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px));
|
||||
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){
|
||||
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled));
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_reply_20px));
|
||||
}
|
||||
HeaderStatusDisplayItem header;
|
||||
items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null));
|
||||
|
||||
ArrayList<StatusDisplayItem> contentItems;
|
||||
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, statusForContent);
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
}else{
|
||||
contentItems=items;
|
||||
}
|
||||
|
||||
if(!TextUtils.isEmpty(statusForContent.content))
|
||||
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent));
|
||||
contentItems.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent));
|
||||
else
|
||||
header.needBottomPadding=true;
|
||||
|
||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
||||
if(!imageAttachments.isEmpty()){
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||
items.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
|
||||
contentItems.add(new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent));
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
if(att.type==Attachment.Type.AUDIO){
|
||||
items.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
contentItems.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
}
|
||||
}
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, items);
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, contentItems);
|
||||
}
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
items.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||
}
|
||||
if(addFooter){
|
||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||
@@ -109,12 +121,20 @@ public abstract class StatusDisplayItem{
|
||||
item.inset=inset;
|
||||
item.index=i++;
|
||||
}
|
||||
if(items!=contentItems){
|
||||
for(StatusDisplayItem item:contentItems){
|
||||
item.inset=inset;
|
||||
item.index=i++;
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){
|
||||
int i=0;
|
||||
for(Poll.Option opt:poll.options){
|
||||
items.add(new PollOptionStatusDisplayItem(parentID, poll, opt, fragment));
|
||||
items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment));
|
||||
i++;
|
||||
}
|
||||
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll));
|
||||
}
|
||||
@@ -133,7 +153,8 @@ public abstract class StatusDisplayItem{
|
||||
HASHTAG,
|
||||
GAP,
|
||||
EXTENDED_FOOTER,
|
||||
MEDIA_GRID
|
||||
MEDIA_GRID,
|
||||
SPOILER
|
||||
}
|
||||
|
||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
|
||||
@@ -3,15 +3,11 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
|
||||
@@ -21,8 +17,7 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
private CharSequence text;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(), spoilerEmojiHelper;
|
||||
private CharSequence parsedSpoilerText;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
public boolean textSelectable;
|
||||
public final Status status;
|
||||
|
||||
@@ -31,11 +26,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
this.text=text;
|
||||
this.status=status;
|
||||
emojiHelper.setText(text);
|
||||
if(!TextUtils.isEmpty(status.spoilerText)){
|
||||
parsedSpoilerText=HtmlParser.parseCustomEmoji(status.spoilerText, status.emojis);
|
||||
spoilerEmojiHelper=new CustomEmojiHelper();
|
||||
spoilerEmojiHelper.setText(parsedSpoilerText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,29 +35,20 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
|
||||
return spoilerEmojiHelper.getImageCount();
|
||||
return emojiHelper.getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
if(spoilerEmojiHelper!=null && !status.spoilerRevealed)
|
||||
return spoilerEmojiHelper.getImageRequest(index);
|
||||
return emojiHelper.getImageRequest(index);
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final LinkedTextView text;
|
||||
private final TextView spoilerTitle;
|
||||
private final View spoilerOverlay;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_text, parent);
|
||||
text=findViewById(R.id.text);
|
||||
spoilerTitle=findViewById(R.id.spoiler_title);
|
||||
spoilerOverlay=findViewById(R.id.spoiler_overlay);
|
||||
itemView.setOnClickListener(v->item.parentFragment.onRevealSpoilerClick(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,29 +56,13 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
text.setText(item.text);
|
||||
text.setTextIsSelectable(item.textSelectable);
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
if(!TextUtils.isEmpty(item.status.spoilerText)){
|
||||
spoilerTitle.setText(item.parsedSpoilerText);
|
||||
if(item.status.spoilerRevealed){
|
||||
spoilerOverlay.setVisibility(View.GONE);
|
||||
text.setVisibility(View.VISIBLE);
|
||||
itemView.setClickable(false);
|
||||
}else{
|
||||
spoilerOverlay.setVisibility(View.VISIBLE);
|
||||
text.setVisibility(View.INVISIBLE);
|
||||
itemView.setClickable(true);
|
||||
}
|
||||
}else{
|
||||
spoilerOverlay.setVisibility(View.GONE);
|
||||
text.setVisibility(View.VISIBLE);
|
||||
itemView.setClickable(false);
|
||||
}
|
||||
itemView.setClickable(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
getEmojiHelper().setImageDrawable(index, image);
|
||||
text.invalidate();
|
||||
spoilerTitle.invalidate();
|
||||
if(image instanceof Animatable){
|
||||
((Animatable) image).start();
|
||||
if(image instanceof MovieDrawable)
|
||||
@@ -112,7 +77,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private CustomEmojiHelper getEmojiHelper(){
|
||||
return item.spoilerEmojiHelper!=null && !item.status.spoilerRevealed ? item.spoilerEmojiHelper : item.emojiHelper;
|
||||
return item.emojiHelper;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AudioAttachmentBackgroundDrawable extends Drawable{
|
||||
private int bgColor, wavesColor;
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private long[] animationStartTimes={0, 0};
|
||||
private boolean animationRunning;
|
||||
private Runnable[] restartRunnables={()->restartAnimation(0), ()->restartAnimation(1)};
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
Rect bounds=getBounds();
|
||||
paint.setColor(bgColor);
|
||||
canvas.drawRect(bounds, paint);
|
||||
|
||||
float initialRadius=V.dp(48);
|
||||
float finalRadius=bounds.width()/2f;
|
||||
long time=SystemClock.uptimeMillis();
|
||||
boolean animationsStillRunning=false;
|
||||
|
||||
for(int i=0;i<animationStartTimes.length;i++){
|
||||
long t=time-animationStartTimes[i];
|
||||
if(t<0)
|
||||
continue;
|
||||
float fraction=t/3000f;
|
||||
if(fraction>1)
|
||||
continue;
|
||||
fraction=CubicBezierInterpolator.EASE_OUT.getInterpolation(fraction);
|
||||
paint.setColor(wavesColor);
|
||||
paint.setAlpha(Math.round(paint.getAlpha()*(1f-fraction)));
|
||||
canvas.drawCircle(bounds.centerX(), bounds.centerY(), initialRadius+(finalRadius-initialRadius)*fraction, paint);
|
||||
animationsStillRunning=true;
|
||||
}
|
||||
|
||||
if(animationsStillRunning){
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
|
||||
public void setColors(int bg, int waves){
|
||||
bgColor=bg;
|
||||
wavesColor=waves;
|
||||
}
|
||||
|
||||
public void startAnimation(){
|
||||
if(animationRunning)
|
||||
return;
|
||||
|
||||
long time=SystemClock.uptimeMillis();
|
||||
animationStartTimes[0]=time;
|
||||
scheduleSelf(restartRunnables[0], time+3000);
|
||||
scheduleSelf(restartRunnables[1], time+1500);
|
||||
animationRunning=true;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public void stopAnimation(boolean gracefully){
|
||||
if(!animationRunning)
|
||||
return;
|
||||
|
||||
animationRunning=false;
|
||||
for(Runnable r:restartRunnables)
|
||||
unscheduleSelf(r);
|
||||
if(!gracefully){
|
||||
animationStartTimes[0]=animationStartTimes[1]=0;
|
||||
}
|
||||
}
|
||||
|
||||
private void restartAnimation(int index){
|
||||
long time=SystemClock.uptimeMillis();
|
||||
animationStartTimes[index]=time;
|
||||
if(animationRunning)
|
||||
scheduleSelf(restartRunnables[index], time+3000);
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,16 @@ import androidx.annotation.Nullable;
|
||||
|
||||
public class SpoilerStripesDrawable extends Drawable{
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private boolean flipped;
|
||||
|
||||
public SpoilerStripesDrawable(){
|
||||
private static final float X1=-0.860365f;
|
||||
private static final float X2=10.6078f;
|
||||
|
||||
public SpoilerStripesDrawable(boolean flipped){
|
||||
paint.setColor(0xff000000);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(3);
|
||||
this.flipped=flipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -34,7 +39,7 @@ public class SpoilerStripesDrawable extends Drawable{
|
||||
float y1=6.80133f;
|
||||
float y2=-1.22874f;
|
||||
while(y2<height){
|
||||
canvas.drawLine(-0.860365f, y1, 10.6078f, y2, paint);
|
||||
canvas.drawLine(flipped ? X2 : X1, y1, flipped ? X1 : X2, y2, paint);
|
||||
y1+=8.03007f;
|
||||
y2+=8.03007f;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class LinkSpan extends CharacterStyle {
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setColor(color=tp.linkColor);
|
||||
tp.setUnderlineText(true);
|
||||
}
|
||||
|
||||
public void onClick(Context context){
|
||||
|
||||
@@ -24,6 +24,7 @@ public class FrameLayoutThatOnlyMeasuresFirstChild extends FrameLayout{
|
||||
return;
|
||||
View child0=getChildAt(0);
|
||||
measureChild(child0, widthMeasureSpec, heightMeasureSpec);
|
||||
super.onMeasure(child0.getMeasuredWidth() | MeasureSpec.EXACTLY, child0.getMeasuredHeight() | MeasureSpec.EXACTLY);
|
||||
int vpad=getPaddingTop()+getPaddingBottom();
|
||||
super.onMeasure(child0.getMeasuredWidth() | MeasureSpec.EXACTLY, (child0.getMeasuredHeight()+vpad) | MeasureSpec.EXACTLY);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user