diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 8ddbe867..cdb7525d 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -69,6 +69,7 @@ dependencies { implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03' implementation 'me.grishka.litex:viewpager:1.0.0' implementation 'me.grishka.litex:viewpager2:1.0.0' + implementation 'me.grishka.litex:palette:1.0.0' implementation 'me.grishka.appkit:appkit:1.2.7' implementation 'com.google.code.gson:gson:2.8.9' implementation 'org.jsoup:jsoup:1.14.3' diff --git a/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java b/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java index ab0c29d1..3f45bb56 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java +++ b/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java @@ -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 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 + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 8587ebc5..0bfdc7d3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -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 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){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java index 371e5b02..ead6d2f1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java @@ -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; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java index c4198fed..37872778 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/OnboardingFollowSuggestionsFragment.java @@ -336,6 +336,8 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment 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 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); + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java index dbabdd6b..38c3403a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java @@ -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{ 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_INT0 && !item.hideCounts){ btn.setText(UiUtils.abbreviateNumber(count)); - btn.setCompoundDrawablePadding(V.dp(8)); + btn.setCompoundDrawablePadding(V.dp(6)); }else{ btn.setText(""); btn.setCompoundDrawablePadding(0); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index 522c736b..367aacb8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -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 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); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java index 5f0daeb3..df24bb17 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java @@ -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){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollFooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollFooterStatusDisplayItem.java index deffc9b4..d4345f50 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollFooterStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollFooterStatusDisplayItem.java @@ -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); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java index 6333f32f..b10beaaa 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java @@ -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 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){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java new file mode 100644 index 00000000..c49e2bc1 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java @@ -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 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 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); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 783c2f3f..b1b64fbe 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -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 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 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 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 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 extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java index 557bede7..79414bce 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java @@ -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 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; } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/AudioAttachmentBackgroundDrawable.java b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/AudioAttachmentBackgroundDrawable.java new file mode 100644 index 00000000..3500b240 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/AudioAttachmentBackgroundDrawable.java @@ -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;i1) + 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); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/SpoilerStripesDrawable.java b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/SpoilerStripesDrawable.java index 7c07f946..f97dfd2d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/SpoilerStripesDrawable.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/SpoilerStripesDrawable.java @@ -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 + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_poll_option_clickable.xml b/mastodon/src/main/res/drawable/bg_poll_option_clickable.xml index 2ca7b19a..cc3187a9 100644 --- a/mastodon/src/main/res/drawable/bg_poll_option_clickable.xml +++ b/mastodon/src/main/res/drawable/bg_poll_option_clickable.xml @@ -2,8 +2,14 @@ - - + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_poll_option_voted.xml b/mastodon/src/main/res/drawable/bg_poll_option_voted.xml index 6a1330fa..7bf3663c 100644 --- a/mastodon/src/main/res/drawable/bg_poll_option_voted.xml +++ b/mastodon/src/main/res/drawable/bg_poll_option_voted.xml @@ -1,17 +1,17 @@ - - - - + + + + + + - - - - - - + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_round_ripple.xml b/mastodon/src/main/res/drawable/bg_round_ripple.xml new file mode 100644 index 00000000..efbfcb5c --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_round_ripple.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_spoiler.xml b/mastodon/src/main/res/drawable/bg_spoiler.xml new file mode 100644 index 00000000..96855989 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_spoiler.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_check_20px.xml b/mastodon/src/main/res/drawable/ic_check_20px.xml new file mode 100644 index 00000000..59e22ef8 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_check_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_forward_10_48px.xml b/mastodon/src/main/res/drawable/ic_forward_10_48px.xml new file mode 100644 index 00000000..98ea05bb --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_forward_10_48px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_more_vert_20px.xml b/mastodon/src/main/res/drawable/ic_more_vert_20px.xml new file mode 100644 index 00000000..b2acc7fe --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_more_vert_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_pause_48px.xml b/mastodon/src/main/res/drawable/ic_pause_48px.xml new file mode 100644 index 00000000..a6b14ed8 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_pause_48px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_play_arrow_48px.xml b/mastodon/src/main/res/drawable/ic_play_arrow_48px.xml new file mode 100644 index 00000000..4bd70b88 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_play_arrow_48px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_poll_check.xml b/mastodon/src/main/res/drawable/ic_poll_check.xml new file mode 100644 index 00000000..d74e61ba --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_poll_check.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_repeat_20px.xml b/mastodon/src/main/res/drawable/ic_repeat_20px.xml new file mode 100644 index 00000000..377d13d0 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_repeat_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_repeat_selector.xml b/mastodon/src/main/res/drawable/ic_repeat_selector.xml new file mode 100644 index 00000000..f0da541e --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_repeat_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_repeat_wght700grad200fill1_20px.xml b/mastodon/src/main/res/drawable/ic_repeat_wght700grad200fill1_20px.xml new file mode 100644 index 00000000..1b57f8d6 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_repeat_wght700grad200fill1_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_replay_5_48px.xml b/mastodon/src/main/res/drawable/ic_replay_5_48px.xml new file mode 100644 index 00000000..b98f9264 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_replay_5_48px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_reply_20px.xml b/mastodon/src/main/res/drawable/ic_reply_20px.xml new file mode 100644 index 00000000..22b557ba --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_reply_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_share_20px.xml b/mastodon/src/main/res/drawable/ic_share_20px.xml new file mode 100644 index 00000000..037117e6 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_share_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_star_20px.xml b/mastodon/src/main/res/drawable/ic_star_20px.xml new file mode 100644 index 00000000..885f71dd --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_star_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_star_selector.xml b/mastodon/src/main/res/drawable/ic_star_selector.xml new file mode 100644 index 00000000..001d9b62 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_star_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_star_wght700grad200fill1_20px.xml b/mastodon/src/main/res/drawable/ic_star_wght700grad200fill1_20px.xml new file mode 100644 index 00000000..a2ad3c34 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_star_wght700grad200fill1_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/layout/display_item_audio.xml b/mastodon/src/main/res/layout/display_item_audio.xml index 43053a69..cab2a4a0 100644 --- a/mastodon/src/main/res/layout/display_item_audio.xml +++ b/mastodon/src/main/res/layout/display_item_audio.xml @@ -3,52 +3,60 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="-8dp" - android:layout_marginBottom="-8dp" - android:paddingLeft="16dp" - android:paddingRight="16dp" - android:paddingTop="8dp" android:paddingBottom="8dp" android:clipToPadding="false"> - - - - - - + android:layout_height="188dp"> + + - + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_footer.xml b/mastodon/src/main/res/layout/display_item_footer.xml index 594abc0e..7e2f9749 100644 --- a/mastodon/src/main/res/layout/display_item_footer.xml +++ b/mastodon/src/main/res/layout/display_item_footer.xml @@ -3,25 +3,30 @@ xmlns:tools="http://schemas.android.com/tools" android:orientation="horizontal" android:layout_width="match_parent" - android:layout_height="48dp" - android:paddingLeft="20dp" - android:paddingRight="20dp"> + android:layout_height="42dp" + android:paddingBottom="8dp" + android:paddingLeft="8dp" + android:paddingRight="8dp"> + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:background="?android:actionBarItemBackground" + android:minWidth="34dp"> @@ -34,18 +39,21 @@ android:id="@+id/boost_btn" android:layout_width="wrap_content" android:layout_height="match_parent" - android:minWidth="56dp"> + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:background="?android:actionBarItemBackground" + android:minWidth="34dp"> @@ -58,18 +66,21 @@ android:id="@+id/favorite_btn" android:layout_width="wrap_content" android:layout_height="match_parent" - android:minWidth="56dp"> + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:background="?android:actionBarItemBackground" + android:minWidth="34dp"> @@ -82,14 +93,17 @@ android:id="@+id/share_btn" android:layout_width="wrap_content" android:layout_height="match_parent" - android:minWidth="56dp"> + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:background="?android:actionBarItemBackground" + android:minWidth="34dp"> diff --git a/mastodon/src/main/res/layout/display_item_header.xml b/mastodon/src/main/res/layout/display_item_header.xml index fdad5a0c..be70fede 100644 --- a/mastodon/src/main/res/layout/display_item_header.xml +++ b/mastodon/src/main/res/layout/display_item_header.xml @@ -13,30 +13,22 @@ android:layout_height="24dp" android:layout_alignParentTop="true" android:layout_alignParentEnd="true" + android:layout_marginTop="-2dp" + android:layout_marginEnd="-2dp" android:background="?android:selectableItemBackgroundBorderless" android:scaleType="center" - android:tint="?android:textColorSecondary" + android:tint="?colorM3OnSurfaceVariant" android:contentDescription="@string/more_options" - android:src="@drawable/ic_post_more" /> - - + android:src="@drawable/ic_more_vert_20px" /> + android:layout_marginTop="2dp" + android:layout_marginEnd="8dp" /> - - - - - - - - - + android:singleLine="true" + android:ellipsize="end" + android:textAppearance="@style/m3_title_small" + android:gravity="center_vertical" + android:textColor="?colorM3OnSurfaceVariant" + tools:text="9h ago · \@Gargron@mastodon.social"/> \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_photo.xml b/mastodon/src/main/res/layout/display_item_photo.xml index 2ba0d286..becb5e1f 100644 --- a/mastodon/src/main/res/layout/display_item_photo.xml +++ b/mastodon/src/main/res/layout/display_item_photo.xml @@ -17,8 +17,8 @@ android:layout_width="40dp" android:layout_height="22dp" android:layout_gravity="start|bottom" - android:layout_marginLeft="16dp" - android:layout_marginRight="16dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:importantForAccessibility="no" diff --git a/mastodon/src/main/res/layout/display_item_poll_footer.xml b/mastodon/src/main/res/layout/display_item_poll_footer.xml index cc3e4a3f..9a5a7d57 100644 --- a/mastodon/src/main/res/layout/display_item_poll_footer.xml +++ b/mastodon/src/main/res/layout/display_item_poll_footer.xml @@ -1,6 +1,7 @@ @@ -8,20 +9,24 @@ + android:layout_marginBottom="8dp" + android:textAppearance="@style/m3_body_medium" + android:gravity="center_vertical" + android:textColor="?colorM3OnSurfaceVariant" + tools:text="fdsafdsafsdafds"/>