Merge remote-tracking branch 'upstream/master'

This commit is contained in:
sk
2023-01-26 00:56:05 +01:00
8 changed files with 511 additions and 353 deletions

View File

@@ -5,6 +5,7 @@ import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -19,6 +20,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.parceler.Parcels; import org.parceler.Parcels;
@@ -42,6 +44,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Callback; import okhttp3.Callback;
@@ -58,6 +61,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
private ArrayList<Item> items=new ArrayList<>(); private ArrayList<Item> items=new ArrayList<>();
private Call currentRequest; private Call currentRequest;
private ItemsAdapter itemsAdapter; private ItemsAdapter itemsAdapter;
private ElevationOnScrollListener onScrollListener;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -72,7 +76,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)); setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
instance=Parcels.unwrap(getArguments().getParcelable("instance")); instance=Parcels.unwrap(getArguments().getParcelable("instance"));
items.add(new Item("Mastodon for Android Privacy Policy", "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png")); items.add(new Item("Mastodon for Android Privacy Policy", getString(R.string.privacy_policy_explanation), "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
loadServerPrivacyPolicy(); loadServerPrivacyPolicy();
} }
@@ -93,18 +97,24 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
list.setLayoutManager(new LinearLayoutManager(getActivity())); list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false); View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text); TextView text=headerView.findViewById(R.id.text);
text.setText(R.string.privacy_policy_subtitle); text.setText(getString(R.string.privacy_policy_subtitle, instance.uri));
adapter=new MergeRecyclerAdapter(); adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(itemsAdapter=new ItemsAdapter()); adapter.addAdapter(itemsAdapter=new ItemsAdapter());
list.setAdapter(adapter); list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next); btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick()); btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar); buttonBar=view.findViewById(R.id.button_bar);
Button backBtn=view.findViewById(R.id.btn_back);
backBtn.setText(getString(R.string.server_policy_disagree, instance.uri));
backBtn.setOnClickListener(v->{
setResult(false, null);
Nav.finish(this);
});
return view; return view;
} }
@@ -113,13 +123,17 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background)); setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background)); view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
} }
@Override @Override
protected void onUpdateToolbar(){ protected void onUpdateToolbar(){
super.onUpdateToolbar(); super.onUpdateToolbar();
getToolbar().setBackground(null); getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0); getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
} }
protected void onButtonClick(){ protected void onButtonClick(){
@@ -158,7 +172,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
if(!response.isSuccessful()) if(!response.isSuccessful())
return; return;
Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString()); Document doc=Jsoup.parse(Objects.requireNonNull(body).byteStream(), Objects.requireNonNull(body.contentType()).charset(StandardCharsets.UTF_8).name(), req.url().toString());
final Item item=new Item(doc.title(), instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico"); final Item item=new Item(doc.title(), null, instance.uri, req.url().toString(), "https://"+instance.uri+"/favicon.ico");
Activity activity=getActivity(); Activity activity=getActivity();
if(activity!=null){ if(activity!=null){
activity.runOnUiThread(()->{ activity.runOnUiThread(()->{
@@ -192,16 +206,23 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{ private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
private final TextView title; private final TextView title;
private final TextView subtitle;
public ItemViewHolder(){ public ItemViewHolder(){
super(getActivity(), R.layout.item_privacy_policy_link, list); super(getActivity(), R.layout.item_privacy_policy_link, list);
title=findViewById(R.id.title); title=findViewById(R.id.title);
title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); subtitle=findViewById(R.id.subtitle);
} }
@Override @Override
public void onBind(Item item){ public void onBind(Item item){
title.setText(item.title); title.setText(item.title);
if(TextUtils.isEmpty(item.subtitle)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(item.subtitle);
}
} }
@Override @Override
@@ -211,10 +232,11 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
} }
private static class Item{ private static class Item{
public String title, domain, url, faviconUrl; public String title, subtitle, domain, url, faviconUrl;
public Item(String title, String domain, String url, String faviconUrl){ public Item(String title, String subtitle, String domain, String url, String faviconUrl){
this.title=title; this.title=title;
this.subtitle=subtitle;
this.domain=domain; this.domain=domain;
this.url=url; this.url=url;
this.faviconUrl=faviconUrl; this.faviconUrl=faviconUrl;

View File

@@ -1,14 +1,8 @@
package org.joinmastodon.android.fragments.onboarding; package org.joinmastodon.android.fragments.onboarding;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
@@ -37,6 +31,7 @@ import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView; import org.joinmastodon.android.ui.views.FilterChipView;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList; import java.util.ArrayList;
@@ -56,7 +51,6 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
@@ -211,47 +205,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
setStatusBarColor(0); setStatusBarColor(0);
topBar=view.findViewById(R.id.top_bar); topBar=view.findViewById(R.id.top_bar);
LayerDrawable topBg=(LayerDrawable) topBar.getBackground().mutate(); list.addOnScrollListener(new ElevationOnScrollListener(null, topBar, buttonBar));
topBar.setBackground(topBg);
Drawable topOverlay=topBg.findDrawableByLayerId(R.id.color_overlay);
topOverlay.setAlpha(0);
LayerDrawable btmBg=(LayerDrawable) buttonBar.getBackground().mutate();
buttonBar.setBackground(btmBg);
Drawable btmOverlay=btmBg.findDrawableByLayerId(R.id.color_overlay);
btmOverlay.setAlpha(0);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
private boolean isAtTop=true;
private Animator currentPanelsAnim;
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
if(newAtTop!=isAtTop){
isAtTop=newAtTop;
if(currentPanelsAnim!=null)
currentPanelsAnim.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(topOverlay, "alpha", isAtTop ? 0 : 20),
ObjectAnimator.ofInt(btmOverlay, "alpha", isAtTop ? 0 : 20),
ObjectAnimator.ofFloat(topBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3)),
ObjectAnimator.ofFloat(buttonBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3))
);
set.setDuration(150);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentPanelsAnim=null;
}
});
set.start();
currentPanelsAnim=set;
}
}
});
searchEdit=view.findViewById(R.id.search_edit); searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed); searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
@@ -684,4 +638,5 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
return (this==GENERAL)==isGeneral; return (this==GENERAL)==isGeneral;
} }
} }
} }

View File

@@ -17,6 +17,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels; import org.parceler.Parcels;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -28,6 +29,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceRulesFragment extends ToolbarFragment{ public class InstanceRulesFragment extends ToolbarFragment{
@@ -36,6 +38,9 @@ public class InstanceRulesFragment extends ToolbarFragment{
private Button btn; private Button btn;
private View buttonBar; private View buttonBar;
private Instance instance; private Instance instance;
private ElevationOnScrollListener onScrollListener;
private static final int RULES_REQUEST=376;
@Override @Override
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
@@ -81,19 +86,31 @@ public class InstanceRulesFragment extends ToolbarFragment{
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
// setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background)); // setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
// view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background)); // view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
} }
@Override @Override
protected void onUpdateToolbar(){ protected void onUpdateToolbar(){
super.onUpdateToolbar(); super.onUpdateToolbar();
getToolbar().setBackground(null); getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0); getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
} }
protected void onButtonClick(){ protected void onButtonClick(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance)); args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), GoogleMadeMeAddThisFragment.class, args); Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
super.onFragmentResult(reqCode, success, result);
if(reqCode==RULES_REQUEST && !success){
Nav.finish(this);
}
} }
@Override @Override

View File

@@ -127,47 +127,48 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import okhttp3.MediaType; import okhttp3.MediaType;
public class UiUtils{ public class UiUtils {
private static Handler mainHandler=new Handler(Looper.getMainLooper()); private static Handler mainHandler = new Handler(Looper.getMainLooper());
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR=DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT=DateTimeFormatter.ofPattern("d MMM"); private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR = DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT = DateTimeFormatter.ofPattern("d MMM");
public static final DateTimeFormatter DATE_TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT); public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
private UiUtils(){} private UiUtils() {
}
public static void launchWebBrowser(Context context, String url){ public static void launchWebBrowser(Context context, String url) {
try{ try {
if(GlobalUserPreferences.useCustomTabs){ if (GlobalUserPreferences.useCustomTabs) {
new CustomTabsIntent.Builder() new CustomTabsIntent.Builder()
.setShowTitle(true) .setShowTitle(true)
.build() .build()
.launchUrl(context, Uri.parse(url)); .launchUrl(context, Uri.parse(url));
}else{ } else {
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
} }
}catch(ActivityNotFoundException x){ } catch (ActivityNotFoundException x) {
Toast.makeText(context, R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
} }
} }
public static String formatRelativeTimestamp(Context context, Instant instant){ public static String formatRelativeTimestamp(Context context, Instant instant) {
long t=instant.toEpochMilli(); long t = instant.toEpochMilli();
long now=System.currentTimeMillis(); long now = System.currentTimeMillis();
long diff=now-t; long diff = now - t;
if(diff<1000L){ if (diff < 1000L) {
return context.getString(R.string.time_now); return context.getString(R.string.time_now);
}else if(diff<60_000L){ } else if (diff < 60_000L) {
return context.getString(R.string.time_seconds, diff/1000L); return context.getString(R.string.time_seconds, diff / 1000L);
}else if(diff<3600_000L){ } else if (diff < 3600_000L) {
return context.getString(R.string.time_minutes, diff/60_000L); return context.getString(R.string.time_minutes, diff / 60_000L);
}else if(diff<3600_000L*24L){ } else if (diff < 3600_000L * 24L) {
return context.getString(R.string.time_hours, diff/3600_000L); return context.getString(R.string.time_hours, diff / 3600_000L);
}else{ } else {
int days=(int)(diff/(3600_000L*24L)); int days = (int) (diff / (3600_000L * 24L));
if(days>30){ if (days > 30) {
ZonedDateTime dt=instant.atZone(ZoneId.systemDefault()); ZonedDateTime dt = instant.atZone(ZoneId.systemDefault());
if(dt.getYear()==ZonedDateTime.now().getYear()){ if (dt.getYear() == ZonedDateTime.now().getYear()) {
return DATE_FORMATTER_SHORT.format(dt); return DATE_FORMATTER_SHORT.format(dt);
}else{ } else {
return DATE_FORMATTER_SHORT_WITH_YEAR.format(dt); return DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
} }
} }
@@ -175,216 +176,220 @@ public class UiUtils{
} }
} }
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant){ public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant) {
long t=instant.toEpochMilli(); long t = instant.toEpochMilli();
long now=System.currentTimeMillis(); long now = System.currentTimeMillis();
long diff=now-t; long diff = now - t;
if(diff<1000L){ if (diff < 1000L) {
return context.getString(R.string.time_just_now); return context.getString(R.string.time_just_now);
}else if(diff<60_000L){ } else if (diff < 60_000L) {
int secs=(int)(diff/1000L); int secs = (int) (diff / 1000L);
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs); return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
}else if(diff<3600_000L){ } else if (diff < 3600_000L) {
int mins=(int)(diff/60_000L); int mins = (int) (diff / 60_000L);
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins); return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
}else{ } else {
return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault())); return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
} }
} }
public static String formatTimeLeft(Context context, Instant instant){ public static String formatTimeLeft(Context context, Instant instant) {
long t=instant.toEpochMilli(); long t = instant.toEpochMilli();
long now=System.currentTimeMillis(); long now = System.currentTimeMillis();
long diff=t-now; long diff = t - now;
if(diff<60_000L){ if (diff < 60_000L) {
int secs=(int)(diff/1000L); int secs = (int) (diff / 1000L);
return context.getResources().getQuantityString(R.plurals.x_seconds_left, secs, secs); return context.getResources().getQuantityString(R.plurals.x_seconds_left, secs, secs);
}else if(diff<3600_000L){ } else if (diff < 3600_000L) {
int mins=(int)(diff/60_000L); int mins = (int) (diff / 60_000L);
return context.getResources().getQuantityString(R.plurals.x_minutes_left, mins, mins); return context.getResources().getQuantityString(R.plurals.x_minutes_left, mins, mins);
}else if(diff<3600_000L*24L){ } else if (diff < 3600_000L * 24L) {
int hours=(int)(diff/3600_000L); int hours = (int) (diff / 3600_000L);
return context.getResources().getQuantityString(R.plurals.x_hours_left, hours, hours); return context.getResources().getQuantityString(R.plurals.x_hours_left, hours, hours);
}else{ } else {
int days=(int)(diff/(3600_000L*24L)); int days = (int) (diff / (3600_000L * 24L));
return context.getResources().getQuantityString(R.plurals.x_days_left, days, days); return context.getResources().getQuantityString(R.plurals.x_days_left, days, days);
} }
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
public static String abbreviateNumber(int n){ public static String abbreviateNumber(int n) {
if(n<1000){ if (n < 1000) {
return String.format("%,d", n); return String.format("%,d", n);
}else if(n<1_000_000){ } else if (n < 1_000_000) {
float a=n/1000f; float a = n / 1000f;
return a>99f ? String.format("%,dK", (int)Math.floor(a)) : String.format("%,.1fK", a); return a > 99f ? String.format("%,dK", (int) Math.floor(a)) : String.format("%,.1fK", a);
}else{ } else {
float a=n/1_000_000f; float a = n / 1_000_000f;
return a>99f ? String.format("%,dM", (int)Math.floor(a)) : String.format("%,.1fM", n/1_000_000f); return a > 99f ? String.format("%,dM", (int) Math.floor(a)) : String.format("%,.1fM", n / 1_000_000f);
} }
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
public static String abbreviateNumber(long n){ public static String abbreviateNumber(long n) {
if(n<1_000_000_000L) if (n < 1_000_000_000L)
return abbreviateNumber((int)n); return abbreviateNumber((int) n);
double a=n/1_000_000_000.0; double a = n / 1_000_000_000.0;
return a>99f ? String.format("%,dB", (int)Math.floor(a)) : String.format("%,.1fB", n/1_000_000_000.0); return a > 99f ? String.format("%,dB", (int) Math.floor(a)) : String.format("%,.1fB", n / 1_000_000_000.0);
} }
/** /**
* Android 6.0 has a bug where start and end compound drawables don't get tinted. * Android 6.0 has a bug where start and end compound drawables don't get tinted.
* This works around it by setting the tint colors directly to the drawables. * This works around it by setting the tint colors directly to the drawables.
*
* @param textView * @param textView
*/ */
public static void fixCompoundDrawableTintOnAndroid6(TextView textView){ public static void fixCompoundDrawableTintOnAndroid6(TextView textView) {
Drawable[] drawables=textView.getCompoundDrawablesRelative(); Drawable[] drawables = textView.getCompoundDrawablesRelative();
for(int i=0;i<drawables.length;i++){ for (int i = 0; i < drawables.length; i++) {
if(drawables[i]!=null){ if (drawables[i] != null) {
Drawable tinted=drawables[i].mutate(); Drawable tinted = drawables[i].mutate();
tinted.setTintList(textView.getTextColors()); tinted.setTintList(textView.getTextColors());
drawables[i]=tinted; drawables[i] = tinted;
} }
} }
textView.setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]); textView.setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]);
} }
public static void runOnUiThread(Runnable runnable){ public static void runOnUiThread(Runnable runnable) {
mainHandler.post(runnable); mainHandler.post(runnable);
} }
public static void runOnUiThread(Runnable runnable, long delay){ public static void runOnUiThread(Runnable runnable, long delay) {
mainHandler.postDelayed(runnable, delay); mainHandler.postDelayed(runnable, delay);
} }
public static void removeCallbacks(Runnable runnable){ public static void removeCallbacks(Runnable runnable) {
mainHandler.removeCallbacks(runnable); mainHandler.removeCallbacks(runnable);
} }
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */ /**
* Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}.
*/
public static int lerp(int startValue, int endValue, float fraction) { public static int lerp(int startValue, int endValue, float fraction) {
return startValue + Math.round(fraction * (endValue - startValue)); return startValue + Math.round(fraction * (endValue - startValue));
} }
public static String getFileName(Uri uri){ public static String getFileName(Uri uri) {
if(uri.getScheme().equals("content")){ if (uri.getScheme().equals("content")) {
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)){ try (Cursor cursor = MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
cursor.moveToFirst(); cursor.moveToFirst();
String name=cursor.getString(0); String name = cursor.getString(0);
if(name!=null) if (name != null)
return name; return name;
}catch(Throwable ignore){} } catch (Throwable ignore) {
}
} }
return uri.getLastPathSegment(); return uri.getLastPathSegment();
} }
public static String formatFileSize(Context context, long size, boolean atLeastKB){ public static String formatFileSize(Context context, long size, boolean atLeastKB) {
if(size<1024 && !atLeastKB){ if (size < 1024 && !atLeastKB) {
return context.getString(R.string.file_size_bytes, size); return context.getString(R.string.file_size_bytes, size);
}else if(size<1024*1024){ } else if (size < 1024 * 1024) {
return context.getString(R.string.file_size_kb, size/1024.0); return context.getString(R.string.file_size_kb, size / 1024.0);
}else if(size<1024*1024*1024){ } else if (size < 1024 * 1024 * 1024) {
return context.getString(R.string.file_size_mb, size/(1024.0*1024.0)); return context.getString(R.string.file_size_mb, size / (1024.0 * 1024.0));
}else{ } else {
return context.getString(R.string.file_size_gb, size/(1024.0*1024.0*1024.0)); return context.getString(R.string.file_size_gb, size / (1024.0 * 1024.0 * 1024.0));
} }
} }
public static MediaType getFileMediaType(File file){ public static MediaType getFileMediaType(File file) {
String name=file.getName(); String name = file.getName();
return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.')+1))); return MediaType.parse(MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(name.lastIndexOf('.') + 1)));
} }
public static void loadCustomEmojiInTextView(TextView view){ public static void loadCustomEmojiInTextView(TextView view) {
CharSequence _text=view.getText(); CharSequence _text = view.getText();
if(!(_text instanceof Spanned)) if (!(_text instanceof Spanned))
return; return;
Spanned text=(Spanned)_text; Spanned text = (Spanned) _text;
CustomEmojiSpan[] spans=text.getSpans(0, text.length(), CustomEmojiSpan.class); CustomEmojiSpan[] spans = text.getSpans(0, text.length(), CustomEmojiSpan.class);
if(spans.length==0) if (spans.length == 0)
return; return;
int emojiSize=V.dp(20); int emojiSize = V.dp(20);
Map<Emoji, List<CustomEmojiSpan>> spansByEmoji=Arrays.stream(spans).collect(Collectors.groupingBy(s->s.emoji)); Map<Emoji, List<CustomEmojiSpan>> spansByEmoji = Arrays.stream(spans).collect(Collectors.groupingBy(s -> s.emoji));
for(Map.Entry<Emoji, List<CustomEmojiSpan>> emoji:spansByEmoji.entrySet()){ for (Map.Entry<Emoji, List<CustomEmojiSpan>> emoji : spansByEmoji.entrySet()) {
ViewImageLoader.load(new ViewImageLoader.Target(){ ViewImageLoader.load(new ViewImageLoader.Target() {
@Override @Override
public void setImageDrawable(Drawable d){ public void setImageDrawable(Drawable d) {
if(d==null) if (d == null)
return; return;
for(CustomEmojiSpan span:emoji.getValue()){ for (CustomEmojiSpan span : emoji.getValue()) {
span.setDrawable(d); span.setDrawable(d);
} }
view.invalidate(); view.invalidate();
} }
@Override @Override
public View getView(){ public View getView() {
return view; return view;
} }
}, null, new UrlImageLoaderRequest(emoji.getKey().url, emojiSize, emojiSize), null, false, true); }, null, new UrlImageLoaderRequest(emoji.getKey().url, emojiSize, emojiSize), null, false, true);
} }
} }
public static int getThemeColor(Context context, @AttrRes int attr){ public static int getThemeColor(Context context, @AttrRes int attr) {
TypedArray ta=context.obtainStyledAttributes(new int[]{attr}); TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
int color=ta.getColor(0, 0xff00ff00); int color = ta.getColor(0, 0xff00ff00);
ta.recycle(); ta.recycle();
return color; return color;
} }
public static void openProfileByID(Context context, String selfID, String id){ public static void openProfileByID(Context context, String selfID, String id) {
Bundle args=new Bundle(); Bundle args = new Bundle();
args.putString("account", selfID); args.putString("account", selfID);
args.putString("profileAccountID", id); args.putString("profileAccountID", id);
Nav.go((Activity)context, ProfileFragment.class, args); Nav.go((Activity) context, ProfileFragment.class, args);
} }
public static void openHashtagTimeline(Context context, String accountID, String hashtag, @Nullable Boolean following){ public static void openHashtagTimeline(Context context, String accountID, String hashtag, @Nullable Boolean following) {
Bundle args=new Bundle(); Bundle args = new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putString("hashtag", hashtag); args.putString("hashtag", hashtag);
if (following != null) args.putBoolean("following", following); if (following != null) args.putBoolean("following", following);
Nav.go((Activity)context, HashtagTimelineFragment.class, args); Nav.go((Activity) context, HashtagTimelineFragment.class, args);
} }
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){ public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed) {
showConfirmationAlert(context, title, message, confirmButton, 0, onConfirmed); showConfirmationAlert(context, title, message, confirmButton, 0, onConfirmed);
} }
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed){ public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, @DrawableRes int icon, Runnable onConfirmed) {
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), icon, onConfirmed); showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), icon, onConfirmed);
} }
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, int icon, Runnable onConfirmed){ public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, int icon, Runnable onConfirmed) {
new M3AlertDialogBuilder(context) new M3AlertDialogBuilder(context)
.setTitle(title) .setTitle(title)
.setMessage(message) .setMessage(message)
.setPositiveButton(confirmButton, (dlg, i)->onConfirmed.run()) .setPositiveButton(confirmButton, (dlg, i) -> onConfirmed.run())
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setIcon(icon) .setIcon(icon)
.show(); .show();
} }
public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback){ public static void confirmToggleBlockUser(Activity activity, String accountID, Account account, boolean currentlyBlocked, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title), showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_title : R.string.confirm_block_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName), activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, account.displayName),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
R.drawable.ic_fluent_person_prohibited_28_regular, R.drawable.ic_fluent_person_prohibited_28_regular,
()->{ () -> {
new SetAccountBlocked(account.id, !currentlyBlocked) new SetAccountBlocked(account.id, !currentlyBlocked)
.setCallback(new Callback<>(){ .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Relationship result){ public void onSuccess(Relationship result) {
if (activity == null) return; if (activity == null) return;
resultCallback.accept(result); resultCallback.accept(result);
if(!currentlyBlocked){ if (!currentlyBlocked) {
E.post(new RemoveAccountPostsEvent(accountID, account.id, false)); E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
} }
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error) {
error.showToast(activity); error.showToast(activity);
} }
}) })
@@ -393,7 +398,7 @@ public class UiUtils{
}); });
} }
public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback){ public static void confirmSoftBlockUser(Activity activity, String accountID, Account account, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity, showConfirmationAlert(activity,
activity.getString(R.string.sk_remove_follower), activity.getString(R.string.sk_remove_follower),
activity.getString(R.string.sk_remove_follower_confirm, account.displayName), activity.getString(R.string.sk_remove_follower_confirm, account.displayName),
@@ -426,21 +431,21 @@ public class UiUtils{
); );
} }
public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback){ public static void confirmToggleBlockDomain(Activity activity, String accountID, String domain, boolean currentlyBlocked, Runnable resultCallback) {
showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_domain_title : R.string.confirm_block_domain_title), showConfirmationAlert(activity, activity.getString(currentlyBlocked ? R.string.confirm_unblock_domain_title : R.string.confirm_block_domain_title),
activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, domain), activity.getString(currentlyBlocked ? R.string.confirm_unblock : R.string.confirm_block, domain),
activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block), activity.getString(currentlyBlocked ? R.string.do_unblock : R.string.do_block),
R.drawable.ic_fluent_shield_28_regular, R.drawable.ic_fluent_shield_28_regular,
()->{ () -> {
new SetDomainBlocked(domain, !currentlyBlocked) new SetDomainBlocked(domain, !currentlyBlocked)
.setCallback(new Callback<>(){ .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Object result){ public void onSuccess(Object result) {
resultCallback.run(); resultCallback.run();
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error) {
error.showToast(activity); error.showToast(activity);
} }
}) })
@@ -449,24 +454,24 @@ public class UiUtils{
}); });
} }
public static void confirmToggleMuteUser(Activity activity, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback){ public static void confirmToggleMuteUser(Activity activity, String accountID, Account account, boolean currentlyMuted, Consumer<Relationship> resultCallback) {
showConfirmationAlert(activity, activity.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title), showConfirmationAlert(activity, activity.getString(currentlyMuted ? R.string.confirm_unmute_title : R.string.confirm_mute_title),
activity.getString(currentlyMuted ? R.string.confirm_unmute : R.string.confirm_mute, account.displayName), activity.getString(currentlyMuted ? R.string.confirm_unmute : R.string.confirm_mute, account.displayName),
activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), activity.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute),
currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular, currentlyMuted ? R.drawable.ic_fluent_speaker_0_28_regular : R.drawable.ic_fluent_speaker_off_28_regular,
()->{ () -> {
new SetAccountMuted(account.id, !currentlyMuted) new SetAccountMuted(account.id, !currentlyMuted)
.setCallback(new Callback<>(){ .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Relationship result){ public void onSuccess(Relationship result) {
resultCallback.accept(result); resultCallback.accept(result);
if(!currentlyMuted){ if (!currentlyMuted) {
E.post(new RemoveAccountPostsEvent(accountID, account.id, false)); E.post(new RemoveAccountPostsEvent(accountID, account.id, false));
} }
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error) {
error.showToast(activity); error.showToast(activity);
} }
}) })
@@ -474,27 +479,28 @@ public class UiUtils{
.exec(accountID); .exec(accountID);
}); });
} }
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback) {
confirmDeletePost(activity, accountID, status, resultCallback, false); confirmDeletePost(activity, accountID, status, resultCallback, false);
} }
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft){ public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback, boolean forRedraft) {
showConfirmationAlert(activity, showConfirmationAlert(activity,
forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title, forRedraft ? R.string.sk_confirm_delete_and_redraft_title : R.string.confirm_delete_title,
forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete, forRedraft ? R.string.sk_confirm_delete_and_redraft : R.string.confirm_delete,
forRedraft ? R.string.sk_delete_and_redraft : R.string.delete, forRedraft ? R.string.sk_delete_and_redraft : R.string.delete,
forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular, forRedraft ? R.drawable.ic_fluent_arrow_clockwise_28_regular : R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus(status.id) () -> new DeleteStatus(status.id)
.setCallback(new Callback<>(){ .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Status result){ public void onSuccess(Status result) {
resultCallback.accept(result); resultCallback.accept(result);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id); AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
E.post(new StatusDeletedEvent(status.id, accountID)); E.post(new StatusDeletedEvent(status.id, accountID));
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error) {
error.showToast(activity); error.showToast(activity);
} }
}) })
@@ -503,7 +509,7 @@ public class UiUtils{
); );
} }
public static void confirmDeleteScheduledPost(Activity activity, String accountID, ScheduledStatus status, Runnable resultCallback){ public static void confirmDeleteScheduledPost(Activity activity, String accountID, ScheduledStatus status, Runnable resultCallback) {
boolean isDraft = status.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT); boolean isDraft = status.scheduledAt.isAfter(CreateStatus.DRAFTS_AFTER_INSTANT);
showConfirmationAlert(activity, showConfirmationAlert(activity,
isDraft ? R.string.sk_confirm_delete_draft_title : R.string.sk_confirm_delete_scheduled_post_title, isDraft ? R.string.sk_confirm_delete_draft_title : R.string.sk_confirm_delete_scheduled_post_title,
@@ -511,15 +517,15 @@ public class UiUtils{
R.string.delete, R.string.delete,
R.drawable.ic_fluent_delete_28_regular, R.drawable.ic_fluent_delete_28_regular,
() -> new DeleteStatus.Scheduled(status.id) () -> new DeleteStatus.Scheduled(status.id)
.setCallback(new Callback<>(){ .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Object o){ public void onSuccess(Object o) {
resultCallback.run(); resultCallback.run();
E.post(new ScheduledStatusDeletedEvent(status.id, accountID)); E.post(new ScheduledStatusDeletedEvent(status.id, accountID));
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error) {
error.showToast(activity); error.showToast(activity);
} }
}) })
@@ -528,13 +534,13 @@ public class UiUtils{
); );
} }
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){ public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback) {
showConfirmationAlert(activity, showConfirmationAlert(activity,
pinned ? R.string.sk_confirm_pin_post_title : R.string.sk_confirm_unpin_post_title, pinned ? R.string.sk_confirm_pin_post_title : R.string.sk_confirm_unpin_post_title,
pinned ? R.string.sk_confirm_pin_post : R.string.sk_confirm_unpin_post, pinned ? R.string.sk_confirm_pin_post : R.string.sk_confirm_unpin_post,
pinned ? R.string.sk_pin_post : R.string.sk_unpin_post, pinned ? R.string.sk_pin_post : R.string.sk_unpin_post,
pinned ? R.drawable.ic_fluent_pin_28_regular : R.drawable.ic_fluent_pin_off_28_regular, pinned ? R.drawable.ic_fluent_pin_28_regular : R.drawable.ic_fluent_pin_off_28_regular,
()->{ () -> {
new SetStatusPinned(status.id, pinned) new SetStatusPinned(status.id, pinned)
.setCallback(new Callback<>() { .setCallback(new Callback<>() {
@Override @Override
@@ -597,42 +603,42 @@ public class UiUtils{
.exec(accountID)); .exec(accountID));
} }
public static void setRelationshipToActionButton(Relationship relationship, Button button){ public static void setRelationshipToActionButton(Relationship relationship, Button button) {
setRelationshipToActionButton(relationship, button, false); setRelationshipToActionButton(relationship, button, false);
} }
public static void setRelationshipToActionButton(Relationship relationship, Button button, boolean keepText){ public static void setRelationshipToActionButton(Relationship relationship, Button button, boolean keepText) {
CharSequence textBefore = keepText ? button.getText() : null; CharSequence textBefore = keepText ? button.getText() : null;
boolean secondaryStyle; boolean secondaryStyle;
if(relationship.blocking){ if (relationship.blocking) {
button.setText(R.string.button_blocked); button.setText(R.string.button_blocked);
secondaryStyle=true; secondaryStyle = true;
}else if(relationship.blockedBy){ } else if (relationship.blockedBy) {
button.setText(R.string.button_follow); button.setText(R.string.button_follow);
secondaryStyle=false; secondaryStyle = false;
}else if(relationship.requested){ } else if (relationship.requested) {
button.setText(R.string.button_follow_pending); button.setText(R.string.button_follow_pending);
secondaryStyle=true; secondaryStyle = true;
}else if(!relationship.following){ } else if (!relationship.following) {
button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow); button.setText(relationship.followedBy ? R.string.follow_back : R.string.button_follow);
secondaryStyle=false; secondaryStyle = false;
}else{ } else {
button.setText(R.string.button_following); button.setText(R.string.button_following);
secondaryStyle=true; secondaryStyle = true;
} }
if (keepText) button.setText(textBefore); if (keepText) button.setText(textBefore);
button.setEnabled(!relationship.blockedBy); button.setEnabled(!relationship.blockedBy);
int attr=secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle; int attr = secondaryStyle ? R.attr.secondaryButtonStyle : android.R.attr.buttonStyle;
TypedArray ta=button.getContext().obtainStyledAttributes(new int[]{attr}); TypedArray ta = button.getContext().obtainStyledAttributes(new int[]{attr});
int styleRes=ta.getResourceId(0, 0); int styleRes = ta.getResourceId(0, 0);
ta.recycle(); ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background}); ta = button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
button.setBackground(ta.getDrawable(0)); button.setBackground(ta.getDrawable(0));
ta.recycle(); ta.recycle();
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor}); ta = button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
if(relationship.blocking) if (relationship.blocking)
button.setTextColor(button.getResources().getColorStateList(R.color.error_600)); button.setTextColor(button.getResources().getColorStateList(R.color.error_600));
else else
button.setTextColor(ta.getColorStateList(0)); button.setTextColor(ta.getColorStateList(0));
@@ -647,7 +653,7 @@ public class UiUtils{
public void onSuccess(Relationship result) { public void onSuccess(Relationship result) {
resultCallback.accept(result); resultCallback.accept(result);
progressCallback.accept(false); progressCallback.accept(false);
Toast.makeText(activity, activity.getString(result.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username), Toast.LENGTH_SHORT).show(); Toast.makeText(activity, activity.getString(result.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@' + account.username), Toast.LENGTH_SHORT).show();
} }
@Override @Override
@@ -658,26 +664,26 @@ public class UiUtils{
}).exec(accountID); }).exec(accountID);
} }
public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback){ public static void performAccountAction(Activity activity, Account account, String accountID, Relationship relationship, Button button, Consumer<Boolean> progressCallback, Consumer<Relationship> resultCallback) {
if(relationship.blocking){ if (relationship.blocking) {
confirmToggleBlockUser(activity, accountID, account, true, resultCallback); confirmToggleBlockUser(activity, accountID, account, true, resultCallback);
}else if(relationship.muting){ } else if (relationship.muting) {
confirmToggleMuteUser(activity, accountID, account, true, resultCallback); confirmToggleMuteUser(activity, accountID, account, true, resultCallback);
}else{ } else {
progressCallback.accept(true); progressCallback.accept(true);
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true, false) new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true, false)
.setCallback(new Callback<>(){ .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Relationship result){ public void onSuccess(Relationship result) {
resultCallback.accept(result); resultCallback.accept(result);
progressCallback.accept(false); progressCallback.accept(false);
if(!result.following){ if (!result.following) {
E.post(new RemoveAccountPostsEvent(accountID, account.id, true)); E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
} }
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error) {
error.showToast(activity); error.showToast(activity);
progressCallback.accept(false); progressCallback.accept(false);
} }
@@ -707,7 +713,8 @@ public class UiUtils{
@Override @Override
public void onSuccess(Relationship rel) { public void onSuccess(Relationship rel) {
E.post(new FollowRequestHandledEvent(accountID, false, account, rel)); E.post(new FollowRequestHandledEvent(accountID, false, account, rel));
if (notificationID != null) E.post(new NotificationDeletedEvent(notificationID)); if (notificationID != null)
E.post(new NotificationDeletedEvent(notificationID));
resultCallback.accept(rel); resultCallback.accept(rel);
} }
@@ -720,34 +727,34 @@ public class UiUtils{
} }
} }
public static <T> void updateList(List<T> oldList, List<T> newList, RecyclerView list, RecyclerView.Adapter<?> adapter, BiPredicate<T, T> areItemsSame){ public static <T> void updateList(List<T> oldList, List<T> newList, RecyclerView list, RecyclerView.Adapter<?> adapter, BiPredicate<T, T> areItemsSame) {
// Save topmost item position and offset because for some reason RecyclerView would scroll the list to weird places when you insert items at the top // Save topmost item position and offset because for some reason RecyclerView would scroll the list to weird places when you insert items at the top
int topItem, topItemOffset; int topItem, topItemOffset;
if(list.getChildCount()==0){ if (list.getChildCount() == 0) {
topItem=topItemOffset=0; topItem = topItemOffset = 0;
}else{ } else {
View child=list.getChildAt(0); View child = list.getChildAt(0);
topItem=list.getChildAdapterPosition(child); topItem = list.getChildAdapterPosition(child);
topItemOffset=child.getTop(); topItemOffset = child.getTop();
} }
DiffUtil.calculateDiff(new DiffUtil.Callback(){ DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override @Override
public int getOldListSize(){ public int getOldListSize() {
return oldList.size(); return oldList.size();
} }
@Override @Override
public int getNewListSize(){ public int getNewListSize() {
return newList.size(); return newList.size();
} }
@Override @Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return areItemsSame.test(oldList.get(oldItemPosition), newList.get(newItemPosition)); return areItemsSame.test(oldList.get(oldItemPosition), newList.get(newItemPosition));
} }
@Override @Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return true; return true;
} }
}).dispatchUpdatesTo(adapter); }).dispatchUpdatesTo(adapter);
@@ -755,77 +762,80 @@ public class UiUtils{
list.scrollBy(0, topItemOffset); list.scrollBy(0, topItemOffset);
} }
public static Bitmap getBitmapFromDrawable(Drawable d){ public static Bitmap getBitmapFromDrawable(Drawable d) {
if(d instanceof BitmapDrawable) if (d instanceof BitmapDrawable)
return ((BitmapDrawable) d).getBitmap(); return ((BitmapDrawable) d).getBitmap();
Bitmap bitmap=Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
d.draw(new Canvas(bitmap)); d.draw(new Canvas(bitmap));
return bitmap; return bitmap;
} }
public static void insetPopupMenuIcon(Context context, MenuItem item) { public static void insetPopupMenuIcon(Context context, MenuItem item) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary)); ColorStateList iconTint = ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
insetPopupMenuIcon(item, iconTint); insetPopupMenuIcon(item, iconTint);
} }
public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) { public static void insetPopupMenuIcon(MenuItem item, ColorStateList iconTint) {
Drawable icon=item.getIcon().mutate(); Drawable icon = item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26) item.setIconTintList(iconTint); if (Build.VERSION.SDK_INT >= 26) item.setIconTintList(iconTint);
else icon.setTintList(iconTint); else icon.setTintList(iconTint);
icon=new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0); icon = new InsetDrawable(icon, V.dp(8), 0, V.dp(8), 0);
item.setIcon(icon); item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle()); SpannableStringBuilder ssb = new SpannableStringBuilder(item.getTitle());
item.setTitle(ssb); item.setTitle(ssb);
} }
public static void resetPopupItemTint(MenuItem item) { public static void resetPopupItemTint(MenuItem item) {
if(Build.VERSION.SDK_INT>=26) { if (Build.VERSION.SDK_INT >= 26) {
item.setIconTintList(null); item.setIconTintList(null);
} else { } else {
Drawable icon=item.getIcon().mutate(); Drawable icon = item.getIcon().mutate();
icon.setTintList(null); icon.setTintList(null);
item.setIcon(icon); item.setIcon(icon);
} }
} }
public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) { public static void enableOptionsMenuIcons(Context context, Menu menu, @IdRes int... asAction) {
if(menu.getClass().getSimpleName().equals("MenuBuilder")){ if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
try { try {
Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE); Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true); m.setAccessible(true);
m.invoke(menu, true); m.invoke(menu, true);
enableMenuIcons(context, menu, asAction); enableMenuIcons(context, menu, asAction);
} catch (Exception ignored) {
} }
catch(Exception ignored){}
} }
} }
public static void enableMenuIcons(Context context, Menu m, @IdRes int... exclude) { public static void enableMenuIcons(Context context, Menu m, @IdRes int... exclude) {
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary)); ColorStateList iconTint = ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){ for (int i = 0; i < m.size(); i++) {
MenuItem item=m.getItem(i); MenuItem item = m.getItem(i);
SubMenu subMenu = item.getSubMenu(); SubMenu subMenu = item.getSubMenu();
if (subMenu != null) enableMenuIcons(context, subMenu, exclude); if (subMenu != null) enableMenuIcons(context, subMenu, exclude);
if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId())) continue; if (item.getIcon() == null || Arrays.stream(exclude).anyMatch(id -> id == item.getItemId()))
continue;
insetPopupMenuIcon(item, iconTint); insetPopupMenuIcon(item, iconTint);
} }
} }
public static void enablePopupMenuIcons(Context context, PopupMenu menu){ public static void enablePopupMenuIcons(Context context, PopupMenu menu) {
Menu m=menu.getMenu(); Menu m = menu.getMenu();
if(Build.VERSION.SDK_INT>=29){ if (Build.VERSION.SDK_INT >= 29) {
menu.setForceShowIcon(true); menu.setForceShowIcon(true);
}else{ } else {
try{ try {
Method setOptionalIconsVisible=m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class); Method setOptionalIconsVisible = m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
setOptionalIconsVisible.setAccessible(true); setOptionalIconsVisible.setAccessible(true);
setOptionalIconsVisible.invoke(m, true); setOptionalIconsVisible.invoke(m, true);
}catch(Exception ignore){} } catch (Exception ignore) {
}
} }
enableMenuIcons(context, m); enableMenuIcons(context, m);
} }
public static void setUserPreferredTheme(Context context){ public static void setUserPreferredTheme(Context context) {
context.setTheme(switch (theme) { context.setTheme(switch (theme) {
case LIGHT -> R.style.Theme_Mastodon_Light; case LIGHT -> R.style.Theme_Mastodon_Light;
case DARK -> trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark; case DARK -> trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
@@ -835,10 +845,11 @@ public class UiUtils{
ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color); ColorPalette palette = ColorPalette.palettes.get(GlobalUserPreferences.color);
if (palette != null) palette.apply(context); if (palette != null) palette.apply(context);
} }
public static boolean isDarkTheme(){
if(theme==GlobalUserPreferences.ThemePreference.AUTO) public static boolean isDarkTheme() {
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES; if (theme == GlobalUserPreferences.ThemePreference.AUTO)
return theme==GlobalUserPreferences.ThemePreference.DARK; return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
return theme == GlobalUserPreferences.ThemePreference.DARK;
} }
// https://mastodon.foo.bar/@User // https://mastodon.foo.bar/@User
@@ -865,7 +876,8 @@ public class UiUtils{
return false; return false;
} }
if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null) return false; if (uri.getQuery() != null || uri.getFragment() != null || uri.getPath() == null)
return false;
String it = uri.getPath(); String it = uri.getPath();
return it.matches("^/@[^/]+$") || return it.matches("^/@[^/]+$") ||
@@ -890,8 +902,8 @@ public class UiUtils{
} }
public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) { public static void pickAccount(Context context, String exceptFor, @StringRes int titleRes, @DrawableRes int iconRes, Consumer<AccountSession> sessionConsumer, Consumer<AlertDialog.Builder> transformDialog) {
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts() List<AccountSession> sessions = AccountSessionManager.getInstance().getLoggedInAccounts()
.stream().filter(s->!s.getID().equals(exceptFor)).collect(Collectors.toList()); .stream().filter(s -> !s.getID().equals(exceptFor)).collect(Collectors.toList());
AlertDialog.Builder builder = new M3AlertDialogBuilder(context) AlertDialog.Builder builder = new M3AlertDialogBuilder(context)
.setItems( .setItems(
@@ -966,7 +978,8 @@ public class UiUtils{
@Override @Override
public void onSuccess(SearchResults results) { public void onSuccess(SearchResults results) {
if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0)); if (!results.statuses.isEmpty()) statusConsumer.accept(results.statuses.get(0));
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); else
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
} }
@Override @Override
@@ -974,7 +987,7 @@ public class UiUtils{
error.showToast(context); error.showToast(context);
} }
}) })
.wrapProgress((Activity)context, R.string.loading, true, .wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, targetAccountID, null, d)) d -> transformDialogForLookup(context, targetAccountID, null, d))
.exec(targetAccountID); .exec(targetAccountID);
} }
@@ -998,28 +1011,28 @@ public class UiUtils{
} }
} }
public static void openURL(Context context, String accountID, String url, boolean launchBrowser){ public static void openURL(Context context, String accountID, String url, boolean launchBrowser) {
Uri uri=Uri.parse(url); Uri uri = Uri.parse(url);
List<String> path=uri.getPathSegments(); List<String> path = uri.getPathSegments();
if(accountID!=null && "https".equals(uri.getScheme())){ if (accountID != null && "https".equals(uri.getScheme())) {
if(path.size()==2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())){ if (path.size() == 2 && path.get(0).matches("^@[a-zA-Z0-9_]+$") && path.get(1).matches("^[0-9]+$") && AccountSessionManager.getInstance().getAccount(accountID).domain.equalsIgnoreCase(uri.getAuthority())) {
new GetStatusByID(path.get(1)) new GetStatusByID(path.get(1))
.setCallback(new Callback<>(){ .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(Status result){ public void onSuccess(Status result) {
Bundle args=new Bundle(); Bundle args = new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
args.putParcelable("status", Parcels.wrap(result)); args.putParcelable("status", Parcels.wrap(result));
Nav.go((Activity) context, ThreadFragment.class, args); Nav.go((Activity) context, ThreadFragment.class, args);
} }
@Override @Override
public void onError(ErrorResponse error){ public void onError(ErrorResponse error) {
error.showToast(context); error.showToast(context);
if (launchBrowser) launchWebBrowser(context, url); if (launchBrowser) launchWebBrowser(context, url);
} }
}) })
.wrapProgress((Activity)context, R.string.loading, true, .wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d)) d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID); .exec(accountID);
return; return;
@@ -1028,7 +1041,7 @@ public class UiUtils{
.setCallback(new Callback<>() { .setCallback(new Callback<>() {
@Override @Override
public void onSuccess(SearchResults results) { public void onSuccess(SearchResults results) {
Bundle args=new Bundle(); Bundle args = new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
if (!results.statuses.isEmpty()) { if (!results.statuses.isEmpty()) {
args.putParcelable("status", Parcels.wrap(results.statuses.get(0))); args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
@@ -1038,7 +1051,8 @@ public class UiUtils{
Nav.go((Activity) context, ProfileFragment.class, args); Nav.go((Activity) context, ProfileFragment.class, args);
} else { } else {
if (launchBrowser) launchWebBrowser(context, url); if (launchBrowser) launchWebBrowser(context, url);
else Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show(); else
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
} }
} }
@@ -1048,7 +1062,7 @@ public class UiUtils{
if (launchBrowser) launchWebBrowser(context, url); if (launchBrowser) launchWebBrowser(context, url);
} }
}) })
.wrapProgress((Activity)context, R.string.loading, true, .wrapProgress((Activity) context, R.string.loading, true,
d -> transformDialogForLookup(context, accountID, url, d)) d -> transformDialogForLookup(context, accountID, url, d))
.exec(accountID); .exec(accountID);
return; return;
@@ -1060,36 +1074,45 @@ public class UiUtils{
public static void copyText(View v, String text) { public static void copyText(View v, String text) {
Context context = v.getContext(); Context context = v.getContext();
context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text)); context.getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, text));
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()) { // Android 13+ SystemUI shows its own thing when you put things into the clipboard
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
} }
v.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK); v.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
} }
private static String getSystemProperty(String key){ private static String getSystemProperty(String key) {
try{ try {
Class<?> props=Class.forName("android.os.SystemProperties"); Class<?> props = Class.forName("android.os.SystemProperties");
Method get=props.getMethod("get", String.class); Method get = props.getMethod("get", String.class);
return (String)get.invoke(null, key); return (String) get.invoke(null, key);
}catch(Exception ignore){} } catch (Exception ignore) {
}
return null; return null;
} }
public static boolean isMIUI(){ public static boolean isMIUI() {
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code")); return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
} }
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText){ public static int alphaBlendColors(int color1, int color2, float alpha) {
float alpha0 = 1f - alpha;
int r = Math.round(((color1 >> 16) & 0xFF) * alpha0 + ((color2 >> 16) & 0xFF) * alpha);
int g = Math.round(((color1 >> 8) & 0xFF) * alpha0 + ((color2 >> 8) & 0xFF) * alpha);
int b = Math.round((color1 & 0xFF) * alpha0 + (color2 & 0xFF) * alpha);
return 0xFF000000 | (r << 16) | (g << 8) | b;
}
public static boolean pickAccountForCompose(Activity activity, String accountID, String prefilledText) {
Bundle args = new Bundle(); Bundle args = new Bundle();
if (prefilledText != null) args.putString("prefilledText", prefilledText); if (prefilledText != null) args.putString("prefilledText", prefilledText);
return pickAccountForCompose(activity, accountID, args); return pickAccountForCompose(activity, accountID, args);
} }
public static boolean pickAccountForCompose(Activity activity, String accountID){ public static boolean pickAccountForCompose(Activity activity, String accountID) {
return pickAccountForCompose(activity, accountID, (String) null); return pickAccountForCompose(activity, accountID, (String) null);
} }
public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args){ public static boolean pickAccountForCompose(Activity activity, String accountID, Bundle args) {
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1) { if (AccountSessionManager.getInstance().getLoggedInAccounts().size() > 1) {
UiUtils.pickAccount(activity, accountID, 0, 0, session -> { UiUtils.pickAccount(activity, accountID, 0, 0, session -> {
args.putString("account", session.getID()); args.putString("account", session.getID());

View File

@@ -0,0 +1,116 @@
package org.joinmastodon.android.utils;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ElevationOnScrollListener extends RecyclerView.OnScrollListener implements View.OnScrollChangeListener{
private boolean isAtTop;
private Animator currentPanelsAnim;
private View[] views;
private FragmentRootLinearLayout fragmentRootLayout;
public ElevationOnScrollListener(FragmentRootLinearLayout fragmentRootLayout, View... views){
isAtTop=true;
this.fragmentRootLayout=fragmentRootLayout;
this.views=views;
for(View v:views){
Drawable bg=v.getBackground().mutate();
v.setBackground(bg);
if(bg instanceof LayerDrawable ld){
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
if(overlay!=null){
overlay.setAlpha(0);
}
}
}
}
public void setViews(View... views){
List<View> oldViews=Arrays.asList(this.views);
this.views=views;
for(View v:views){
if(oldViews.contains(v))
continue;
Drawable bg=v.getBackground().mutate();
v.setBackground(bg);
if(bg instanceof LayerDrawable ld){
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
if(overlay!=null){
overlay.setAlpha(isAtTop ? 0 : 20);
}
}
v.setTranslationZ(isAtTop ? 0 : V.dp(3));
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
handleScroll(recyclerView.getContext(), newAtTop);
}
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
handleScroll(v.getContext(), scrollY==0);
}
private void handleScroll(Context context, boolean newAtTop){
if(newAtTop!=isAtTop){
isAtTop=newAtTop;
if(currentPanelsAnim!=null)
currentPanelsAnim.cancel();
AnimatorSet set=new AnimatorSet();
ArrayList<Animator> anims=new ArrayList<>();
for(View v:views){
if(v.getBackground() instanceof LayerDrawable ld){
Drawable overlay=ld.findDrawableByLayerId(R.id.color_overlay);
if(overlay!=null){
anims.add(ObjectAnimator.ofInt(overlay, "alpha", isAtTop ? 0 : 20));
}
}
anims.add(ObjectAnimator.ofFloat(v, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3)));
}
if(fragmentRootLayout!=null){
int color;
if(isAtTop){
color=UiUtils.getThemeColor(context, R.attr.colorM3Background);
}else{
color=UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Background), UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.07843137f);
}
anims.add(ObjectAnimator.ofArgb(fragmentRootLayout, "statusBarColor", color));
}
set.playTogether(anims);
set.setDuration(150);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentPanelsAnim=null;
}
});
set.start();
currentPanelsAnim=set;
}
}
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -11,6 +11,7 @@
android:layout_weight="1"/> android:layout_weight="1"/>
<LinearLayout <LinearLayout
android:background="@drawable/bg_onboarding_panel"
android:id="@+id/button_bar" android:id="@+id/button_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -41,4 +42,4 @@
</LinearLayout> </LinearLayout>
</me.grishka.appkit.views.FragmentRootLinearLayout> </LinearLayout>

View File

@@ -1,18 +1,39 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingEnd="24dp"
android:paddingBottom="12dp" android:paddingBottom="12dp"
android:paddingStart="16dp" android:paddingStart="16dp"
android:textSize="16sp" android:paddingEnd="24dp">
android:textColor="?colorM3Primary"
android:drawableStart="@drawable/ic_outline_link_24"
android:drawablePadding="16dp"
android:drawableTint="?colorM3Primary"
tools:text="Privacy Policy - example.social">
</TextView> <View
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:backgroundTint="?colorM3Primary"
android:background="@drawable/ic_outline_link_24"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/icon"
android:layout_marginStart="16dp"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3Primary"
tools:text="Privacy Policy - example.social"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/icon"
android:layout_below="@id/title"
android:layout_marginStart="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="Privacy Policy - example.social"/>
</RelativeLayout>

View File

@@ -389,7 +389,7 @@
<string name="download_update">Download (%s)</string> <string name="download_update">Download (%s)</string>
<string name="install_update">Install</string> <string name="install_update">Install</string>
<string name="privacy_policy_title">Your Privacy</string> <string name="privacy_policy_title">Your Privacy</string>
<string name="privacy_policy_subtitle">Although the Mastodon app does not collect any data, the server you sign up through may have a different policy. Take a minute to review and agree to the Mastodon app privacy policy and your server\'s privacy policy.</string> <string name="privacy_policy_subtitle">Although the Mastodon app does not collect any data, the server you sign up through may have a different policy.\n\nIf you disagree with the policy for %s, you can go back and pick a different server.</string>
<string name="i_agree">I Agree</string> <string name="i_agree">I Agree</string>
<string name="empty_list">This list is empty</string> <string name="empty_list">This list is empty</string>
<string name="instance_signup_closed">This server does not accept new registrations.</string> <string name="instance_signup_closed">This server does not accept new registrations.</string>
@@ -429,4 +429,7 @@
<string name="popular_on_mastodon">Popular on Mastodon</string> <string name="popular_on_mastodon">Popular on Mastodon</string>
<string name="follow_all">Follow all</string> <string name="follow_all">Follow all</string>
<string name="server_rules_disagree">Disagree</string> <string name="server_rules_disagree">Disagree</string>
<string name="privacy_policy_explanation">TL;DR: We don\'t collect or process anything.</string>
<!-- %s is server domain -->
<string name="server_policy_disagree">Disagree with %s</string>
</resources> </resources>