Wrapstodon wip

This commit is contained in:
Grishka 2024-01-20 12:08:34 +03:00
parent 48f9aabaf7
commit 1f9147624f
62 changed files with 3868 additions and 2 deletions

View File

@ -9,7 +9,7 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 82
versionCode 83
versionName "2.2.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"

View File

@ -84,6 +84,12 @@
</intent-filter>
</receiver>
<provider
android:authorities="${applicationId}.provider.wrapstodon_share"
android:name=".WrapstodonImageProvider"
android:exported="false"
android:grantUriPermissions="true"/>
</application>
</manifest>

View File

@ -0,0 +1,74 @@
package org.joinmastodon.android;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import java.io.File;
import java.io.FileNotFoundException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class WrapstodonImageProvider extends ContentProvider{
@Override
public boolean onCreate(){
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
File image=new File(getContext().getCacheDir(), "wrapstodon.png");
if(projection==null)
projection=new String[]{"_display_name"};
MatrixCursor cursor=new MatrixCursor(projection);
if(!image.exists())
return cursor;
Object[] values=new Object[projection.length];
for(int i=0;i<projection.length;i++){
if("_display_name".equals(projection[i]))
values[i]="wrapstodon.png";
if("_size".equals(projection[i]))
values[i]=image.length();
}
cursor.addRow(values);
return cursor;
}
@Nullable
@Override
public String getType(@NonNull Uri uri){
return "image/png";
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values){
throw new UnsupportedOperationException();
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs){
throw new UnsupportedOperationException();
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs){
throw new UnsupportedOperationException();
}
@Nullable
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
if(!"r".equals(mode))
throw new FileNotFoundException("Unsupported mode");
File image=new File(getContext().getCacheDir(), "wrapstodon.png");
if(!image.exists())
throw new FileNotFoundException();
return ParcelFileDescriptor.open(image, ParcelFileDescriptor.MODE_READ_ONLY);
}
}

View File

@ -0,0 +1,44 @@
package org.joinmastodon.android.api.requests.annual_reports;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AnnualReport;
import org.joinmastodon.android.model.BaseModel;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetAnnualReports extends MastodonAPIRequest<GetAnnualReports.Response>{
public GetAnnualReports(){
super(HttpMethod.GET, "/annual_reports", Response.class);
}
@AllFieldsAreRequired
public static class Response extends BaseModel{
public List<AnnualReportYear> annualReports;
public List<Account> accounts;
public List<Status> statuses;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
for(AnnualReportYear r:annualReports){
if(r.data==null)
throw new ObjectValidationException("data is null");
r.data.postprocess();
}
for(Account a:accounts)
a.postprocess();
for(Status s:statuses)
s.postprocess();
}
public static class AnnualReportYear{
public int year;
public AnnualReport data;
}
}
}

View File

@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.os.Bundle;
import android.text.TextUtils;
@ -41,9 +42,12 @@ import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
@ -81,6 +85,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private FollowList currentList;
private MergeRecyclerAdapter mergeAdapter;
private DiscoverInfoBannerHelper localTimelineBannerHelper;
private HideableSingleViewRecyclerAdapter yearlyWrapBannerAdapter;
private String maxID;
private String lastSavedMarkerID;
@ -635,7 +640,11 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
@Override
protected RecyclerView.Adapter getAdapter(){
yearlyWrapBannerAdapter=new HideableSingleViewRecyclerAdapter(makeWrapstodonBannerView());
// TODO only show banner when some condition is met (TBD)
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(yearlyWrapBannerAdapter);
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@ -661,6 +670,43 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
};
}
private View makeWrapstodonBannerView(){
int bgColor, accentColor, iconContainerColor, textColor;
accentColor=0xFFBAFF3B;
bgColor=0xFF2F0C7A;
iconContainerColor=0xFF563ACC;
if(UiUtils.isDarkTheme()){
textColor=0xFFEEE5FF;
}else{
textColor=0xFFCCCFFF;
}
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
TextView bannerText=banner.findViewById(R.id.text);
TextView bannerTitle=banner.findViewById(R.id.title);
ImageView bannerIcon=banner.findViewById(R.id.icon);
Button bannerButton=banner.findViewById(R.id.button);
banner.findViewById(R.id.button2).setVisibility(View.GONE);
bannerTitle.setText(getString(R.string.yearly_wrap_title, "2023"));
bannerText.setText(R.string.yearly_wrap_text);
bannerButton.setText(R.string.yearly_wrap_view);
bannerIcon.setImageResource(R.drawable.ic_celebration_24px);
banner.setBackgroundColor(bgColor);
bannerText.setTextColor(textColor);
bannerTitle.setTextColor(textColor);
bannerIcon.setBackgroundTintList(ColorStateList.valueOf(iconContainerColor));
bannerIcon.setImageTintList(ColorStateList.valueOf(accentColor));
bannerButton.setTextColor(accentColor);
bannerButton.setBackgroundTintList(ColorStateList.valueOf(accentColor));
bannerButton.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), WrapstodonFragment.class, args);
});
banner.setOutlineProvider(OutlineProviders.roundedRect(12));
banner.setClipToOutline(true);
return banner;
}
private enum ListMode{
FOLLOWING,
LOCAL,

View File

@ -0,0 +1,322 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import android.widget.Toast;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.annual_reports.GetAnnualReports;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AnnualReport;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.wrapstodon.AppsWrapScene;
import org.joinmastodon.android.ui.wrapstodon.ArchetypeWrapScene;
import org.joinmastodon.android.ui.wrapstodon.ByTheNumbersWrapScene;
import org.joinmastodon.android.ui.wrapstodon.FavoriteAccountsWrapScene;
import org.joinmastodon.android.ui.wrapstodon.FavoriteHashtagsWrapScene;
import org.joinmastodon.android.ui.wrapstodon.InteractedAccountsWrapScene;
import org.joinmastodon.android.ui.wrapstodon.SummaryWrapScene;
import org.joinmastodon.android.ui.wrapstodon.TimeSeriesWrapScene;
import org.joinmastodon.android.ui.wrapstodon.WelcomeWrapScene;
import org.joinmastodon.android.ui.wrapstodon.AnnualWrapScene;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.V;
public class WrapstodonFragment extends AppKitFragment{
private boolean statusBarHidden;
private FrameLayout contentWrap;
private ViewPager2 pager;
private ProgressBar progress;
private FrameLayout innerWrap;
private boolean dataLoaded;
private String accountID;
private List<AnnualWrapScene> scenes=List.of();
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
if(BuildConfig.DEBUG && new File(getActivity().getFilesDir(), "mockAnnualReports.json").exists()){
try(FileReader reader=new FileReader(new File(getActivity().getFilesDir(), "mockAnnualReports.json"))){
GetAnnualReports.Response res=MastodonAPIController.gson.fromJson(reader, GetAnnualReports.Response.class);
showReport(res);
}catch(IOException x){
throw new RuntimeException(x);
}
}else{
new GetAnnualReports()
.setCallback(new Callback<>(){
@Override
public void onSuccess(GetAnnualReports.Response result){
showReport(result);
}
@Override
public void onError(ErrorResponse error){
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
Nav.finish(WrapstodonFragment.this);
}
})
.exec(accountID);
}
}
private void showReport(GetAnnualReports.Response result){
if(result.annualReports.isEmpty()){
Nav.finish(WrapstodonFragment.this);
return;
}
dataLoaded=true;
if(progress!=null){
V.setVisibilityAnimated(progress, View.GONE);
V.setVisibilityAnimated(innerWrap, View.VISIBLE);
}
AccountSession session=AccountSessionManager.get(accountID);
String year=String.valueOf(result.annualReports.get(result.annualReports.size()-1).year);
AnnualReport report=result.annualReports.get(result.annualReports.size()-1).data;
Map<String, Account> accounts=result.accounts.stream().collect(Collectors.toMap(a->a.id, Function.identity(), (a1, a2)->a2));
Map<String, Status> statuses=result.statuses.stream().collect(Collectors.toMap(s->s.id, Function.identity(), (s1, s2)->s2));
scenes=List.of(
new WelcomeWrapScene(),
new ArchetypeWrapScene(session.self.username, session.domain, session.self.avatarStatic, report.archetype),
new ByTheNumbersWrapScene(report.typeDistribution),
new FavoriteAccountsWrapScene(accounts, report.mostRebloggedAccounts),
new FavoriteHashtagsWrapScene(report.topHashtags),
new InteractedAccountsWrapScene(session.self, accounts, report.commonlyInteractedWithAccounts),
new AppsWrapScene(report.mostUsedApps),
new TimeSeriesWrapScene(report.timeSeries),
new SummaryWrapScene(session.self, accounts, statuses, report)
);
for(AnnualWrapScene scene:scenes){
scene.setYear(year);
}
if(pager!=null)
pager.getAdapter().notifyDataSetChanged();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
contentWrap=new ContentWrapFrameLayout(getActivity());
contentWrap.setBackgroundColor(0xff000000); // TODO optimize overdraw
progress=new ProgressBar(getActivity());
progress.setIndeterminateTintList(ColorStateList.valueOf(0xffffffff));
contentWrap.addView(progress, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
innerWrap=new InnerWrapFrameLayout(getActivity());
innerWrap.setBackgroundColor(0xFF17063B);
innerWrap.setOutlineProvider(OutlineProviders.roundedRect(8));
innerWrap.setClipToOutline(true);
contentWrap.addView(innerWrap, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
pager=new ViewPager2(getActivity());
pager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
pager.setAdapter(new ScenesAdapter());
pager.setOffscreenPageLimit(1);
innerWrap.addView(pager, new FrameLayout.LayoutParams(V.dp(360), V.dp(640), Gravity.CENTER));
ContextThemeWrapper themeContext=new ContextThemeWrapper(getActivity(), R.style.Theme_Mastodon_Dark);
LayoutInflater.from(themeContext).inflate(R.layout.wrap_top_bar, innerWrap);
innerWrap.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
Button shareButton=innerWrap.findViewById(R.id.btn_share);
shareButton.setOnClickListener(v->shareCurrentScene());
shareButton.setVisibility(View.INVISIBLE);
if(dataLoaded)
progress.setVisibility(View.GONE);
else
innerWrap.setVisibility(View.INVISIBLE);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
private boolean isFirst=true;
@Override
public void onPageSelected(int position){
if(isFirst!=(position==0)){
isFirst=position==0;
V.setVisibilityAnimated(shareButton, isFirst ? View.INVISIBLE : View.VISIBLE);
}
}
});
return contentWrap;
}
@Override
protected void onShown(){
super.onShown();
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
@Override
protected void onHidden(){
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
super.onHidden();
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(insets);
contentWrap.setPadding(0, statusBarHidden ? 0 : insets.getSystemWindowInsetTop(), 0, insets.getSystemWindowInsetBottom());
}
@Override
public boolean wantsLightNavigationBar(){
return false;
}
@Override
public boolean wantsLightStatusBar(){
return false;
}
private void shareCurrentScene(){
Bitmap bmp=scenes.get(pager.getCurrentItem()).renderToBitmap();
try(FileOutputStream out=new FileOutputStream(new File(getActivity().getCacheDir(), "wrapstodon.png"))){
bmp.compress(Bitmap.CompressFormat.PNG, 100, out);
Intent intent=new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://"+getActivity().getPackageName()+".provider.wrapstodon_share/wrapstodon.png"));
intent.setType("image/png");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, getString(R.string.share_toot_title)));
}catch(IOException x){
Toast.makeText(getActivity(), R.string.error_saving_file, Toast.LENGTH_SHORT).show();
}
}
private class ContentWrapFrameLayout extends FrameLayout{
public ContentWrapFrameLayout(@NonNull Context context){
super(context);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh){
super.onSizeChanged(w, h, oldw, oldh);
post(this::updateStatusBarVisibility);
}
private void updateStatusBarVisibility(){
float aspect=(float)getWidth()/(getHeight()-getPaddingBottom());
// Hide the status bar if the screen is 9:16 or wider
if(aspect>=0.5625f){
setSystemUiVisibility(getSystemUiVisibility() | SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
statusBarHidden=true;
}else{
setSystemUiVisibility(getSystemUiVisibility() & ~(SYSTEM_UI_FLAG_FULLSCREEN | SYSTEM_UI_FLAG_IMMERSIVE_STICKY));
statusBarHidden=false;
}
}
}
private class InnerWrapFrameLayout extends FrameLayout{
public InnerWrapFrameLayout(@NonNull Context context){
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int w=MeasureSpec.getSize(widthMeasureSpec);
int h=MeasureSpec.getSize(heightMeasureSpec);
int mw, mh;
if(w>h){
mw=Math.round(h*0.5626f);
mh=h;
}else{
float aspect=(float)w/h;
if(aspect<0.5625f){ // 9:16 or taller
mw=w;
mh=Math.round(w/0.5625f);
}else if(aspect<0.62){ // special case for 9:16 screens with a navigation bar
mw=w;
mh=h;
}else{
mw=Math.round(h*0.5626f);
mh=h;
}
}
float contentScaleFactor=(float)mw/V.dp(360);
View child0=getChildAt(0);
child0.getLayoutParams().height=Math.round(mh/contentScaleFactor);
child0.setScaleX(contentScaleFactor);
child0.setScaleY(contentScaleFactor);
super.onMeasure(mw | MeasureSpec.EXACTLY, mh | MeasureSpec.EXACTLY);
}
}
private class ScenesAdapter extends RecyclerView.Adapter<SceneViewHolder>{
@NonNull
@Override
public SceneViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new SceneViewHolder(scenes.get(viewType));
}
@Override
public void onBindViewHolder(@NonNull SceneViewHolder holder, int position){
}
@Override
public int getItemCount(){
return scenes.size();
}
@Override
public int getItemViewType(int position){
return position;
}
}
private class SceneViewHolder extends RecyclerView.ViewHolder{
public SceneViewHolder(AnnualWrapScene scene){
super(scene.createContentView(getActivity()));
itemView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
}
}

View File

@ -0,0 +1,61 @@
package org.joinmastodon.android.model;
import com.google.gson.annotations.SerializedName;
import java.util.List;
import java.util.Map;
public class AnnualReport extends BaseModel{
public Archetype archetype;
public Map<String, Double> percentiles;
public List<TimeSeriesPoint> timeSeries;
public List<NameAndCount> topHashtags;
public TopStatuses topStatuses;
public List<NameAndCount> mostUsedApps;
public TypeDistribution typeDistribution;
public List<AccountAndCount> mostRebloggedAccounts;
public List<AccountAndCount> commonlyInteractedWithAccounts;
public enum Archetype{
@SerializedName("lurker")
LURKER,
@SerializedName("booster")
BOOSTER,
@SerializedName("replier")
REPLIER,
@SerializedName("pollster")
POLLSTER,
@SerializedName("oracle")
ORACLE
}
public static class TimeSeriesPoint{
public int month;
public int statuses;
public int followers;
public int following;
}
public static class NameAndCount{
public String name;
public int count;
}
public static class TopStatuses{
public String byReblogs;
public String byReplies;
public String byFavourites;
}
public static class TypeDistribution{
public int total;
public int reblogs;
public int replies;
public int standalone;
}
public static class AccountAndCount{
public String accountId;
public int count;
}
}

View File

@ -0,0 +1,80 @@
package org.joinmastodon.android.ui.drawables;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class InnerShadowDrawable extends Drawable{
private int radius, color, offsetX, offsetY, cornerRadius;
private Path path=new Path();
private Paint shadowPaint=new Paint(Paint.ANTI_ALIAS_FLAG), bitmapShadowPaint=new Paint(Paint.ANTI_ALIAS_FLAG), bitmapPaint=new Paint();
private RectF tmpRect=new RectF();
private Bitmap bitmap;
public InnerShadowDrawable(int cornerRadius, int blurRadius, int color, int offsetX, int offsetY){
this.radius=blurRadius;
this.color=color;
this.offsetX=offsetX;
this.offsetY=offsetY;
this.cornerRadius=cornerRadius;
shadowPaint.setColor(color);
shadowPaint.setShadowLayer(blurRadius, offsetX, offsetY, color | 0xFF000000);
bitmapPaint.setColor(color);
bitmapShadowPaint.setColor(0xFF000000);
bitmapShadowPaint.setShadowLayer(blurRadius, offsetX, offsetY, 0xFF000000);
}
@Override
public void draw(@NonNull Canvas canvas){
if(Build.VERSION.SDK_INT>=28 || !canvas.isHardwareAccelerated()){
canvas.drawPath(path, shadowPaint);
}else{
if(bitmap==null){
bitmap=Bitmap.createBitmap(getBounds().width(), getBounds().height(), Bitmap.Config.ALPHA_8);
new Canvas(bitmap).drawPath(path, bitmapShadowPaint);
}
canvas.drawBitmap(bitmap, getBounds().left, getBounds().top, bitmapPaint);
}
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
updatePath();
}
private void updatePath(){
path.rewind();
tmpRect.set(getBounds());
tmpRect.inset(-100, -100);
path.addRect(tmpRect, Path.Direction.CW);
tmpRect.set(getBounds());
path.addRoundRect(tmpRect, cornerRadius, cornerRadius, Path.Direction.CCW);
bitmap=null;
}
}

View File

@ -0,0 +1,37 @@
package org.joinmastodon.android.ui.utils;
import android.annotation.SuppressLint;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
public class NestedScrollingTouchDisallower implements View.OnTouchListener{
private float downY, touchslop;
private boolean didDisallow;
public NestedScrollingTouchDisallower(View v){
touchslop=ViewConfiguration.get(v.getContext()).getScaledTouchSlop();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent ev){
if(ev.getAction()==MotionEvent.ACTION_DOWN){
if(v.canScrollVertically(-1) || v.canScrollVertically(1)){
v.getParent().requestDisallowInterceptTouchEvent(true);
didDisallow=true;
}else{
didDisallow=false;
}
downY=ev.getY();
}else if(didDisallow && ev.getAction()==MotionEvent.ACTION_MOVE){
if(Math.abs(downY-ev.getY())>=touchslop){
if(!v.canScrollVertically((int)(downY-ev.getY()))){
didDisallow=false;
v.getParent().requestDisallowInterceptTouchEvent(false);
}
}
}
return false;
}
}

View File

@ -2,7 +2,6 @@ package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.widget.ScrollView;
@ -43,6 +42,7 @@ public class NestableScrollView extends ScrollView{
didDisallow=true;
}else{
didDisallow=false;
return false;
}
downY=ev.getY();
}else if(didDisallow && ev.getAction()==MotionEvent.ACTION_MOVE){
@ -50,6 +50,7 @@ public class NestableScrollView extends ScrollView{
if(!canScrollVertically((int)(downY-ev.getY()))){
didDisallow=false;
getParent().requestDisallowInterceptTouchEvent(false);
return false;
}
}
}

View File

@ -0,0 +1,73 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.view.View;
/**
* Note: all scenes are rendered in a fixed-size viewport of 360*640dp, then scaled as necessary to fit the screen.
* Works well on common phone screen sizes, might be kinda meh on tablets.
*/
public abstract class AnnualWrapScene{
protected View contentView;
protected String year;
protected abstract View onCreateContentView(Context context);
protected abstract void onDestroyContentView();
public View createContentView(Context context){
if(contentView!=null)
return contentView;
return contentView=onCreateContentView(context);
}
public void destroyContentView(){
onDestroyContentView();
contentView=null;
}
protected View getViewForScreenshot(){
return contentView;
}
public Bitmap renderToBitmap(){
if(contentView==null)
throw new IllegalStateException();
View v=getViewForScreenshot();
Bitmap bmp=Bitmap.createBitmap(1080, Math.round(1080/(float)v.getWidth()*v.getHeight()), Bitmap.Config.ARGB_8888);
Canvas c=new Canvas(bmp);
c.drawColor(0xFF17063B);
float scale=3f/v.getResources().getDisplayMetrics().density;
c.scale(scale, scale, 0, 0);
v.draw(c);
return bmp;
}
public void setYear(String year){
this.year=year;
}
protected CharSequence replaceBoldWithColor(CharSequence src, int color){
Spannable ssb;
if(src instanceof Spannable s){
ssb=s;
}else{
ssb=new SpannableString(src);
}
StyleSpan[] spans=ssb.getSpans(0, ssb.length(), StyleSpan.class);
for(StyleSpan span:spans){
int start=ssb.getSpanStart(span);
int end=ssb.getSpanEnd(span);
ssb.removeSpan(span);
ssb.setSpan(new ForegroundColorSpan(color), start, end, 0);
}
return ssb;
}
}

View File

@ -0,0 +1,143 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.AnnualReport;
import org.joinmastodon.android.ui.drawables.TiledDrawable;
import java.util.List;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class AppsWrapScene extends AnnualWrapScene{
private final List<AnnualReport.NameAndCount> appStats;
public AppsWrapScene(List<AnnualReport.NameAndCount> appStats){
this.appStats=appStats;
}
@Override
protected View onCreateContentView(Context context){
LayoutInflater inflater=LayoutInflater.from(context);
View content=inflater.inflate(R.layout.wrap_apps, null);
content.setBackground(new TiledDrawable(context.getResources().getDrawable(R.drawable.chiclet_pattern, context.getTheme())));
View[] appBars={
content.findViewById(R.id.app1_bar),
content.findViewById(R.id.app2_bar),
content.findViewById(R.id.app3_bar)
};
ImageView[] appIcons={
content.findViewById(R.id.app1_icon),
content.findViewById(R.id.app2_icon),
content.findViewById(R.id.app3_icon)
};
TextView[] appNames={
content.findViewById(R.id.app1_name),
content.findViewById(R.id.app2_name),
content.findViewById(R.id.app3_name)
};
int max=appStats.get(0).count;
int i=0;
for(AnnualReport.NameAndCount app:appStats){
appNames[i].setText(app.name);
appBars[i].setTranslationY(V.dp(250)*(1-(app.count/(float)max)));
if("Mastodon for Android".equals(app.name) || "Mastodon for iOS".equals(app.name)){
appIcons[i].setImageResource(R.mipmap.ic_launcher);
}else{
String url=getIconUrl(app.name);
if(url==null)
url=getIconUrl(app.name.split(" ")[0]);
if(url!=null){
ViewImageLoader.loadWithoutAnimation(appIcons[i], null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(96), V.dp(96), List.of(), Uri.parse(url)));
}
}
i++;
if(i==3)
break;
}
if(i<3){
for(int j=i;j<3;j++)
appBars[j].setVisibility(View.INVISIBLE);
}
return content;
}
@Override
protected void onDestroyContentView(){
}
static String getIconUrl(String name){
return switch(name){
case "Web" -> "https://upload.wikimedia.org/wikipedia/commons/thumb/0/08/Netscape_icon.svg/128px-Netscape_icon.svg.png";
case "Rodent" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Frodent.d6a6e73b.png&w=1080&q=75";
case "Focus" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ffocus.5d3f5755.png&w=1080&q=75";
case "Tusky" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftusky.28fb3514.png&w=384&q=75";
case "Subway Tooter" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fsubway-tooter.165ce486.png&w=384&q=75";
case "Fedilab" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ffedilab.8fce088e.png&w=384&q=75";
case "Megalodon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmegalodon.d1fd421a.png&w=384&q=75";
case "Moshidon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmoshidon.d0d53493.png&w=384&q=75";
case "ZonePane" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fzonepane.add34ea8.png&w=1080&q=75";
case "Pachli" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fpachli.0a3fda9d.png&w=1080&q=75";
case "Toot!" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftoot.9fce2178.jpg&w=640&q=75";
case "Mast" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmast.17193b21.png&w=640&q=75";
case "iMast" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fimast_icon.9713d97a.png&w=1080&q=75";
case "Ice Cubes" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ficecubes.141ad567.png&w=828&q=75";
case "Ivory" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fivory.3878bb47.png&w=1080&q=75";
case "Mammoth" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmammoth.19d0726e.png&w=828&q=75";
case "Woolly" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fwoolly.270ab54a.png&w=1080&q=75";
case "DAWN for Mastodon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdawn.5a728ea4.png&w=1080&q=75";
case "Mona" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmona.962904ab.png&w=1080&q=75";
case "Radiant" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fradiant.88eaaf27.png&w=1080&q=75";
case "TootDesk" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftootdesk.089737e3.png&w=1080&q=75";
case "Stomp (watchOS)" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fstomp.ac7757e7.png&w=2048&q=75";
case "feather" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ffeather.448a23c4.png&w=2048&q=75";
case "SoraSNS" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fsora.aa83aab1.png&w=1200&q=75";
case "Pipilo" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fpipilo.9d0314d1.png&w=1080&q=75";
case "Pinafore" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fpinafore.ba6b1933.png&w=256&q=75";
case "Elk" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Felk.db28f01a.png&w=384&q=75";
case "Buffer" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fbuffer.1b722f8e.png&w=1080&q=75";
case "Statuzer" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fstatuzer.04c7fa8a.png&w=384&q=75";
case "Fedica" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ffedica.a2b32162.png&w=2048&q=75";
case "Phanpy" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fphanpy.97a4a3c1.png&w=384&q=75";
case "Trunks" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftrunks.1e0e665e.png&w=1080&q=75";
case "Litterbox" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Flitterbox.f4015748.png&w=640&q=75";
case "Tooty" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftooty.f8664e1a.png&w=640&q=75";
case "Mastodeck" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmastodeck.985ab21b.png&w=640&q=75";
case "Tokodon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftokodon.ba8f924d.png&w=640&q=75";
case "Whalebird" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fwhalebird.cd50e388.png&w=256&q=75";
case "TheDesk" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fthedesk.1cd41d27.png&w=1080&q=75";
case "Hyper­space" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fhyperspace.668fa418.png&w=256&q=75";
case "Mastonaut" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmastonaut.c026dca5.png&w=640&q=75";
case "Sengi" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fsengi.c16fc152.png&w=828&q=75";
case "Bitlbee-Mastodon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fbitlbee.35ddf1a4.png&w=384&q=75";
case "Tuba" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftuba.adee2feb.png&w=640&q=75";
case "TootRain" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftootrain.34eff04b.png&w=640&q=75";
case "Fedistar" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ffedistar.a2b814f8.png&w=256&q=75";
case "Tooter" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ftooter.1e9ff8f1.png&w=256&q=75";
case "Amidon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Famidon.3578681d.png&w=256&q=75";
case "BREXXTODON" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fbrexxtodon.c6174a1a.png&w=256&q=75";
case "DOStodon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdostodon.cc63613e.png&w=256&q=75";
case "Macstodon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmacstodon.a47d72c0.png&w=256&q=75";
case "Masto9" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmastonine.2f86e9a0.png&w=256&q=75";
case "Mastodon for Apple II" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmastodonforappleii.6b9ba8ef.png&w=256&q=75";
case "Mastodon 3.11 for Workgroups" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmastodonforworkgroups.c6524eb1.png&w=256&q=75";
case "Heffalump" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fheffalump.b421f5e9.png&w=256&q=75";
case "MOStodon" -> "https://joinmastodon.org/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fmostodon.bb1cb01c.png&w=3840&q=75";
default -> null;
};
}
}

View File

@ -0,0 +1,187 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Property;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.AnnualReport;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.InnerShadowDrawable;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class ArchetypeWrapScene extends AnnualWrapScene{
private String username, domain, avaURL;
private AnnualReport.Archetype archetype;
private Paint photoEffectPaint=new Paint();
private Property<View, Float> photoDevelopEffectProp=new Property<>(Float.class, "fdsafdsa"){
@Override
public Float get(View object){
return null;
}
@Override
public void set(View object, Float value){
int colorMod=-Math.round((1f-value)*255);
photoEffectPaint.setColorFilter(new ColorMatrixColorFilter(new float[]{
1, 0, 0, 0, colorMod,
0, 1, 0, 0, colorMod,
0, 0, 1, 0, colorMod,
0, 0, 0, value, 0
}));
object.setLayerType(View.LAYER_TYPE_HARDWARE, photoEffectPaint);
}
};
public ArchetypeWrapScene(String username, String domain, String avaURL, AnnualReport.Archetype archetype){
this.username=username;
this.domain=domain;
this.archetype=archetype;
this.avaURL=avaURL;
photoEffectPaint.setAlpha(255);
}
@SuppressLint("SetTextI18n")
@Override
protected View onCreateContentView(Context context){
View view=context.getSystemService(LayoutInflater.class).inflate(R.layout.wrap_archetype, null);
TextView subtitle=view.findViewById(R.id.subtitle);
TextView username=view.findViewById(R.id.username);
TextView domain=view.findViewById(R.id.domain);
TextView archetypeTitle=view.findViewById(R.id.archetype_text);
TextView archetypeExplanation=view.findViewById(R.id.archetype_explanation);
View frame=view.findViewById(R.id.picture_frame);
ImageView photo=view.findViewById(R.id.photo);
RoundedFrameLayout photoWrap=view.findViewById(R.id.photo_wrap);
subtitle.setText(context.getString(R.string.yearly_wrap_archetype_subtitle, year));
username.setText("@"+this.username);
domain.setText("@"+this.domain);
archetypeTitle.setText(switch(archetype){
case LURKER -> R.string.yearly_wrap_archetype_lurker;
case BOOSTER -> R.string.yearly_wrap_archetype_booster;
case REPLIER -> R.string.yearly_wrap_archetype_replier;
case POLLSTER -> R.string.yearly_wrap_archetype_pollster;
case ORACLE -> R.string.yearly_wrap_archetype_oracle;
});
archetypeExplanation.setText("TBD");
frame.setOutlineProvider(OutlineProviders.roundedRect(4));
frame.setClipToOutline(true);
frame.setBackground(new RoundRectClippingDrawable(frame.getBackground()));
photoWrap.setCornerRadius(V.dp(2));
photoWrap.setForeground(new InnerShadowDrawable(V.dp(2), V.dp(1), 0x40000000, 0, V.dp(1)));
photoWrap.setBackgroundColor(0xFF000000);
if(avaURL!=null){
ViewImageLoader.loadWithoutAnimation(photo, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, 0, 0, List.of(), Uri.parse(avaURL)));
}
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(frame, View.ROTATION, -5, 2),
ObjectAnimator.ofFloat(frame, View.SCALE_X, 1, 0.9f),
ObjectAnimator.ofFloat(frame, View.SCALE_Y, 1, 0.9f),
ObjectAnimator.ofFloat(photo, photoDevelopEffectProp, 0, 1)
);
set.setDuration(5000);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.start();
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
photo.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
return view;
}
@Override
protected void onDestroyContentView(){
}
private static class RoundRectClippingDrawable extends Drawable{
private final Drawable inner;
private Path path=new Path();
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private RectF tmpRect=new RectF();
private RoundRectClippingDrawable(Drawable inner){
this.inner=inner;
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
paint.setColor(0xff000000);
}
@Override
public void draw(@NonNull Canvas canvas){
if(canvas.isHardwareAccelerated()){
inner.draw(canvas);
return;
}
tmpRect.set(getBounds());
canvas.saveLayer(tmpRect, null);
inner.draw(canvas);
canvas.drawPath(path, paint);
canvas.restore();
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
inner.setBounds(bounds);
path.rewind();
path.addRoundRect(bounds.left, bounds.top, bounds.right, bounds.bottom, V.dp(4), V.dp(4), Path.Direction.CW);
if(!path.isInverseFillType())
path.toggleInverseFillType();
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
}
}

View File

@ -0,0 +1,208 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.AnnualReport;
import org.joinmastodon.android.ui.utils.NestedScrollingTouchDisallower;
import java.text.NumberFormat;
import java.util.function.Consumer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.utils.V;
public class ByTheNumbersWrapScene extends AnnualWrapScene{
private static final int GRID_W=15, GRID_H=27;
private final AnnualReport.TypeDistribution typeDistribution;
private int boostsCircleCount, repliesCircleCount, standaloneCircleCount;
private int revealedCircleCount;
private Drawable boostIcon, replyIcon, postIcon;
private RectF tmpRect=new RectF();
public ByTheNumbersWrapScene(AnnualReport.TypeDistribution typeDistribution){
this.typeDistribution=typeDistribution;
int totalCircles=GRID_W*GRID_H;
boostsCircleCount=Math.round(totalCircles*(typeDistribution.reblogs/(float)typeDistribution.total));
repliesCircleCount=Math.round(totalCircles*(typeDistribution.replies/(float)typeDistribution.total));
standaloneCircleCount=totalCircles-boostsCircleCount-repliesCircleCount;
}
@Override
protected View onCreateContentView(Context context){
boostIcon=prepareIcon(context, R.drawable.ic_repeat_wght700grad200fill1_20px);
replyIcon=prepareIcon(context, R.drawable.ic_reply_wght700_20px);
postIcon=prepareIcon(context, R.drawable.ic_chat_bubble_wght700_20px);
ViewPager2 pager=new ViewPager2(context);
pager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
pager.setAdapter(new PagerAdapter());
pager.getChildAt(0).setOnTouchListener(new NestedScrollingTouchDisallower(pager));
BackgroundDrawable bg=new BackgroundDrawable();
pager.setBackground(bg);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
if(position==0){
revealedCircleCount=Math.round(boostsCircleCount*positionOffset);
}else if(position==1){
revealedCircleCount=boostsCircleCount+Math.round(repliesCircleCount*positionOffset);
}else if(position==2){
revealedCircleCount=boostsCircleCount+repliesCircleCount+Math.round(standaloneCircleCount*positionOffset);
}else{
revealedCircleCount=GRID_W*GRID_H;
}
pager.invalidate();
}
});
return pager;
}
@Override
protected void onDestroyContentView(){
}
private Drawable prepareIcon(Context context, int res){
Drawable d=context.getResources().getDrawable(res, context.getTheme()).mutate();
d.setTint(0xFF17063B);
d.setBounds(V.dp(2), V.dp(2), V.dp(14), V.dp(14));
return d;
}
private class PagerAdapter extends RecyclerView.Adapter<PageViewHolder>{
@NonNull
@Override
public PageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new PageViewHolder(parent);
}
@Override
public void onBindViewHolder(@NonNull PageViewHolder holder, int position){
Resources r=holder.itemView.getResources();
holder.topText.setText(switch(position){
case 0 -> r.getString(R.string.wrap_numbers_total_title);
case 1 -> r.getString(R.string.wrap_numbers_boosts_title, typeDistribution.reblogs);
case 2 -> r.getString(R.string.wrap_numbers_replies_title, typeDistribution.replies);
case 3 -> r.getString(R.string.wrap_numbers_standalone_posts_title, typeDistribution.standalone);
default -> throw new IllegalStateException("Unexpected value: "+position);
});
holder.number.setText(switch(position){
case 0 -> NumberFormat.getInstance().format(typeDistribution.total);
case 1 -> Math.round(typeDistribution.reblogs/(float)typeDistribution.total*100f)+"%";
case 2 -> Math.round(typeDistribution.replies/(float)typeDistribution.total*100f)+"%";
case 3 -> Math.round(typeDistribution.standalone/(float)typeDistribution.total*100f)+"%";
default -> throw new IllegalStateException("Unexpected value: "+position);
});
holder.bottomText.setText(switch(position){
case 0 -> r.getString(R.string.wrap_numbers_x_times_in_year, year);
case 1 -> r.getString(R.string.wrap_numbers_boosts);
case 2 -> r.getString(R.string.wrap_numbers_replies);
case 3 -> r.getString(R.string.wrap_numbers_standalone_posts);
default -> throw new IllegalStateException("Unexpected value: "+position);
});
holder.itemView.setBackgroundTintList(ColorStateList.valueOf(switch(position){
case 0, 1 -> 0xFFFFBE2E;
case 2 -> 0xFF858AFA;
case 3 -> 0xFFBAFF3B;
default -> throw new IllegalStateException("Unexpected value: "+position);
}));
}
@Override
public int getItemCount(){
return 4;
}
}
private static class PageViewHolder extends RecyclerView.ViewHolder{
private final TextView topText, number, bottomText;
public PageViewHolder(@NonNull ViewGroup parent){
super(LayoutInflater.from(parent.getContext()).inflate(R.layout.wrap_numbers_page, parent, false));
topText=itemView.findViewById(R.id.top_text);
number=itemView.findViewById(R.id.number);
bottomText=itemView.findViewById(R.id.bottom_text);
}
}
private class BackgroundDrawable extends Drawable{
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
@Override
public void draw(@NonNull Canvas canvas){
Rect bounds=getBounds();
float spacing=(bounds.width()-V.dp(16+16*GRID_W))/(float)(GRID_W-1);
int i=0;
float radius=V.dp(8);
canvas.save();
canvas.translate(radius, radius);
for(int y=0;y<GRID_H;y++){
canvas.save();
for(int x=0;x<GRID_W;x++){
int color;
Drawable icon;
if(i<revealedCircleCount){
if(i<boostsCircleCount){
color=0xFFFFBE2E;
icon=boostIcon;
}else if(i<boostsCircleCount+repliesCircleCount){
color=0xFF858AFA;
icon=replyIcon;
}else{
color=0xFFBAFF3B;
icon=postIcon;
}
}else{
color=0xFF2F0C7A;
icon=null;
}
paint.setColor(color);
canvas.drawCircle(radius, radius, radius, paint);
if(icon!=null){
icon.draw(canvas);
}
i++;
canvas.translate(radius*2+spacing, 0);
}
canvas.restore();
canvas.translate(0, radius*2+spacing);
}
canvas.restore();
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
}
}

View File

@ -0,0 +1,128 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.SuperscriptSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AnnualReport;
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.NestableScrollView;
import java.util.List;
import java.util.Map;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class FavoriteAccountsWrapScene extends AnnualWrapScene{
private final Map<String, Account> accounts;
private final List<AnnualReport.AccountAndCount> topAccounts;
private LinearLayout scrollContent;
public FavoriteAccountsWrapScene(Map<String, Account> accounts, List<AnnualReport.AccountAndCount> topAccounts){
this.accounts=accounts;
this.topAccounts=topAccounts;
}
@Override
protected View onCreateContentView(Context context){
NestableScrollView scroll=new NestableScrollView(context);
scroll.setNestedScrollingEnabled(true);
LayoutInflater inflater=LayoutInflater.from(context);
LinearLayout ll=new LinearLayout(context);
ll.setOrientation(LinearLayout.VERTICAL);
scrollContent=ll;
scroll.addView(ll);
View header=inflater.inflate(R.layout.wrap_faves_header, ll, false);
TextView title=header.findViewById(R.id.title);
TextView subtitle=header.findViewById(R.id.subtitle);
title.setText(replaceBoldWithColor(context.getResources().getText(R.string.wrap_most_reblogged_title), 0xFFBAFF3B));
SpannableStringBuilder subtitleStr=new SpannableStringBuilder(context.getResources().getText(R.string.wrap_most_reblogged_subtitle));
int index=subtitleStr.toString().indexOf("%s");
if(index!=-1){
subtitleStr.replace(index, index+2, year);
}
subtitle.setText(replaceBoldWithColor(subtitleStr, 0xFFFFBE2E));
ll.addView(header);
for(int i=0;i<Math.min(topAccounts.size(), 4);i++){
int boostCount=topAccounts.get(i).count;
Account account=accounts.get(topAccounts.get(i).accountId);
if(account==null)
continue;
LinearLayout row=(LinearLayout) inflater.inflate(R.layout.wrap_faves_account_row, ll, false);
row.setDividerDrawable(new EmptyDrawable(V.dp(16), 1));
row.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
if(i%2==1){
View firstChild=row.getChildAt(0);
row.removeView(firstChild);
row.addView(firstChild);
}
ll.addView(row);
TextView rank=row.findViewById(R.id.rank);
TextView count=row.findViewById(R.id.count);
ImageView cover=row.findViewById(R.id.cover);
ImageView ava=row.findViewById(R.id.avatar);
TextView name=row.findViewById(R.id.name);
TextView username=row.findViewById(R.id.username);
TextView postsCount=row.findViewById(R.id.posts_value);
TextView postsLabel=row.findViewById(R.id.posts_label);
TextView followersCount=row.findViewById(R.id.followers_value);
TextView followersLabel=row.findViewById(R.id.followers_label);
TextView followingCount=row.findViewById(R.id.following_value);
TextView followingLabel=row.findViewById(R.id.following_label);
SpannableString rankStr=new SpannableString("#"+(i+1));
rankStr.setSpan(new SuperscriptSpan(), 0, 1, 0);
rankStr.setSpan(new RelativeSizeSpan(0.6f), 0, 1, 0);
rank.setText(rankStr);
count.setText(context.getResources().getQuantityString(R.plurals.x_reblogs, boostCount, boostCount));
CharSequence nameStr=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
name.setText(nameStr);
UiUtils.loadCustomEmojiInTextView(name);
username.setText(account.getDisplayUsername());
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
postsLabel.setText(context.getResources().getQuantityString(R.plurals.posts, account.statusesCount>1000 ? 1000 : (int)account.statusesCount));
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
followersLabel.setText(context.getResources().getQuantityString(R.plurals.followers, account.followersCount>1000 ? 1000 : (int)account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
followingLabel.setText(context.getResources().getQuantityString(R.plurals.following, account.followingCount>1000 ? 1000 : (int)account.followingCount));
ViewImageLoader.loadWithoutAnimation(cover, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(300), 0, List.of(), Uri.parse(account.headerStatic)));
ViewImageLoader.loadWithoutAnimation(ava, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(56), V.dp(56), List.of(), Uri.parse(account.avatarStatic)));
}
return scroll;
}
@Override
protected void onDestroyContentView(){
}
@Override
protected View getViewForScreenshot(){
return scrollContent;
}
}

View File

@ -0,0 +1,236 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.AnnualReport;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import me.grishka.appkit.utils.V;
public class FavoriteHashtagsWrapScene extends AnnualWrapScene{
private final List<AnnualReport.NameAndCount> tags;
private int[] buffer;
private Paint paint=new Paint();
private ByteBuffer pixelBuffer;
public FavoriteHashtagsWrapScene(List<AnnualReport.NameAndCount> tags){
this.tags=tags.stream().sorted(Comparator.comparingInt((AnnualReport.NameAndCount t)->t.count).reversed()).collect(Collectors.toList());
}
@Override
protected View onCreateContentView(Context context){
LinearLayout ll=new LinearLayout(context);
ll.setOrientation(LinearLayout.VERTICAL);
LayoutInflater inflater=LayoutInflater.from(context);
View header=inflater.inflate(R.layout.wrap_faves_header, ll, false);
TextView title=header.findViewById(R.id.title);
TextView subtitle=header.findViewById(R.id.subtitle);
title.setText(replaceBoldWithColor(context.getResources().getText(R.string.wrap_favorite_tags_title), 0xFFBAFF3B));
subtitle.setText(R.string.wrap_favorite_tags_subtitle);
ll.addView(header);
FrameLayout tagsCanvas=new FrameLayout(context);
ll.addView(tagsCanvas, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(360)));
Typeface font;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
font=context.getResources().getFont(R.font.manrope_w400);
}else{
font=Typeface.DEFAULT;
}
int max=tags.get(0).count;
int min=tags.get(tags.size()-1).count;
buffer=new int[12*360];
paint.setTypeface(font);
paint.setColor(0xFF000000);
paint.setShadowLayer(1, 0, 0, 0xFF000000);
pixelBuffer=ByteBuffer.allocate(120*384);
int[] spriteSize={0, 0};
int[] xy={0, 0};
Random rand=new Random();
// int i=0;
for(AnnualReport.NameAndCount tag:tags){
// if(i==20) break;
int size;
if(max==min){
size=80;
}else{
float fraction=(tag.count-min)/(float)(max-min);
size=UiUtils.lerp(15, 80, fraction*fraction*fraction/*fraction*/);
paint.setShadowLayer(fraction, 0, 0, 0xFF000000);
}
paint.setTextSize(size);
while(paint.measureText(tag.name)>360){
size/=2;
paint.setTextSize(size);
}
int[] sprite=getWordSprite(tag.name, size, spriteSize);
// int startX=360/2-spriteSize[0]/2;
// int startY=360/2-spriteSize[1]/2;
int startX=rand.nextInt(360-spriteSize[0]);
int startY=rand.nextInt(360-spriteSize[1]);
double dt=rand.nextBoolean() ? 1 : -1;
double t=-dt;
int maxDelta=509;
int dx, dy;
boolean placed=false;
int x, y;
do{
spiral(t+=dt, xy);
dx=xy[0];
dy=xy[1];
x=startX+dx;
y=startY+dy;
if(x<0 || y<0 || x+spriteSize[0]>360 || y+spriteSize[1]>360)
continue;
if(!collide(sprite, x, y, spriteSize[0], spriteSize[1])){
addMask(sprite, x, y, spriteSize[0], spriteSize[1]);
placed=true;
break;
}
}while(Math.max(Math.abs(dx), Math.abs(dy))<maxDelta);
// i++;
if(!placed)
break;
TextView text=new TextView(context);
text.setTypeface(font);
text.setTextSize(TypedValue.COMPLEX_UNIT_DIP, size);
text.setTextColor(0xFFCCCFFF);
text.setSingleLine();
text.setText(tag.name);
text.setTranslationX(V.dp(x));
text.setTranslationY(V.dp(y));
tagsCanvas.addView(text, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
}
if(BuildConfig.DEBUG){
tagsCanvas.setOnClickListener(v->{
FixedAspectRatioImageView vv=new FixedAspectRatioImageView(context);
vv.setAspectRatio(1);
vv.setBackground(new BitmapDrawable(getDebugBitmap()));
new AlertDialog.Builder(context)
.setView(vv)
.show();
});
}
return ll;
}
@Override
protected void onDestroyContentView(){
}
private int[] getWordSprite(String text, int size, int[] outSize){
// Makes a binary image of the string
paint.setTextSize(size);
int w=Math.min(384, (int)paint.measureText(text)+2);
if(w%32!=0){
w+=32-w%32;
}
int h=(int)(paint.descent()-paint.ascent());
Bitmap bmp=Bitmap.createBitmap(w, h, Bitmap.Config.ALPHA_8);
new Canvas(bmp).drawText(text, 1, -paint.ascent(), paint);
bmp.copyPixelsToBuffer(pixelBuffer);
pixelBuffer.rewind();
outSize[0]=w;
outSize[1]=h;
int spriteW=w >> 5;
int[] sprite=new int[spriteW*h];
byte[] pixelData=pixelBuffer.array();
int offsetIntoPixelData=0;
int stride=bmp.getRowBytes();
for(int y=0;y<h;y++){
for(int x=0;x<spriteW;x++){
int slice=0;
for(int i=0;i<32;i++){
slice<<=1;
if(pixelData[offsetIntoPixelData+(x << 5)+i]!=0)
slice|=1;
}
sprite[y*spriteW+x]=slice;
}
offsetIntoPixelData+=stride;
}
return sprite;
}
private void spiral(double t, int[] outXY){
t*=3;
outXY[0]=(int)Math.round(t*Math.cos(t));
outXY[1]=(int)Math.round(t*Math.sin(t));
}
private boolean collide(int[] sprite, int x, int y, int w, int h){
int packedWidth=w>>5, packedX=x>>5;
int shiftX=x & 0x7f, invShiftX=32-shiftX;
int last;
for(int iy=0;iy<h;iy++){
last=0;
for(int ix=0;ix<=packedWidth;ix++){
if((((last << invShiftX) | (ix<packedWidth ? (last=sprite[iy*packedWidth+ix]) >>> shiftX : 0)) & buffer[(iy+y)*12+ix+packedX])!=0)
return true;
}
}
return false;
}
private void addMask(int[] sprite, int x, int y, int w, int h){
int packedWidth=w>>5, packedX=x>>5;
int shiftX=x & 0x7f, invShiftX=32-shiftX;
int last;
for(int iy=0;iy<h;iy++){
last=0;
for(int ix=0;ix<=packedWidth;ix++){
buffer[(iy+y)*12+ix+packedX]|=(last << invShiftX) | (ix<packedWidth ? (last=sprite[iy*packedWidth+ix]) >>> shiftX : 0);
}
}
}
private Bitmap getDebugBitmap(){
Bitmap bmp=Bitmap.createBitmap(384, 360, Bitmap.Config.ALPHA_8);
for(int y=0;y<360;y++){
for(int x=0;x<12;x++){
int packed=buffer[y*12+x];
for(int i=0;i<32;i++){
if((packed & 0x80000000)!=0){
bmp.setPixel(x*32+i, y, 0xFF000000);
}
packed<<=1;
}
}
}
return bmp;
}
}

View File

@ -0,0 +1,324 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.DashPathEffect;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AnnualReport;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
public class InteractedAccountsWrapScene extends AnnualWrapScene{
private final Account self;
private final Map<String, Account> allAccounts;
private final List<AnnualReport.AccountAndCount> accounts;
private List<ImageView> avatarViews=new ArrayList<>();
private ImageView avatarToFollowAround;
private final Runnable followAroundAnimationUpdater=this::updateFollowAroundAnimation;
private View content;
private FrameLayout contentWrap;
private FrameLayout orbit;
private ImageView selfAva;
private View orbitBGView;
private TextView title, subtitle;
private View detailsOverlay;
private TextView detailsNumber, detailsText;
private Animator currentZoomAnim;
public InteractedAccountsWrapScene(Account self, Map<String, Account> allAccounts, List<AnnualReport.AccountAndCount> accounts){
this.self=self;
this.allAccounts=allAccounts;
this.accounts=accounts;
}
@Override
protected View onCreateContentView(Context context){
LayoutInflater inflater=LayoutInflater.from(context);
contentWrap=(FrameLayout) inflater.inflate(R.layout.wrap_interacted_accounts, null);
content=contentWrap.findViewById(R.id.content);
selfAva=content.findViewById(R.id.self_ava);
ViewImageLoader.loadWithoutAnimation(selfAva, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(160), V.dp(160), List.of(), Uri.parse(self.avatarStatic)));
title=content.findViewById(R.id.title);
subtitle=content.findViewById(R.id.subtitle);
detailsOverlay=contentWrap.findViewById(R.id.details_overlay);
detailsNumber=contentWrap.findViewById(R.id.details_number);
detailsText=contentWrap.findViewById(R.id.details_text);
orbit=content.findViewById(R.id.orbit);
orbitBGView=content.findViewById(R.id.orbit_bg);
LayerDrawable orbitBG=new LayerDrawable(new Drawable[]{
new OrbitBackgroundDrawable(),
new OrbitBackgroundDrawable()
});
orbitBG.setLayerSize(0, V.dp(409+4), V.dp(409+4));
orbitBG.setLayerSize(1, V.dp(256+4), V.dp(256+4));
orbitBG.setLayerGravity(0, Gravity.CENTER);
orbitBG.setLayerGravity(1, Gravity.CENTER);
orbitBGView.setBackground(orbitBG);
FrameLayout innerRing=content.findViewById(R.id.inner_ring);
FrameLayout outerRing=content.findViewById(R.id.outer_ring);
List<AnnualReport.AccountAndCount> forInnerRing=accounts.subList(0, Math.min(6, accounts.size()));
List<AnnualReport.AccountAndCount> forOuterRing=accounts.size()>6 ? accounts.subList(6, Math.min(18, accounts.size())) : List.of();
if(forInnerRing.isEmpty())
return content;
final long innerDuration=15000, outerDuration=20000;
ArrayList<Animator> anims=new ArrayList<>();
anims.add(repeat(ObjectAnimator.ofFloat(innerRing, View.ROTATION, 0, 360).setDuration(innerDuration)));
anims.add(repeat(ObjectAnimator.ofFloat(outerRing, View.ROTATION, 0, 360).setDuration(outerDuration)));
double angle=0;
double deltaAngle=2*Math.PI/forInnerRing.size();
for(AnnualReport.AccountAndCount acc:forInnerRing){
Account account=allAccounts.get(acc.accountId);
if(account==null)
continue;
ImageView iv=makeImageView(context, account);
innerRing.addView(iv, new FrameLayout.LayoutParams(V.dp(40), V.dp(40), Gravity.CENTER));
float r=V.dp(128);
iv.setTranslationX(r*(float)Math.cos(angle));
iv.setTranslationY(r*(float)Math.sin(angle));
iv.setOnClickListener(this::onAvatarClick);
iv.setTag(acc);
anims.add(repeat(ObjectAnimator.ofFloat(iv, View.ROTATION, 0, -360).setDuration(innerDuration)));
avatarViews.add(iv);
angle+=deltaAngle;
}
if(!forOuterRing.isEmpty()){
angle=0;
deltaAngle=2*Math.PI/forOuterRing.size();
for(AnnualReport.AccountAndCount acc:forOuterRing){
Account account=allAccounts.get(acc.accountId);
if(account==null)
continue;
ImageView iv=makeImageView(context, account);
outerRing.addView(iv, new FrameLayout.LayoutParams(V.dp(40), V.dp(40), Gravity.CENTER));
float r=V.dp(204.5f);
iv.setTranslationX(r*(float)Math.cos(angle));
iv.setTranslationY(r*(float)Math.sin(angle));
iv.setOnClickListener(this::onAvatarClick);
iv.setTag(acc);
anims.add(repeat(ObjectAnimator.ofFloat(iv, View.ROTATION, 0, -360).setDuration(outerDuration)));
avatarViews.add(iv);
angle+=deltaAngle;
}
}
detailsOverlay.setVisibility(View.GONE);
AnimatorSet set=new AnimatorSet();
set.playTogether(anims);
set.setInterpolator(new LinearInterpolator());
set.start();
return contentWrap;
}
// Because Android's animation API is stupid
private ObjectAnimator repeat(ObjectAnimator anim){
anim.setRepeatCount(ObjectAnimator.INFINITE);
return anim;
}
private ImageView makeImageView(Context context, Account account){
RoundedImageView iv=new RoundedImageView(context);
iv.setCornerRadius(V.dp(20));
iv.setForeground(iv.getResources().getDrawable(R.drawable.wrap_ava_border_1dp, iv.getContext().getTheme()));
ViewImageLoader.loadWithoutAnimation(iv, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(40), V.dp(40), List.of(), Uri.parse(account.avatarStatic)));
return iv;
}
@Override
protected void onDestroyContentView(){
}
private void onAvatarClick(View v){
if(currentZoomAnim!=null)
currentZoomAnim.cancel();
ImageView iv=(ImageView) v;
AnnualReport.AccountAndCount acc=(AnnualReport.AccountAndCount) v.getTag();
((ViewGroup)content).setClipChildren(false);
orbit.setClipChildren(false);
contentWrap.setClipChildren(false);
avatarToFollowAround=iv;
content.setPivotX(0);
content.setPivotY(0);
detailsNumber.setText(NumberFormat.getInstance().format(acc.count));
detailsOverlay.setVisibility(View.VISIBLE);
detailsOverlay.setAlpha(0);
SpannableStringBuilder ssb=new SpannableStringBuilder(v.getResources().getQuantityString(R.plurals.wrap_replies_exchanged, acc.count));
int index=ssb.toString().indexOf("%s");
if(index!=-1){
String name=Objects.requireNonNull(allAccounts.get(acc.accountId)).displayName;
ssb.replace(index, index+2, name);
ssb.setSpan(new ForegroundColorSpan(0xFFBAFF3B), index, index+name.length(), 0);
}
detailsText.setText(ssb);
AnimatorSet set=new AnimatorSet();
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(content, View.SCALE_X, 3.2f));
anims.add(ObjectAnimator.ofFloat(content, View.SCALE_Y, 3.2f));
anims.add(ObjectAnimator.ofFloat(selfAva, View.ALPHA, .15f));
anims.add(ObjectAnimator.ofFloat(orbitBGView, View.ALPHA, .15f));
anims.add(ObjectAnimator.ofFloat(title, View.ALPHA, .15f));
anims.add(ObjectAnimator.ofFloat(subtitle, View.ALPHA, .15f));
for(ImageView ava:avatarViews){
if(ava!=iv){
anims.add(ObjectAnimator.ofFloat(ava, View.ALPHA, .15f));
}
ava.setEnabled(false);
}
for(Animator a:anims)
a.setDuration(1000);
ObjectAnimator detailFadeIn=ObjectAnimator.ofFloat(detailsOverlay, View.ALPHA, 1);
detailFadeIn.setDuration(200);
detailFadeIn.setStartDelay(800);
anims.add(detailFadeIn);
set.playTogether(anims);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentZoomAnim=null;
}
});
currentZoomAnim=set;
set.start();
contentView.postOnAnimation(followAroundAnimationUpdater);
contentWrap.setOnClickListener(v_->zoomOut());
}
private void updateFollowAroundAnimation(){
if(avatarToFollowAround==null)
return;
int[] loc={0, 0};
content.setTranslationX(0);
content.setTranslationY(0);
avatarToFollowAround.getLocationInWindow(loc);
float avaX=loc[0], avaY=loc[1];
content.getLocationInWindow(loc);
float contX=loc[0], contY=loc[1];
float xOffset=content.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL ? content.getWidth()-V.dp(21+128) : V.dp(21);
content.setTranslationX((contX-avaX+xOffset)*(content.getScaleX()-1f)/2.2f);
content.setTranslationY((contY-avaY+content.getHeight()/2f-V.dp(64))*(content.getScaleY()-1f)/2.2f);
contentView.postOnAnimation(followAroundAnimationUpdater);
}
private void zoomOut(){
if(currentZoomAnim!=null)
currentZoomAnim.cancel();
AnimatorSet set=new AnimatorSet();
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(content, View.SCALE_X, 1));
anims.add(ObjectAnimator.ofFloat(content, View.SCALE_Y, 1));
anims.add(ObjectAnimator.ofFloat(selfAva, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(orbitBGView, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(title, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(subtitle, View.ALPHA, 1));
for(ImageView ava:avatarViews){
anims.add(ObjectAnimator.ofFloat(ava, View.ALPHA, 1));
ava.setEnabled(true);
}
for(Animator a:anims)
a.setDuration(500);
anims.add(ObjectAnimator.ofFloat(detailsOverlay, View.ALPHA, 0).setDuration(150));
set.playTogether(anims);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
avatarToFollowAround=null;
contentWrap.setOnClickListener(null);
detailsOverlay.setVisibility(View.GONE);
currentZoomAnim=null;
}
});
currentZoomAnim=set;
set.start();
}
private class OrbitBackgroundDrawable extends Drawable{
private Paint strokePaint=new Paint(Paint.ANTI_ALIAS_FLAG), fillPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private RadialGradient gradient;
private Matrix matrix=new Matrix();
public OrbitBackgroundDrawable(){
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setStrokeWidth(V.dp(4));
strokePaint.setPathEffect(new DashPathEffect(new float[]{V.dp(4), V.dp(12)}, 0));
strokePaint.setColor(0xFF562CFC);
strokePaint.setStrokeCap(Paint.Cap.ROUND);
fillPaint.setShader(gradient=new RadialGradient(0f, 0f, 1f, new int[]{0x8017063B, 0x8017063B, 0x802F0C7A}, new float[]{0f, .77f, 1f}, Shader.TileMode.CLAMP));
}
@Override
public void draw(@NonNull Canvas canvas){
Rect bounds=getBounds();
float radius=bounds.width()/2f-V.dp(2);
matrix.setTranslate(bounds.centerX(), bounds.centerY());
matrix.postScale(radius, radius, bounds.centerX(), bounds.centerY());
gradient.setLocalMatrix(matrix);
canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, fillPaint);
canvas.drawCircle(bounds.centerX(), bounds.centerY(), radius, strokePaint);
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
}
}

View File

@ -0,0 +1,71 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.FrameLayout;
import org.joinmastodon.android.R;
public class RoundedFrameLayout extends FrameLayout{
private int cornerRadius;
private boolean roundBottomCorners=true;
private Paint clearPaint=new Paint(Paint.ANTI_ALIAS_FLAG), paint=new Paint(Paint.ANTI_ALIAS_FLAG);
public RoundedFrameLayout(Context context){
this(context, null);
}
public RoundedFrameLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public RoundedFrameLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.RoundedImageView);
cornerRadius=ta.getDimensionPixelOffset(R.styleable.RoundedImageView_cornerRadius, 0);
roundBottomCorners=ta.getBoolean(R.styleable.RoundedImageView_roundBottomCorners, true);
ta.recycle();
setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight()+(roundBottomCorners ? 0 : cornerRadius), cornerRadius);
}
});
setClipToOutline(true);
clearPaint.setColor(0xFFFFFFFF);
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
paint.setColor(0xFF0ff000);
}
public void setCornerRadius(int cornerRadius){
this.cornerRadius=cornerRadius;
invalidateOutline();
}
public void setRoundBottomCorners(boolean roundBottomCorners){
this.roundBottomCorners=roundBottomCorners;
invalidateOutline();
}
@Override
public void draw(Canvas canvas){
if(canvas.isHardwareAccelerated()){
super.draw(canvas);
return;
}
canvas.saveLayer(0, 0, getWidth(), getHeight(), null);
canvas.drawRoundRect(0, 0, getWidth(), getHeight()+(roundBottomCorners ? 0 : cornerRadius), cornerRadius, cornerRadius, paint);
canvas.saveLayer(0, 0, getWidth(), getHeight(), clearPaint);
super.draw(canvas);
canvas.restore();
canvas.restore();
}
}

View File

@ -0,0 +1,74 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.ImageView;
import org.joinmastodon.android.R;
/**
* Software-rendering-friendly rounded-corners image view. Relies on arcane xrefmode magic.
*/
public class RoundedImageView extends ImageView{
private int cornerRadius;
private boolean roundBottomCorners=true;
private Paint clearPaint=new Paint(Paint.ANTI_ALIAS_FLAG), paint=new Paint(Paint.ANTI_ALIAS_FLAG);
public RoundedImageView(Context context){
this(context, null);
}
public RoundedImageView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public RoundedImageView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.RoundedImageView);
cornerRadius=ta.getDimensionPixelOffset(R.styleable.RoundedImageView_cornerRadius, 0);
roundBottomCorners=ta.getBoolean(R.styleable.RoundedImageView_roundBottomCorners, true);
ta.recycle();
setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight()+(roundBottomCorners ? 0 : cornerRadius), cornerRadius);
}
});
setClipToOutline(true);
clearPaint.setColor(0xFFFFFFFF);
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
paint.setColor(0xFF0ff000);
}
public void setCornerRadius(int cornerRadius){
this.cornerRadius=cornerRadius;
invalidateOutline();
}
public void setRoundBottomCorners(boolean roundBottomCorners){
this.roundBottomCorners=roundBottomCorners;
invalidateOutline();
}
@Override
public void draw(Canvas canvas){
if(canvas.isHardwareAccelerated()){
super.draw(canvas);
return;
}
canvas.saveLayer(0, 0, getWidth(), getHeight(), null);
canvas.drawRoundRect(0, 0, getWidth(), getHeight()+(roundBottomCorners ? 0 : cornerRadius), cornerRadius, cornerRadius, paint);
canvas.saveLayer(0, 0, getWidth(), getHeight(), clearPaint);
super.draw(canvas);
canvas.restore();
canvas.restore();
}
}

View File

@ -0,0 +1,225 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AnnualReport;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.text.NumberFormat;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class SummaryWrapScene extends AnnualWrapScene{
private final Map<String, Account> allAccounts;
private final Map<String, Status> allStatuses;
private final AnnualReport report;
private final Account self;
private Drawable postIcon;
public SummaryWrapScene(Account self, Map<String, Account> allAccounts, Map<String, Status> allStatuses, AnnualReport report){
this.allAccounts=allAccounts;
this.allStatuses=allStatuses;
this.report=report;
this.self=self;
}
@SuppressLint("SetTextI18n")
@Override
protected View onCreateContentView(Context context){
LayoutInflater inflater=LayoutInflater.from(context);
View content=inflater.inflate(R.layout.wrap_summary, null);
ImageView appIcon=content.findViewById(R.id.app_icon);
ImageView selfAva=content.findViewById(R.id.self_ava);
TextView selfName=content.findViewById(R.id.self_name);
TextView mostBoostedText=content.findViewById(R.id.most_boosted_post_text);
TextView plusFollowers=content.findViewById(R.id.plus_followers);
TextView followersLabel=content.findViewById(R.id.followers_label);
TextView totalFollowers=content.findViewById(R.id.followers_total);
View followersChart=content.findViewById(R.id.followers_chart);
TextView hashtag=content.findViewById(R.id.most_used_hashtag);
TextView followersPercent=content.findViewById(R.id.followers_percentile);
TextView newPostsCount=content.findViewById(R.id.new_posts);
TextView newPostsLabel=content.findViewById(R.id.new_posts_label);
View newPostsBlock=content.findViewById(R.id.new_posts_block);
postIcon=prepareIcon(context, R.drawable.ic_chat_bubble_wght700_20px);
if(!report.mostUsedApps.isEmpty()){
AnnualReport.NameAndCount app=report.mostUsedApps.get(0);
if("Mastodon for Android".equals(app.name) || "Mastodon for iOS".equals(app.name)){
appIcon.setImageResource(R.mipmap.ic_launcher);
}else{
String url=AppsWrapScene.getIconUrl(app.name);
if(url==null)
url=AppsWrapScene.getIconUrl(app.name.split(" ")[0]);
if(url!=null){
ViewImageLoader.loadWithoutAnimation(appIcon, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(96), V.dp(96), List.of(), Uri.parse(url)));
}
}
}
ViewImageLoader.loadWithoutAnimation(selfAva, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(40), V.dp(40), List.of(), Uri.parse(self.avatarStatic)));
selfName.setText(HtmlParser.parseCustomEmoji(self.displayName, self.emojis));
UiUtils.loadCustomEmojiInTextView(selfName);
Status mostBoosted=allStatuses.get(report.topStatuses.byReblogs);
if(mostBoosted!=null){
mostBoostedText.setText(HtmlParser.parse(mostBoosted.content, mostBoosted.emojis, mostBoosted.mentions, mostBoosted.tags, null, mostBoosted));
}
int newFollowers=report.timeSeries.stream().mapToInt(p->p.followers).sum();
plusFollowers.setText("+"+UiUtils.abbreviateNumber(newFollowers));
followersLabel.setText(content.getResources().getQuantityString(R.plurals.followers, newFollowers>1000 ? 9999 : newFollowers));
totalFollowers.setText(context.getString(R.string.x_followers_total, UiUtils.abbreviateNumber(self.followersCount)));
if(!report.topHashtags.isEmpty()){
hashtag.setText("#"+report.topHashtags.get(0).name);
}
double followersPercentile=Objects.requireNonNullElse(report.percentiles.get("followers"), 0.0);
followersPercent.setText(Math.max(1, Math.round(100-followersPercentile))+"%");
newPostsCount.setText(NumberFormat.getInstance().format(report.typeDistribution.standalone));
newPostsLabel.setText(context.getResources().getQuantityString(R.plurals.new_posts, report.typeDistribution.standalone));
newPostsBlock.setBackground(new NewPostsBackgroundDrawable());
followersChart.setBackground(new FollowersChartDrawable());
return content;
}
@Override
protected void onDestroyContentView(){
}
private Drawable prepareIcon(Context context, int res){
Drawable d=context.getResources().getDrawable(res, context.getTheme()).mutate();
d.setTint(0xFF17063B);
d.setBounds(V.dp(2), V.dp(2), V.dp(14), V.dp(14));
return d;
}
private class NewPostsBackgroundDrawable extends Drawable{
private static final int GRID_W=11;
private static final int GRID_H=6;
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
@Override
public void draw(@NonNull Canvas canvas){
Rect bounds=getBounds();
paint.setColor(0xFF2F0C7A);
canvas.drawRect(bounds, paint);
float spacing=(bounds.width()+V.dp(16)-V.dp(16*GRID_W))/(float)(GRID_W-1);
float radius=V.dp(8);
canvas.saveLayerAlpha(bounds.left, bounds.top, bounds.right, bounds.bottom, 51);
canvas.translate(-radius, -radius);
for(int y=0;y<GRID_H;y++){
canvas.save();
for(int x=0;x<GRID_W;x++){
int color;
Drawable icon;
color=0xFFFFBE2E;
icon=postIcon;
paint.setColor(color);
canvas.drawCircle(radius, radius, radius, paint);
if(icon!=null){
icon.draw(canvas);
}
canvas.translate(radius*2+spacing, 0);
}
canvas.restore();
canvas.translate(0, radius*2+spacing);
}
canvas.restore();
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.OPAQUE;
}
}
private class FollowersChartDrawable extends Drawable{
private Path path=new Path();
private Paint fillPaint=new Paint(Paint.ANTI_ALIAS_FLAG), strokePaint=new Paint(Paint.ANTI_ALIAS_FLAG);
public FollowersChartDrawable(){
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setStrokeWidth(V.dp(1));
strokePaint.setColor(0xFF562CFC);
}
@Override
public void draw(@NonNull Canvas canvas){
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
path.rewind();
int pad=V.dp(16);
path.moveTo(-20, bounds.height()+20);
float dx=bounds.width()/11f;
float x=0f;
int max=report.timeSeries.stream().mapToInt(p->p.followers).max().orElse(1);
for(AnnualReport.TimeSeriesPoint point:report.timeSeries){
float fraction=point.followers/(float)max;
path.lineTo(x, pad+(bounds.height()-pad*2)*(1f-fraction));
x+=dx;
}
path.lineTo(bounds.width()+20, bounds.height()+20);
path.close();
fillPaint.setShader(new LinearGradient(0, pad, 0, bounds.height()-pad, 0x80562CFC, 0x00562CFC, Shader.TileMode.CLAMP));
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
}
}

View File

@ -0,0 +1,102 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.content.res.Resources;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.AnnualReport;
import java.text.NumberFormat;
import java.util.List;
public class TimeSeriesWrapScene extends AnnualWrapScene{
private final List<AnnualReport.TimeSeriesPoint> points;
private TextView postsCounter, followersCounter, followingCounter;
private TextView postsLabel, followersLabel, followingLabel;
private TextView monthLabel;
private int currentMonth, monthOnGestureStart;
private float downX, downY, touchslop;
private boolean trackingTouch, figuringOutWhetherWeWantThisGesture;
public TimeSeriesWrapScene(List<AnnualReport.TimeSeriesPoint> points){
this.points=points;
}
@Override
protected View onCreateContentView(Context context){
LayoutInflater inflater=LayoutInflater.from(context);
View content=inflater.inflate(R.layout.wrap_time_series, null);
postsCounter=content.findViewById(R.id.posts_counter);
followersCounter=content.findViewById(R.id.followers_counter);
followingCounter=content.findViewById(R.id.following_counter);
postsLabel=content.findViewById(R.id.posts_label);
followersLabel=content.findViewById(R.id.followers_label);
followingLabel=content.findViewById(R.id.following_label);
monthLabel=content.findViewById(R.id.month_label);
setMonth(0);
content.setOnTouchListener(this::onContentTouch);
touchslop=ViewConfiguration.get(context).getScaledTouchSlop();
return content;
}
@Override
protected void onDestroyContentView(){
}
private boolean onContentTouch(View v, MotionEvent ev){
if(ev.getAction()==MotionEvent.ACTION_DOWN){
downX=ev.getX();
downY=ev.getY();
trackingTouch=false;
figuringOutWhetherWeWantThisGesture=true;
monthOnGestureStart=currentMonth;
v.getParent().requestDisallowInterceptTouchEvent(true);
}else if(ev.getAction()==MotionEvent.ACTION_MOVE){
if(!trackingTouch){
float dX=Math.abs(downX-ev.getX());
float dY=Math.abs(downY-ev.getY());
if(dX>dY && dX>=touchslop){
trackingTouch=true;
figuringOutWhetherWeWantThisGesture=false;
}else if(dY>=touchslop){
v.getParent().requestDisallowInterceptTouchEvent(false);
figuringOutWhetherWeWantThisGesture=false;
}
}else{
int monthsToAdvance=(int)((ev.getX()-downX)/(v.getWidth()/15f));
if(monthsToAdvance!=0){
int newCurrentMonth=Math.min(11, Math.max(0, monthOnGestureStart+monthsToAdvance));
if(newCurrentMonth!=currentMonth){
setMonth(newCurrentMonth);
v.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
}
}
}
}
return trackingTouch || figuringOutWhetherWeWantThisGesture;
}
private void setMonth(int monthIndex){
currentMonth=monthIndex;
AnnualReport.TimeSeriesPoint point=points.get(monthIndex);
postsCounter.setText(NumberFormat.getInstance().format(point.statuses));
followersCounter.setText(NumberFormat.getInstance().format(point.followers));
followingCounter.setText(NumberFormat.getInstance().format(point.following));
Resources r=postsCounter.getResources();
postsLabel.setText(r.getQuantityString(R.plurals.posts, point.statuses));
followersLabel.setText(r.getQuantityString(R.plurals.followers, point.followers));
followingLabel.setText(r.getQuantityString(R.plurals.following, point.following));
monthLabel.setText(r.getStringArray(R.array.months_standalone)[monthIndex]);
}
}

View File

@ -0,0 +1,20 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.view.View;
import org.joinmastodon.android.model.Status;
public class TopPostsWrapScene extends AnnualWrapScene{
private Status mostBoosted, mostFavorited, mostReplied;
@Override
protected View onCreateContentView(Context context){
return null;
}
@Override
protected void onDestroyContentView(){
}
}

View File

@ -0,0 +1,23 @@
package org.joinmastodon.android.ui.wrapstodon;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.joinmastodon.android.R;
public class WelcomeWrapScene extends AnnualWrapScene{
@Override
protected View onCreateContentView(Context context){
View view=context.getSystemService(LayoutInflater.class).inflate(R.layout.wrap_welcome, null);
TextView title=view.findViewById(R.id.title);
title.setText(context.getString(R.string.yearly_wrap_intro_title, year));
return view;
}
@Override
protected void onDestroyContentView(){
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M34.34,7.947L51.66,17.947A12,12 67.622,0 1,56.052 34.34L46.052,51.66A12,12 72.459,0 1,29.66 56.053L12.34,46.053A12,12 72.459,0 1,7.947 29.66L17.947,12.34A12,12 68.44,0 1,34.34 7.947z"
android:fillColor="#332F0C7A"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M2,22 L7,8 16,17ZM5.3,18.7 L12.35,16.2 7.8,11.65ZM14.55,12.55 L13.5,11.5 19.1,5.9Q19.9,5.1 21.025,5.1Q22.15,5.1 22.95,5.9L23.55,6.5L22.5,7.55L21.9,6.95Q21.55,6.6 21.025,6.6Q20.5,6.6 20.15,6.95ZM10.55,8.55 L9.5,7.5 10.1,6.9Q10.45,6.55 10.45,6.05Q10.45,5.55 10.1,5.2L9.45,4.55L10.5,3.5L11.15,4.15Q11.95,4.95 11.95,6.05Q11.95,7.15 11.15,7.95ZM12.55,10.55 L11.5,9.5 15.1,5.9Q15.45,5.55 15.45,5.025Q15.45,4.5 15.1,4.15L13.5,2.55L14.55,1.5L16.15,3.1Q16.95,3.9 16.95,5.025Q16.95,6.15 16.15,6.95ZM16.55,14.55 L15.5,13.5 17.1,11.9Q17.9,11.1 19.025,11.1Q20.15,11.1 20.95,11.9L22.55,13.5L21.5,14.55L19.9,12.95Q19.55,12.6 19.025,12.6Q18.5,12.6 18.15,12.95ZM5.3,18.7Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M1.667,18.333V3.417Q1.667,2.688 2.177,2.177Q2.688,1.667 3.417,1.667H16.583Q17.312,1.667 17.823,2.177Q18.333,2.688 18.333,3.417V13.25Q18.333,13.979 17.823,14.49Q17.312,15 16.583,15H5ZM3.417,14.104 L4.271,13.25H16.583Q16.583,13.25 16.583,13.25Q16.583,13.25 16.583,13.25V3.417Q16.583,3.417 16.583,3.417Q16.583,3.417 16.583,3.417H3.417Q3.417,3.417 3.417,3.417Q3.417,3.417 3.417,3.417ZM3.417,3.417Q3.417,3.417 3.417,3.417Q3.417,3.417 3.417,3.417Q3.417,3.417 3.417,3.417Q3.417,3.417 3.417,3.417V13.25Q3.417,13.25 3.417,13.25Q3.417,13.25 3.417,13.25V14.104Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M0.958,19.042V3.667Q0.958,2.521 1.74,1.74Q2.521,0.958 3.667,0.958H16.333Q17.479,0.958 18.26,1.74Q19.042,2.521 19.042,3.667V13Q19.042,14.146 18.26,14.927Q17.479,15.708 16.333,15.708H4.292ZM3.667,13.104 L3.771,13H16.333Q16.333,13 16.333,13Q16.333,13 16.333,13V3.667Q16.333,3.667 16.333,3.667Q16.333,3.667 16.333,3.667H3.667Q3.667,3.667 3.667,3.667Q3.667,3.667 3.667,3.667ZM3.667,3.667Q3.667,3.667 3.667,3.667Q3.667,3.667 3.667,3.667Q3.667,3.667 3.667,3.667Q3.667,3.667 3.667,3.667V13Q3.667,13 3.667,13Q3.667,13 3.667,13V13.104Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M15.125,16.104V12.625Q15.125,11.771 14.542,11.188Q13.958,10.604 13.104,10.604H7.125L9.208,12.688L7.479,14.417L2.417,9.375L7.479,4.333L9.208,6.062L7.125,8.146H13.104Q14.979,8.146 16.281,9.448Q17.583,10.75 17.583,12.625V16.104Z"/>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/paper_texture"
android:tileMode="repeat"/>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="12dp"/>
<stroke android:color="#000" android:width="1dp"/>
<solid android:color="#0000"/>
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="12dp"/>
<stroke android:color="#000" android:width="2dp"/>
<solid android:color="#0000"/>
</shape>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#FFBE2E"/>
</shape>
</item>
<item android:gravity="top">
<shape>
<solid android:color="#80FFFFFF"/>
<size android:height="2dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#562CFC"/>
</shape>
</item>
<item android:gravity="top">
<shape>
<solid android:color="#80FFFFFF"/>
<size android:height="2dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape>
<solid android:color="#72737C"/>
</shape>
</item>
<item android:gravity="top">
<shape>
<solid android:color="#80FFFFFF"/>
<size android:height="2dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="24dp"/>
<stroke android:width="2dp" android:color="#80FFFFFF"/>
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="10dp"/>
<stroke android:width="2dp" android:color="#80FFFFFF"/>
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp"/>
<stroke android:width="2dp" android:color="#80FFFFFF"/>
</shape>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<stroke android:color="#26FFFFFF" android:width="2dp"/>
</shape>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<stroke android:color="#26FFFFFF" android:width="1dp"/>
</shape>

View File

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="38dp"
android:height="77dp"
android:viewportWidth="38"
android:viewportHeight="77">
<path
android:pathData="M35,73L19,61L3,73"
android:strokeAlpha="0.2"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#858AFA"
android:fillAlpha="0.2"/>
<path
android:pathData="M35,45L19,33L3,45"
android:strokeAlpha="0.5"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#858AFA"
android:fillAlpha="0.5"/>
<path
android:pathData="M35,17L19,5L3,17"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#858AFA"/>
</vector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:gravity="center">
<shape android:shape="oval">
<solid android:color="#000"/>
<size android:width="272dp" android:height="272dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="8dp"/>
<solid android:color="#2F0C7A"/>
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
android:fontProviderAuthority="com.google.android.gms.fonts"
android:fontProviderPackage="com.google.android.gms"
android:fontProviderQuery="name=Caveat&amp;weight=700">
</font-family>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
android:fontProviderAuthority="com.google.android.gms.fonts"
android:fontProviderPackage="com.google.android.gms"
android:fontProviderQuery="name=Manrope&amp;weight=400">
</font-family>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
android:fontProviderAuthority="com.google.android.gms.fonts"
android:fontProviderPackage="com.google.android.gms"
android:fontProviderQuery="name=Manrope&amp;weight=500">
</font-family>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
android:fontProviderAuthority="com.google.android.gms.fonts"
android:fontProviderPackage="com.google.android.gms"
android:fontProviderQuery="name=Manrope&amp;weight=600">
</font-family>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
android:fontProviderAuthority="com.google.android.gms.fonts"
android:fontProviderPackage="com.google.android.gms"
android:fontProviderQuery="name=Manrope&amp;weight=700">
</font-family>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android"
android:fontProviderAuthority="com.google.android.gms.fonts"
android:fontProviderPackage="com.google.android.gms"
android:fontProviderQuery="name=Roboto Slab&amp;weight=600">
</font-family>

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="96dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:fontFamily="@font/manrope_w600"
android:textSize="25dp"
android:textColor="#BAFF3B"
android:lineSpacingExtra="-4dp"
android:text="@string/wrap_apps_title"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:fontFamily="@font/manrope_w600"
android:textColor="#858AFA"
android:textSize="16dp"
android:lineSpacingExtra="-3dp"
android:text="@string/wrap_apps_subtitle"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/app2_bar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="32dp"
android:background="@drawable/wrap_app2_bar"/>
<org.joinmastodon.android.ui.wrapstodon.RoundedImageView
android:id="@+id/app2_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_centerHorizontal="true"
app:cornerRadius="24dp"
android:foreground="@drawable/wrap_app_icon_border"
tools:src="#0f0"/>
<TextView
android:id="@+id/app2_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/app2_icon"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:fontFamily="@font/manrope_w600"
android:textSize="16dp"
android:textColor="#CCCFFF"
android:lineSpacingExtra="-3dp"
tools:text="Mastodon for Android"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/app1_bar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="32dp"
android:background="@drawable/wrap_app1_bar"/>
<org.joinmastodon.android.ui.wrapstodon.RoundedImageView
android:id="@+id/app1_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_centerHorizontal="true"
app:cornerRadius="24dp"
android:foreground="@drawable/wrap_app_icon_border"
tools:src="#0f0"/>
<TextView
android:id="@+id/app1_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/app1_icon"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:fontFamily="@font/manrope_w600"
android:textSize="16dp"
android:textColor="#17063B"
android:lineSpacingExtra="-3dp"
tools:text="Mastodon for Android"/>
</RelativeLayout>
<RelativeLayout
android:id="@+id/app3_bar"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="32dp"
android:background="@drawable/wrap_app3_bar"/>
<org.joinmastodon.android.ui.wrapstodon.RoundedImageView
android:id="@+id/app3_icon"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_centerHorizontal="true"
app:cornerRadius="24dp"
android:foreground="@drawable/wrap_app_icon_border"
tools:src="#0f0"/>
<TextView
android:id="@+id/app3_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/app3_icon"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:fontFamily="@font/manrope_w600"
android:textSize="16dp"
android:textColor="#E0E0EB"
android:lineSpacingExtra="-3dp"
tools:text="Mastodon for Android"/>
</RelativeLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="16dp"
android:paddingTop="80dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#CCCFFF"
android:textSize="25dp"
android:fontFamily="@font/manrope_w600"
android:text="@string/yearly_wrap_archetype_title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#858AFA"
android:textSize="20dp"
android:fontFamily="@font/manrope_w600"
tools:text="@string/yearly_wrap_archetype_subtitle"/>
<LinearLayout
android:id="@+id/picture_frame"
android:layout_width="280dp"
android:layout_height="352dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="15dp"
android:background="@drawable/paper_texture_tiled">
<org.joinmastodon.android.ui.wrapstodon.RoundedFrameLayout
android:id="@+id/photo_wrap"
android:layout_width="248dp"
android:layout_height="248dp">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:importantForAccessibility="no"/>
</org.joinmastodon.android.ui.wrapstodon.RoundedFrameLayout>
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:fontFamily="@font/caveat_w700"
android:textSize="28dp"
android:textColor="#222222"
android:singleLine="true"
android:ellipsize="end"
tools:text="\@username"/>
<TextView
android:id="@+id/domain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/caveat_w700"
android:textSize="20dp"
android:textColor="#72737C"
android:singleLine="true"
android:ellipsize="end"
tools:text="\@mastodon.social"/>
</LinearLayout>
<TextView
android:id="@+id/archetype_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="39dp"
android:textColor="#BAFF3B"
android:fontFamily="@font/manrope_w600"
tools:text="Pollster"/>
<TextView
android:id="@+id/archetype_explanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/manrope_w600"
android:textSize="16dp"
android:textColor="#858AFA"
tools:text="You post a lot of polls or something"/>
</LinearLayout>

View File

@ -0,0 +1,200 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="32dp"
android:paddingBottom="16dp"
android:clipToPadding="false">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/rank"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/manrope_w600"
android:textSize="61.04dp"
android:textColor="#FFBE2E"
tools:text="#1"/>
<TextView
android:id="@+id/count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/manrope_w600"
android:textColor="#858AFA"
android:textSize="14dp"
tools:text="47 boosts"/>
</LinearLayout>
<RelativeLayout
android:layout_width="0dp"
android:layout_height="185dp"
android:layout_weight="1"
android:background="@drawable/rect_12dp"
android:backgroundTint="#141218"
android:foreground="@drawable/rect_12dp_stroke"
android:foregroundTint="#49454F">
<org.joinmastodon.android.ui.wrapstodon.RoundedImageView
android:id="@+id/cover"
android:layout_width="match_parent"
android:layout_height="81dp"
android:layout_marginBottom="8dp"
android:scaleType="centerCrop"
app:cornerRadius="13dp"
app:roundBottomCorners="false"
tools:src="#0f0"/>
<org.joinmastodon.android.ui.wrapstodon.RoundedImageView
android:id="@+id/avatar"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="12dp"
android:layout_marginTop="69dp"
android:layout_marginEnd="8dp"
app:cornerRadius="13dp"
android:foreground="@drawable/rect_12dp_stroke2dp"
android:foregroundTint="#141218"
tools:src="#f00"/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/avatar"
android:layout_below="@id/cover"
android:layout_marginEnd="12dp"
android:singleLine="true"
android:ellipsize="end"
android:textColor="#E6E0E9"
android:fontFamily="sans-serif-medium"
android:textSize="14dp"
android:gravity="center_vertical"
tools:text="Cody Fisher"/>
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="16dp"
android:layout_alignLeft="@id/name"
android:layout_alignRight="@id/name"
android:layout_below="@id/name"
android:textSize="12dp"
android:textColor="#CAC4D0"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="\@cfisher@mastodon.social"/>
<LinearLayout
android:id="@+id/posts_counter"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_below="@id/avatar"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="32dp"
android:orientation="vertical">
<TextView
android:id="@+id/posts_value"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textSize="14dp"
android:fontFamily="sans-serif-medium"
android:textColor="#E6E0E9"
android:gravity="center_vertical"
android:singleLine="true"
tools:text="109"/>
<TextView
android:id="@+id/posts_label"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:textSize="11dp"
android:textColor="#CAC4D0"
android:gravity="center_vertical"
android:singleLine="true"
tools:text="posts"/>
</LinearLayout>
<LinearLayout
android:id="@+id/followers_counter"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_below="@id/avatar"
android:layout_toEndOf="@id/posts_counter"
android:layout_marginTop="12dp"
android:layout_marginEnd="32dp"
android:orientation="vertical">
<TextView
android:id="@+id/followers_value"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textSize="14dp"
android:fontFamily="sans-serif-medium"
android:textColor="#E6E0E9"
android:gravity="center_vertical"
android:singleLine="true"
tools:text="109"/>
<TextView
android:id="@+id/followers_label"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:textSize="11dp"
android:textColor="#CAC4D0"
android:gravity="center_vertical"
android:singleLine="true"
tools:text="posts"/>
</LinearLayout>
<LinearLayout
android:id="@+id/following_counter"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:layout_below="@id/avatar"
android:layout_toEndOf="@id/followers_counter"
android:layout_marginTop="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/following_value"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:textSize="14dp"
android:fontFamily="sans-serif-medium"
android:textColor="#E6E0E9"
android:gravity="center_vertical"
android:singleLine="true"
tools:text="109"/>
<TextView
android:id="@+id/following_label"
android:layout_width="wrap_content"
android:layout_height="16dp"
android:textSize="11dp"
android:textColor="#CAC4D0"
android:gravity="center_vertical"
android:singleLine="true"
tools:text="posts"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="96dp"
android:paddingHorizontal="16dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="31.25dp"
android:lineSpacingExtra="-5dp"
android:fontFamily="@font/manrope_w600"
android:textColor="#858AFA"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20dp"
android:lineSpacingExtra="-3dp"
android:fontFamily="@font/manrope_w600"
android:textColor="#858AFA"/>
</LinearLayout>

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:paddingTop="96dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:fontFamily="@font/manrope_w600"
android:text="@string/wrap_interacted_accounts_title"
android:textColor="#BAFF3B"
android:textSize="25dp" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:fontFamily="@font/manrope_w600"
android:text="@string/wrap_interacted_accounts_subtitle"
android:textColor="#858AFA"
android:textSize="20dp" />
<FrameLayout
android:id="@+id/orbit"
android:layout_width="match_parent"
android:layout_height="449dp">
<View
android:id="@+id/orbit_bg"
android:layout_width="449dp"
android:layout_height="449dp"
android:layout_gravity="center" />
<FrameLayout
android:id="@+id/inner_ring"
android:layout_width="296dp"
android:layout_height="296dp"
android:layout_gravity="center" />
<FrameLayout
android:id="@+id/outer_ring"
android:layout_width="449dp"
android:layout_height="449dp"
android:layout_gravity="center" />
<org.joinmastodon.android.ui.wrapstodon.RoundedImageView
android:id="@+id/self_ava"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_gravity="center"
android:foreground="@drawable/wrap_ava_border"
app:cornerRadius="80dp"
tools:src="#0f0" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/details_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_vertical"
android:paddingStart="164dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/details_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:fontFamily="@font/manrope_w600"
android:textSize="76.29dp"
android:textColor="#BAFF3B"
android:singleLine="true"
tools:text="97"/>
<TextView
android:id="@+id/details_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="-15dp"
android:gravity="center"
android:fontFamily="@font/manrope_w600"
android:textColor="#858AFA"
android:textSize="16dp"
tools:text="replies exchanged with Theresa Webb"/>
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/wrap_numbers_bg_circle">
<TextView
android:id="@+id/number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="131dp"
android:textColor="#17063B"
android:singleLine="true"
android:gravity="center_horizontal"
android:letterSpacing="-0.06"
android:fontFamily="@font/manrope_w700"/>
<TextView
android:id="@+id/top_text"
android:layout_width="272dp"
android:layout_height="wrap_content"
android:layout_above="@id/number"
android:layout_centerHorizontal="true"
android:layout_marginBottom="-14dp"
android:textSize="14dp"
android:textColor="#17063B"
android:gravity="center_horizontal"
android:fontFamily="@font/manrope_w400"/>
<TextView
android:id="@+id/bottom_text"
android:layout_width="272dp"
android:layout_height="wrap_content"
android:layout_below="@id/number"
android:layout_centerHorizontal="true"
android:layout_marginTop="-6dp"
android:textSize="17dp"
android:textColor="#17063B"
android:gravity="center_horizontal"
android:fontFamily="@font/manrope_w600"/>
</RelativeLayout>

View File

@ -0,0 +1,306 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingTop="96dp"
android:paddingHorizontal="16dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/manrope_w600"
android:textSize="25dp"
android:lineSpacingExtra="-4dp"
android:textColor="#BAFF3B"
android:text="@string/wrap_summary_title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_marginBottom="16dp"
android:textColor="#CCCFFF"
android:lineSpacingExtra="-3dp"
android:fontFamily="@font/manrope_w600"
android:text="@string/wrap_summary_subtitle"/>
<LinearLayout
android:id="@+id/most_used_app_block"
android:layout_width="104dp"
android:layout_height="113dp"
android:layout_below="@id/subtitle"
android:layout_alignParentStart="true"
android:orientation="vertical"
android:paddingTop="10dp"
android:background="@drawable/wrap_summary_block_bg">
<org.joinmastodon.android.ui.wrapstodon.RoundedImageView
android:id="@+id/app_icon"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="center_horizontal"
android:foreground="@drawable/wrap_app_icon_border_small"
app:cornerRadius="4dp"
tools:src="#f00"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="8dp"
android:fontFamily="@font/manrope_w600"
android:textColor="#CCCFFF"
android:textSize="14dp"
android:lineSpacingExtra="-2dp"
android:text="@string/wrap_most_used_app"/>
</LinearLayout>
<org.joinmastodon.android.ui.wrapstodon.RoundedFrameLayout
android:id="@+id/followers_stats_block"
android:layout_width="104dp"
android:layout_height="127dp"
android:layout_alignParentStart="true"
android:layout_below="@id/most_used_app_block"
android:layout_marginVertical="8dp"
app:cornerRadius="4dp"
android:background="@drawable/wrap_summary_block_bg">
<View
android:id="@+id/followers_chart"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/plus_followers"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp"
android:layout_marginTop="37dp"
android:singleLine="true"
android:fontFamily="@font/manrope_w600"
android:textSize="31.25dp"
android:textColor="#BAFF3B"
tools:text="+1.2K"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/followers_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#CCCFFF"
android:fontFamily="@font/manrope_w600"
android:textSize="14dp"
android:singleLine="true"
tools:text="followers"/>
<TextView
android:id="@+id/followers_total"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#858AFA"
android:textSize="10.24dp"
android:fontFamily="@font/manrope_w400"
android:singleLine="true"
tools:text="21.5K total"/>
</LinearLayout>
</org.joinmastodon.android.ui.wrapstodon.RoundedFrameLayout>
<RelativeLayout
android:id="@+id/most_boosted_post_block"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/subtitle"
android:layout_toEndOf="@id/most_used_app_block"
android:layout_alignBottom="@id/followers_stats_block"
android:layout_marginStart="8dp"
android:background="@drawable/wrap_summary_block_bg"
android:padding="16dp">
<org.joinmastodon.android.ui.wrapstodon.RoundedImageView
android:id="@+id/self_ava"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="8dp"
app:cornerRadius="10dp"
android:foreground="@drawable/wrap_app_icon_border_medium"
tools:src="#00f"/>
<TextView
android:id="@+id/self_name"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/self_ava"
android:textColor="#CCCFFF"
android:fontFamily="sans-serif-medium"
android:textSize="14dp"
android:singleLine="true"
android:ellipsize="end"
tools:text="Cody Fisher"/>
<TextView
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/self_ava"
android:layout_below="@id/self_name"
android:textColor="#858AFA"
android:textSize="14dp"
android:singleLine="true"
android:text="@string/wrap_most_boosted_post"/>
<TextView
android:id="@+id/most_boosted_post_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/self_ava"
android:layout_marginTop="8dp"
android:textColor="#CCCFFF"
android:textSize="16dp"
android:lineSpacingExtra="5dp"
android:ellipsize="end"
tools:text="Post text goes here"/>
</RelativeLayout>
<FrameLayout
android:id="@+id/most_used_hashtag_block"
android:layout_width="216dp"
android:layout_height="75dp"
android:layout_below="@id/followers_stats_block"
android:background="@drawable/wrap_summary_block_bg">
<TextView
android:id="@+id/most_used_hashtag"
android:layout_width="500dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="-8dp"
android:gravity="center"
android:textSize="48.83dp"
android:fontFamily="@font/manrope_w600"
android:textColor="#CCCFFF"
android:singleLine="true"
tools:text="#caturday"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="8dp"
android:singleLine="true"
android:textSize="14dp"
android:fontFamily="@font/manrope_w600"
android:textColor="#858AFA"
android:text="@string/wrap_most_used_hashtag"/>
</FrameLayout>
<org.joinmastodon.android.ui.wrapstodon.RoundedFrameLayout
android:id="@+id/new_posts_block"
android:layout_width="216dp"
android:layout_height="127dp"
android:layout_below="@id/most_used_hashtag_block"
android:layout_marginTop="8dp"
app:cornerRadius="8dp">
<TextView
android:id="@+id/new_posts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_horizontal"
android:singleLine="true"
android:textSize="76.29dp"
android:textColor="#FFC954"
android:fontFamily="@font/manrope_w600"
tools:text="313"/>
<TextView
android:id="@+id/new_posts_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginBottom="13dp"
android:textSize="20dp"
android:fontFamily="@font/manrope_w600"
android:gravity="center_horizontal"
android:textColor="#CCCFFF"
android:singleLine="true"
tools:text="new posts"/>
</org.joinmastodon.android.ui.wrapstodon.RoundedFrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/followers_stats_block"
android:layout_toEndOf="@id/most_used_hashtag_block"
android:layout_alignBottom="@id/new_posts_block"
android:layout_marginStart="8dp"
android:paddingVertical="8dp"
android:background="@drawable/wrap_summary_block_bg">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="7dp"
android:textColor="#858AFA"
android:textSize="10.24dp"
android:fontFamily="@font/manrope_w400"
android:gravity="center_horizontal"
android:text="@string/wrap_percentile_before"/>
<TextView
android:id="@+id/followers_percentile"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:textColor="#FFC954"
android:textSize="61.04dp"
android:fontFamily="@font/manrope_w600"
tools:text="2%"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginHorizontal="7dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#858AFA"
android:textSize="10.24dp"
android:fontFamily="@font/manrope_w400"
android:gravity="center_horizontal"
android:text="@string/wrap_percentile_after"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#858AFA"
android:textSize="8.19dp"
android:fontFamily="@font/manrope_w400"
android:gravity="center_horizontal"
android:alpha="0.5"
android:text="@string/wrap_percentile_after_after"/>
</LinearLayout>
</FrameLayout>
</RelativeLayout>

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="96dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:textColor="#858AFA"
android:fontFamily="@font/manrope_w600"
android:textSize="25dp"
android:text="@string/wrap_numbers_title"/>
<TextView
android:id="@+id/posts_counter"
android:layout_width="match_parent"
android:layout_height="92dp"
android:layout_marginTop="22dp"
android:singleLine="true"
android:gravity="center"
android:fontFamily="@font/manrope_w600"
android:textSize="76.29dp"
android:textColor="#FFC954"
tools:text="12,345"/>
<TextView
android:id="@+id/posts_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:gravity="center"
android:textSize="20dp"
android:fontFamily="@font/manrope_w400"
android:textColor="#858AFA"
tools:text="posts"/>
<TextView
android:id="@+id/followers_counter"
android:layout_width="match_parent"
android:layout_height="92dp"
android:layout_marginTop="32dp"
android:singleLine="true"
android:gravity="center"
android:fontFamily="@font/manrope_w600"
android:textSize="76.29dp"
android:textColor="#FFC954"
tools:text="12,345"/>
<TextView
android:id="@+id/followers_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:gravity="center"
android:textSize="20dp"
android:fontFamily="@font/manrope_w400"
android:textColor="#858AFA"
tools:text="followers"/>
<TextView
android:id="@+id/following_counter"
android:layout_width="match_parent"
android:layout_height="92dp"
android:layout_marginTop="32dp"
android:singleLine="true"
android:gravity="center"
android:fontFamily="@font/manrope_w600"
android:textSize="76.29dp"
android:textColor="#FFC954"
tools:text="12,345"/>
<TextView
android:id="@+id/following_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:gravity="center"
android:textSize="20dp"
android:fontFamily="@font/manrope_w400"
android:textColor="#858AFA"
tools:text="following"/>
<TextView
android:id="@+id/month_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12.8dp"
android:textColor="#FFBE2E"
android:fontFamily="@font/manrope_w600"
tools:text="January"/>
</LinearLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageButton
android:id="@+id/btn_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="16dp"
android:layout_gravity="top|start"
android:tint="?colorM3OnSurfaceVariant"
android:background="@drawable/bg_button_m3_tonal_icon"
android:outlineProvider="background"
android:contentDescription="@string/back"
android:src="@drawable/ic_baseline_close_24"/>
<Button
android:id="@+id/btn_share"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="20dp"
android:layout_gravity="top|end"
android:drawableStart="@drawable/ic_share_20px"
android:drawablePadding="8dp"
android:paddingStart="16dp"
android:paddingEnd="24dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:drawableTint="?colorM3OnPrimary"
android:text="@string/button_share"/>
</merge>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.NestableScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</org.joinmastodon.android.ui.views.NestableScrollView>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:paddingStart="14dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="20dp"
android:textColor="#858AFA"
android:fontFamily="@font/manrope_w600"
android:includeFontPadding="false"
tools:text="Welcome to your 2023"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textSize="48.83dp"
android:textColor="#F0F0F0"
android:fontFamily="@font/manrope_w600"
android:includeFontPadding="false"
android:text="#Wrapstodon"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:textSize="20dp"
android:textColor="#CCCFFF"
android:fontFamily="@font/manrope_w500"
android:includeFontPadding="false"
android:text="@string/yearly_wrap_intro_text"/>
</LinearLayout>
<View
android:layout_width="38dp"
android:layout_height="77dp"
android:layout_marginBottom="48dp"
android:layout_gravity="bottom|center_horizontal"
android:background="@drawable/wrap_intro_chevrons"/>
</FrameLayout>

View File

@ -59,4 +59,9 @@
<attr name="aspectRatio" format="float"/>
<attr name="useHeight" format="boolean"/>
</declare-styleable>
<declare-styleable name="RoundedImageView">
<attr name="cornerRadius" format="dimension"/>
<attr name="roundBottomCorners" format="boolean"/>
</declare-styleable>
</resources>

View File

@ -679,4 +679,68 @@
<string name="this_invite_has_expired">This invite link has expired.</string>
<string name="invite_link_pasted">Link pasted from your clipboard.</string>
<string name="need_invite_to_join_server">To join %s, youll need an invite link from an existing user.</string>
<string name="yearly_wrap_title">Your %s #Wrapstodon awaits!</string>
<string name="yearly_wrap_text">Unveil your years highlights and memorable moments on Mastodon.</string>
<string name="yearly_wrap_view">View #Wrapstodon</string>
<string name="yearly_wrap_intro_title">Welcome to your %s</string>
<string name="yearly_wrap_intro_text">Lets take a minute to celebrate your past year on Mastodon.</string>
<string name="yearly_wrap_archetype_title">Vibe check!</string>
<string name="yearly_wrap_archetype_subtitle">Your %s Mastodon archetype is:</string>
<string name="yearly_wrap_archetype_lurker">Lurker</string>
<string name="yearly_wrap_archetype_booster">Booster</string>
<string name="yearly_wrap_archetype_replier">Replier</string>
<string name="yearly_wrap_archetype_pollster">Pollster</string>
<string name="yearly_wrap_archetype_oracle">Oracle</string>
<string name="wrap_numbers_total_title">you posted, replied, and boosted</string>
<string name="wrap_numbers_boosts_title">%,d, or</string>
<string name="wrap_numbers_replies_title">...%,d, or</string>
<string name="wrap_numbers_standalone_posts_title">...and %,d, or</string>
<string name="wrap_numbers_x_times_in_year">times in %s.</string>
<string name="wrap_numbers_boosts">were boosts...</string>
<string name="wrap_numbers_replies">were replies...</string>
<string name="wrap_numbers_standalone_posts">were new posts.</string>
<string name="wrap_most_reblogged_title">Those were the fan favorites. But what about <b>your favorites</b>?</string>
<string name="wrap_most_reblogged_subtitle">These are accounts you <b>boosted</b> the most in %s:</string>
<plurals name="posts">
<item quantity="one">post</item>
<item quantity="other">posts</item>
</plurals>
<string name="wrap_favorite_tags_title">What about your <b>favorite hashtags</b>?</string>
<string name="wrap_favorite_tags_subtitle">Discover your most posted, liked, and boosted hashtags.</string>
<string name="wrap_interacted_accounts_title">In your orbit</string>
<string name="wrap_interacted_accounts_subtitle">These are the people you replied to the most over the past year.</string>
<string name="wrap_apps_title">Mastodon is nothing without amazing apps.</string>
<string name="wrap_apps_subtitle">It should come as no surprise, but here are your favorites:</string>
<string name="wrap_numbers_title">Its been a big year for you.</string>
<string name="wrap_summary_title">Thanks for being part of Mastodon!</string>
<string name="wrap_summary_subtitle">Thats all for now. Share your stats to your followers:</string>
<string name="wrap_most_used_app">most used app</string>
<string name="wrap_most_boosted_post">most boosted post</string>
<string name="wrap_most_used_hashtag">most used hashtag</string>
<string name="wrap_percentile_before">That puts you in the top</string>
<string name="wrap_percentile_after">of Mastodon users.</string>
<string name="wrap_percentile_after_after">We wont tell Bernie.</string>
<string name="x_followers_total">%s total</string>
<plurals name="wrap_replies_exchanged">
<item quantity="one">reply exchanged with %s</item>
<item quantity="other">replies exchanged with %s</item>
</plurals>
<string-array name="months_standalone">
<item>January</item>
<item>February</item>
<item>March</item>
<item>April</item>
<item>May</item>
<item>June</item>
<item>July</item>
<item>August</item>
<item>September</item>
<item>October</item>
<item>November</item>
<item>December</item>
</string-array>
<plurals name="new_posts">
<item quantity="one">new post</item>
<item quantity="other">new posts</item>
</plurals>
</resources>