Posts redesign wip

This commit is contained in:
Grishka
2023-03-14 19:17:37 +03:00
parent d6bcc9c156
commit 20799ef1a8
52 changed files with 926 additions and 365 deletions

View File

@@ -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
}
}

View File

@@ -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){

View File

@@ -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;

View File

@@ -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);

View File

@@ -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+'\''+
'}';
}
}
}

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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){

View File

@@ -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);
}

View File

@@ -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){

View File

@@ -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);
}
}
}

View File

@@ -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{

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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){

View File

@@ -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);
}
}