Domain badges & info sheet & my fanciest animation yet
This commit is contained in:
@@ -62,10 +62,12 @@ import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.text.ImageSpanThatDoesNotBreakShitForNoGoodReason;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.CoverImageView;
|
||||
@@ -107,7 +109,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private ImageView avatar;
|
||||
private CoverImageView cover;
|
||||
private View avatarBorder;
|
||||
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
|
||||
private TextView name, username, usernameDomain, bio, followersCount, followersLabel, followingCount, followingLabel;
|
||||
private ProgressBarButton actionButton;
|
||||
private ViewPager2 pager;
|
||||
private NestedRecyclerScrollView scrollView;
|
||||
@@ -185,6 +187,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
avatarBorder=content.findViewById(R.id.avatar_border);
|
||||
name=content.findViewById(R.id.name);
|
||||
username=content.findViewById(R.id.username);
|
||||
usernameDomain=content.findViewById(R.id.username_domain);
|
||||
bio=content.findViewById(R.id.bio);
|
||||
followersCount=content.findViewById(R.id.followers_count);
|
||||
followersLabel=content.findViewById(R.id.followers_label);
|
||||
@@ -320,6 +323,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
|
||||
|
||||
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
|
||||
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
@@ -499,22 +504,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
|
||||
if(account.locked){
|
||||
ssb=new SpannableStringBuilder("@");
|
||||
ssb.append(account.acct);
|
||||
if(isSelf){
|
||||
ssb.append('@');
|
||||
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
|
||||
}
|
||||
ssb=new SpannableStringBuilder(account.username);
|
||||
ssb.append(" ");
|
||||
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, getActivity().getTheme()).mutate();
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
lock.setTint(username.getCurrentTextColor());
|
||||
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
|
||||
ssb.append(getString(R.string.manually_approves_followers), new ImageSpanThatDoesNotBreakShitForNoGoodReason(lock, ImageSpan.ALIGN_BOTTOM), 0);
|
||||
username.setText(ssb);
|
||||
}else{
|
||||
// noinspection SetTextI18n
|
||||
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
|
||||
username.setText(account.username);
|
||||
}
|
||||
String domain=account.getDomain();
|
||||
if(TextUtils.isEmpty(domain))
|
||||
domain=AccountSessionManager.get(accountID).domain;
|
||||
usernameDomain.setText(domain);
|
||||
|
||||
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||
if(TextUtils.isEmpty(parsedBio)){
|
||||
bio.setVisibility(View.GONE);
|
||||
|
||||
@@ -90,7 +90,7 @@ public class Snackbar{
|
||||
if(current!=null)
|
||||
current.dismiss();
|
||||
current=this;
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
|
||||
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.LAST_APPLICATION_WINDOW, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
|
||||
lp.width=ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
lp.height=ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
lp.gravity=Gravity.BOTTOM;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.text.style.ImageSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class ImageSpanThatDoesNotBreakShitForNoGoodReason extends ImageSpan{
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Bitmap b){
|
||||
super(b);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Bitmap b, int verticalAlignment){
|
||||
super(b, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Bitmap bitmap){
|
||||
super(context, bitmap);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Bitmap bitmap, int verticalAlignment){
|
||||
super(context, bitmap, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable){
|
||||
super(drawable);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, int verticalAlignment){
|
||||
super(drawable, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, @NonNull String source){
|
||||
super(drawable, source);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Drawable drawable, @NonNull String source, int verticalAlignment){
|
||||
super(drawable, source, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Uri uri){
|
||||
super(context, uri);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, @NonNull Uri uri, int verticalAlignment){
|
||||
super(context, uri, verticalAlignment);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, int resourceId){
|
||||
super(context, resourceId);
|
||||
}
|
||||
|
||||
public ImageSpanThatDoesNotBreakShitForNoGoodReason(@NonNull Context context, int resourceId, int verticalAlignment){
|
||||
super(context, resourceId, verticalAlignment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
|
||||
// Purposefully not touching the font metrics
|
||||
return getDrawable().getBounds().right;
|
||||
}
|
||||
}
|
||||
@@ -929,11 +929,15 @@ public class UiUtils{
|
||||
public static void maybeShowTextCopiedToast(Context context){
|
||||
//show toast, android from S_V2 on has built-in popup, as documented in
|
||||
//https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications
|
||||
if(Build.VERSION.SDK_INT<=Build.VERSION_CODES.S_V2){
|
||||
if(needShowClipboardToast()){
|
||||
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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--);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user