chore: readd all the new bottom sheet code

This commit is contained in:
LucasGGamerM
2024-03-04 16:29:57 -03:00
parent 0d7e10ca8a
commit 71dd974d19
21 changed files with 1075 additions and 36 deletions

View File

@@ -13,7 +13,7 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.internal.StringUtil;

View File

@@ -5,8 +5,6 @@ import android.app.Fragment;
import android.app.NotificationManager;
import android.app.assist.AssistContent;
import android.graphics.drawable.RippleDrawable;
import android.content.Intent;
import android.graphics.Outline;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
@@ -37,7 +35,7 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;

View File

@@ -24,7 +24,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
@@ -18,7 +17,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;

View File

@@ -0,0 +1,90 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.InsetDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public abstract class AccountRestrictionConfirmationSheet extends BottomSheet{
private LinearLayout contentWrap;
protected Button cancelBtn;
protected ProgressBarButton confirmBtn, secondaryBtn;
protected TextView titleView, subtitleView;
protected ImageView icon;
protected boolean loading;
public AccountRestrictionConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_restrict_account, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
contentWrap=findViewById(R.id.content_wrap);
titleView=findViewById(R.id.title);
subtitleView=findViewById(R.id.text);
cancelBtn=findViewById(R.id.btn_cancel);
confirmBtn=findViewById(R.id.btn_confirm);
secondaryBtn=findViewById(R.id.btn_secondary);
icon=findViewById(R.id.icon);
contentWrap.setDividerDrawable(new EmptyDrawable(1, V.dp(8)));
contentWrap.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
confirmBtn.setOnClickListener(v->{
if(loading)
return;
loading=true;
confirmBtn.setProgressBarVisible(true);
confirmCallback.onConfirmed(this::dismiss, ()->{
confirmBtn.setProgressBarVisible(false);
loading=false;
});
});
cancelBtn.setOnClickListener(v->{
if(!loading)
dismiss();
});
}
protected void addRow(@DrawableRes int icon, CharSequence text){
TextView tv=new TextView(getContext());
tv.setTextAppearance(R.style.m3_body_large);
tv.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
tv.setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary)));
tv.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
tv.setText(text);
InsetDrawable drawable=new InsetDrawable(getContext().getResources().getDrawable(icon, getContext().getTheme()), V.dp(8));
drawable.setBounds(0, 0, V.dp(40), V.dp(40));
tv.setCompoundDrawablesRelative(drawable, null, null, null);
tv.setCompoundDrawablePadding(V.dp(16));
contentWrap.addView(tv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
protected void addRow(@DrawableRes int icon, @StringRes int text){
addRow(icon, getContext().getString(text));
}
public interface ConfirmCallback{
void onConfirmed(Runnable onSuccess, Runnable onError);
}
}

View File

@@ -1,4 +1,4 @@
package org.joinmastodon.android.ui;
package org.joinmastodon.android.ui.sheets;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -8,7 +8,6 @@ import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.SpannableStringBuilder;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -27,8 +26,10 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.fragments.onboarding.CustomWelcomeFragment;
import org.joinmastodon.android.ui.ClickableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
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;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;

View File

@@ -0,0 +1,24 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import androidx.annotation.NonNull;
public class BlockAccountConfirmationSheet extends AccountRestrictionConfirmationSheet{
public BlockAccountConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
super(context, user, confirmCallback);
titleView.setText(R.string.block_user_confirm_title);
confirmBtn.setText(R.string.do_block);
secondaryBtn.setVisibility(View.GONE);
icon.setImageResource(R.drawable.ic_fluent_shield_24_regular);
subtitleView.setText(user.getDisplayUsername());
addRow(R.drawable.ic_campaign_24px, R.string.user_can_see_blocked);
addRow(R.drawable.ic_fluent_eye_off_24_regular, R.string.user_cant_see_each_other_posts);
addRow(R.drawable.ic_fluent_mention_24_regular, R.string.you_wont_see_user_mentions);
addRow(R.drawable.ic_fluent_arrow_reply_24_regular, R.string.user_cant_mention_or_follow_you);
}
}

View File

@@ -0,0 +1,101 @@
package org.joinmastodon.android.ui.sheets;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.text.LinkSpan;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.RippleAnimationTextView;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.NodeVisitor;
import androidx.annotation.NonNull;
import me.grishka.appkit.views.BottomSheet;
public class DecentralizationExplainerSheet extends BottomSheet{
private final String handleStr;
public DecentralizationExplainerSheet(@NonNull Context context, String accountID, Account account){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_decentralization_info, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
TextView handleTitle=findViewById(R.id.handle_title);
RippleAnimationTextView handle=findViewById(R.id.handle);
TextView usernameExplanation=findViewById(R.id.username_text);
TextView serverExplanation=findViewById(R.id.server_text);
TextView handleExplanation=findViewById(R.id.handle_explanation);
findViewById(R.id.btn_cancel).setOnClickListener(v->dismiss());
String domain=account.getDomain();
if(TextUtils.isEmpty(domain))
domain=AccountSessionManager.get(accountID).domain;
handleStr="@"+account.username+"@"+domain;
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
handleTitle.setText(isSelf ? R.string.handle_title_own : R.string.handle_title);
handle.setText(handleStr);
usernameExplanation.setText(isSelf ? R.string.handle_username_explanation_own : R.string.handle_username_explanation);
serverExplanation.setText(isSelf ? R.string.handle_server_explanation_own : R.string.handle_server_explanation);
String explanation=context.getString(isSelf ? R.string.handle_explanation_own : R.string.handle_explanation);
SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(explanation).body().traverse(new NodeVisitor(){
private int spanStart;
@Override
public void head(Node node, int depth){
if(node instanceof TextNode tn){
ssb.append(tn.text());
}else if(node instanceof Element){
spanStart=ssb.length();
}
}
@Override
public void tail(Node node, int depth){
if(node instanceof Element){
ssb.setSpan(new LinkSpan("", DecentralizationExplainerSheet.this::showActivityPubAlert, LinkSpan.Type.CUSTOM, null, null, null), spanStart, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
handleExplanation.setText(ssb);
findViewById(R.id.handle_wrap).setOnClickListener(v->{
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, handleStr));
if(UiUtils.needShowClipboardToast()){
new Snackbar.Builder(context)
.setText(R.string.handle_copied)
.show();
}
});
String _domain=domain;
findViewById(R.id.username_row).setOnClickListener(v->handle.animate(1, account.username.length()+1));
findViewById(R.id.server_row).setOnClickListener(v->handle.animate(handleStr.length()-_domain.length(), handleStr.length()));
}
private void showActivityPubAlert(LinkSpan s){
new M3AlertDialogBuilder(getContext())
.setTitle(R.string.what_is_activitypub_title)
.setMessage(R.string.what_is_activitypub)
.setPositiveButton(R.string.ok, null)
.show();
}
}

View File

@@ -0,0 +1,24 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import androidx.annotation.NonNull;
public class MuteAccountConfirmationSheet extends AccountRestrictionConfirmationSheet{
public MuteAccountConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
super(context, user, confirmCallback);
titleView.setText(R.string.mute_user_confirm_title);
confirmBtn.setText(R.string.do_mute);
secondaryBtn.setVisibility(View.GONE);
icon.setImageResource(R.drawable.ic_fluent_speaker_mute_24_regular);
subtitleView.setText(user.getDisplayUsername());
addRow(R.drawable.ic_campaign_24px, R.string.user_wont_know_muted);
addRow(R.drawable.ic_fluent_eye_off_24_regular, R.string.user_can_still_see_your_posts);
addRow(R.drawable.ic_fluent_mention_24_regular, R.string.you_wont_see_user_mentions);
addRow(R.drawable.ic_fluent_arrow_reply_24_regular, R.string.user_can_mention_and_follow_you);
}
}

View File

@@ -0,0 +1,131 @@
package org.joinmastodon.android.ui.sheets;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class NonMutualPreReplySheet extends PreReplySheet{
private boolean fullBioShown=false;
@SuppressLint("DefaultLocale")
public NonMutualPreReplySheet(@NonNull Context context, ResultListener resultListener, Account account, String accountID){
super(context, resultListener);
icon.setImageResource(R.drawable.ic_waving_hand_24px);
title.setText(R.string.non_mutual_sheet_title);
text.setText(R.string.non_mutual_sheet_text);
LinearLayout userInfo=new LinearLayout(context);
userInfo.setOrientation(LinearLayout.HORIZONTAL);
userInfo.setBackgroundResource(R.drawable.bg_user_info);
UiUtils.setAllPaddings(userInfo, 12);
ImageView ava=new ImageView(context);
ava.setScaleType(ImageView.ScaleType.CENTER_CROP);
ava.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
ava.setOutlineProvider(OutlineProviders.roundedRect(12));
ava.setClipToOutline(true);
ava.setForeground(context.getResources().getDrawable(R.drawable.fg_user_info_ava, context.getTheme()));
userInfo.addView(ava, UiUtils.makeLayoutParams(56, 56, 0, 0, 12, 0));
ViewImageLoader.loadWithoutAnimation(ava, context.getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(account.avatarStatic, V.dp(56), V.dp(56)));
LinearLayout nameAndFields=new LinearLayout(context);
nameAndFields.setOrientation(LinearLayout.VERTICAL);
nameAndFields.setMinimumHeight(V.dp(56));
nameAndFields.setGravity(Gravity.CENTER_VERTICAL);
userInfo.addView(nameAndFields, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
TextView name=new TextView(context);
name.setSingleLine();
name.setEllipsize(TextUtils.TruncateAt.END);
name.setTextAppearance(R.style.m3_title_medium);
name.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface));
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames){
name.setText(HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
UiUtils.loadCustomEmojiInTextView(name);
}else{
name.setText(account.displayName);
}
name.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
nameAndFields.addView(name, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(24)));
if(!TextUtils.isEmpty(account.note)){
CharSequence strippedBio=HtmlParser.parseCustomEmoji(HtmlParser.stripAndRemoveInvisibleSpans(account.note), account.emojis);
TextView bioShort=new TextView(context);
bioShort.setTextAppearance(R.style.m3_body_medium);
bioShort.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3Secondary));
bioShort.setMaxLines(2);
bioShort.setEllipsize(TextUtils.TruncateAt.END);
bioShort.setText(strippedBio);
nameAndFields.addView(bioShort, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
TextView bioFull=new TextView(context);
bioFull.setTextAppearance(R.style.m3_body_medium);
bioFull.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3Secondary));
bioFull.setText(strippedBio);
bioFull.setVisibility(View.GONE);
nameAndFields.addView(bioFull, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
nameAndFields.setOnClickListener(v->{
UiUtils.beginLayoutTransition((ViewGroup) getWindow().getDecorView());
fullBioShown=!fullBioShown;
if(fullBioShown){
bioFull.setVisibility(View.VISIBLE);
bioShort.setVisibility(View.GONE);
}else{
bioFull.setVisibility(View.GONE);
bioShort.setVisibility(View.VISIBLE);
}
});
UiUtils.loadCustomEmojiInTextView(bioShort);
UiUtils.loadCustomEmojiInTextView(bioFull);
}else{
TextView username=new TextView(context);
username.setTextAppearance(R.style.m3_body_medium);
username.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3Secondary));
username.setSingleLine();
username.setEllipsize(TextUtils.TruncateAt.END);
username.setText(account.getDisplayUsername());
username.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
nameAndFields.addView(username, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(20)));
}
contentWrap.addView(userInfo, UiUtils.makeLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0, 0, 8));
for(int i=0;i<3;i++){
View item=context.getSystemService(LayoutInflater.class).inflate(R.layout.item_other_numbered_rule, contentWrap, false);
TextView number=item.findViewById(R.id.number);
number.setText(String.format("%d", i+1));
TextView title=item.findViewById(R.id.title);
TextView text=item.findViewById(R.id.text);
title.setText(switch(i){
case 0 -> R.string.non_mutual_title1;
case 1 -> R.string.non_mutual_title2;
case 2 -> R.string.non_mutual_title3;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
text.setText(switch(i){
case 0 -> R.string.non_mutual_text1;
case 1 -> R.string.non_mutual_text2;
case 2 -> R.string.non_mutual_text3;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
contentWrap.addView(item);
}
}
}

View File

@@ -0,0 +1,23 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Status;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import androidx.annotation.NonNull;
public class OldPostPreReplySheet extends PreReplySheet{
public OldPostPreReplySheet(@NonNull Context context, ResultListener resultListener, Status status){
super(context, resultListener);
int months=(int)status.createdAt.atZone(ZoneId.systemDefault()).until(ZonedDateTime.now(), ChronoUnit.MONTHS);
String monthsStr=months>24 ? context.getString(R.string.more_than_two_years) : context.getResources().getQuantityString(R.plurals.x_months, months, months);
title.setText(context.getString(R.string.old_post_sheet_title, monthsStr));
text.setText(R.string.old_post_sheet_text);
icon.setImageResource(R.drawable.ic_fluent_history_24_regular);
}
}

View File

@@ -0,0 +1,54 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.views.BottomSheet;
public abstract class PreReplySheet extends BottomSheet{
protected ImageView icon;
protected TextView title, text;
protected Button gotItButton, dontRemindButton;
protected LinearLayout contentWrap;
public PreReplySheet(@NonNull Context context, ResultListener resultListener){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_pre_reply, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
icon=findViewById(R.id.icon);
title=findViewById(R.id.title);
text=findViewById(R.id.text);
gotItButton=findViewById(R.id.btn_got_it);
dontRemindButton=findViewById(R.id.btn_dont_remind_again);
contentWrap=findViewById(R.id.content_wrap);
gotItButton.setOnClickListener(v->{
dismiss();
resultListener.onButtonClicked(false);
});
dontRemindButton.setOnClickListener(v->{
dismiss();
resultListener.onButtonClicked(true);
});
}
@FunctionalInterface
public interface ResultListener{
void onButtonClicked(boolean notAgain);
}
}

View File

@@ -1847,6 +1847,10 @@ public class UiUtils {
}
}
public static boolean needShowClipboardToast(){
return Build.VERSION.SDK_INT<=Build.VERSION_CODES.S_V2;
}
public static void setAllPaddings(View view, int paddingDp){
int pad=V.dp(paddingDp);
view.setPadding(pad, pad, pad, pad);

View File

@@ -1,23 +1,42 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ProgressBar;
import org.joinmastodon.android.R;
public class ProgressBarButton extends Button{
private boolean textVisible=true;
private ProgressBar progressBar;
private int progressBarID;
public ProgressBarButton(Context context){
super(context);
this(context, null);
}
public ProgressBarButton(Context context, AttributeSet attrs){
super(context, attrs);
this(context, attrs, 0);
}
public ProgressBarButton(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
public ProgressBarButton(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.ProgressBarButton);
progressBarID=ta.getResourceId(R.styleable.ProgressBarButton_progressBar, 0);
ta.recycle();
}
@Override
protected void onAttachedToWindow(){
super.onAttachedToWindow();
if(progressBarID!=0){
progressBar=((ViewGroup)getParent()).findViewById(progressBarID);
}
}
public void setTextVisible(boolean textVisible){
@@ -29,6 +48,19 @@ public class ProgressBarButton extends Button{
return textVisible;
}
public void setProgressBarVisible(boolean visible){
if(progressBar==null)
throw new IllegalStateException("progressBar is not set");
if(visible){
setTextVisible(false);
progressBar.setIndeterminateTintList(getTextColors());
progressBar.setVisibility(View.VISIBLE);
}else{
setTextVisible(true);
progressBar.setVisibility(View.GONE);
}
}
@Override
protected void onDraw(Canvas canvas){
if(textVisible){

View File

@@ -0,0 +1,170 @@
package org.joinmastodon.android.ui.views;
import android.animation.ArgbEvaluator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.util.AttributeSet;
import android.widget.TextView;
import androidx.dynamicanimation.animation.FloatValueHolder;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import me.grishka.appkit.utils.CustomViewHelper;
public class RippleAnimationTextView extends TextView implements CustomViewHelper{
private final Paint animationPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private CharacterAnimationState[] charStates;
private final ArgbEvaluator colorEvaluator=new ArgbEvaluator();
private int runningAnimCount=0;
private Runnable[] delayedAnimations1, delayedAnimations2;
public RippleAnimationTextView(Context context){
this(context, null);
}
public RippleAnimationTextView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public RippleAnimationTextView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter){
super.onTextChanged(text, start, lengthBefore, lengthAfter);
if(charStates!=null){
for(CharacterAnimationState state:charStates){
state.colorAnimation.cancel();
state.shadowAnimation.cancel();
state.scaleAnimation.cancel();
}
for(Runnable r:delayedAnimations1){
if(r!=null)
removeCallbacks(r);
}
for(Runnable r:delayedAnimations2){
if(r!=null)
removeCallbacks(r);
}
}
charStates=new CharacterAnimationState[lengthAfter];
delayedAnimations1=new Runnable[lengthAfter];
delayedAnimations2=new Runnable[lengthAfter];
}
@Override
protected void onDraw(Canvas canvas){
if(runningAnimCount==0 && !areThereDelayedAnimations()){
super.onDraw(canvas);
return;
}
Layout layout=getLayout();
animationPaint.set(getPaint());
CharSequence text=layout.getText();
for(int i=0;i<layout.getLineCount();i++){
int baseline=layout.getLineBaseline(i);
for(int offset=layout.getLineStart(i); offset<layout.getLineEnd(i); offset++){
float x=layout.getPrimaryHorizontal(offset);
CharacterAnimationState state=charStates[offset];
if(state==null || state.scaleAnimation==null){
animationPaint.setColor(getCurrentTextColor());
animationPaint.clearShadowLayer();
canvas.drawText(text, offset, offset+1, x, baseline, animationPaint);
}else{
animationPaint.setColor((int)colorEvaluator.evaluate(Math.max(0, Math.min(1, state.color.getValue())), getCurrentTextColor(), getLinkTextColors().getDefaultColor()));
float scale=state.scale.getValue();
int shadowAlpha=Math.round(255*Math.max(0, Math.min(1, state.shadowAlpha.getValue())));
animationPaint.setShadowLayer(dp(4), 0, dp(3), (getPaint().linkColor & 0xFFFFFF) | (shadowAlpha << 24));
canvas.save();
canvas.scale(scale, scale, x, baseline);
canvas.drawText(text, offset, offset+1, x, baseline, animationPaint);
canvas.restore();
}
}
}
invalidate();
}
public void animate(int startIndex, int endIndex){
for(int i=startIndex;i<endIndex;i++){
CharacterAnimationState _state=charStates[i];
if(_state==null){
_state=charStates[i]=new CharacterAnimationState();
}
CharacterAnimationState state=_state;
int finalI=i;
postOnAnimationDelayed(()->{
if(!state.colorAnimation.isRunning())
runningAnimCount++;
state.colorAnimation.animateToFinalPosition(1f);
if(!state.shadowAnimation.isRunning())
runningAnimCount++;
state.shadowAnimation.animateToFinalPosition(0.3f);
if(!state.scaleAnimation.isRunning())
runningAnimCount++;
state.scaleAnimation.animateToFinalPosition(1.2f);
invalidate();
if(delayedAnimations1[finalI]!=null)
removeCallbacks(delayedAnimations1[finalI]);
if(delayedAnimations2[finalI]!=null)
removeCallbacks(delayedAnimations2[finalI]);
Runnable delay1=()->{
if(!state.colorAnimation.isRunning())
runningAnimCount++;
state.colorAnimation.animateToFinalPosition(0f);
if(!state.shadowAnimation.isRunning())
runningAnimCount++;
state.shadowAnimation.animateToFinalPosition(0f);
invalidate();
delayedAnimations1[finalI]=null;
};
Runnable delay2=()->{
if(!state.scaleAnimation.isRunning())
runningAnimCount++;
state.scaleAnimation.animateToFinalPosition(1f);
delayedAnimations2[finalI]=null;
};
delayedAnimations1[finalI]=delay1;
delayedAnimations2[finalI]=delay2;
postOnAnimationDelayed(delay1, 2000);
postOnAnimationDelayed(delay2, 100);
}, 20L*(i-startIndex));
}
}
private boolean areThereDelayedAnimations(){
for(Runnable r:delayedAnimations1){
if(r!=null)
return true;
}
for(Runnable r:delayedAnimations2){
if(r!=null)
return true;
}
return false;
}
private class CharacterAnimationState extends FloatValueHolder{
private final SpringAnimation scaleAnimation, colorAnimation, shadowAnimation;
private final FloatValueHolder scale=new FloatValueHolder(1), color=new FloatValueHolder(), shadowAlpha=new FloatValueHolder();
public CharacterAnimationState(){
scaleAnimation=new SpringAnimation(scale);
colorAnimation=new SpringAnimation(color);
shadowAnimation=new SpringAnimation(shadowAlpha);
setupSpring(scaleAnimation);
setupSpring(colorAnimation);
setupSpring(shadowAnimation);
}
private void setupSpring(SpringAnimation anim){
anim.setMinimumVisibleChange(0.01f);
anim.setSpring(new SpringForce().setStiffness(500f).setDampingRatio(0.175f));
anim.addEndListener((animation, canceled, value, velocity)->runningAnimCount--);
}
}
}