This commit is contained in:
Grishka 2022-02-07 15:07:12 +03:00
parent cc06715aa6
commit aa193b8921
42 changed files with 7573 additions and 23 deletions

View File

@ -29,10 +29,11 @@ android {
dependencies {
api 'androidx.annotation:annotation:1.3.0'
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
implementation 'me.grishka.litex:recyclerview:1.2.1'
implementation 'me.grishka.litex:recyclerview:1.2.1.1'
implementation 'me.grishka.litex:swiperefreshlayout:1.1.0'
implementation 'me.grishka.litex:browser:1.4.0'
implementation 'me.grishka.litex:dynamicanimation:1.1.0-alpha03'
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.appkit:appkit:1.2'
implementation 'com.google.code.gson:gson:2.8.9'

View File

@ -1,6 +1,7 @@
package org.joinmastodon.android.api;
import android.net.Uri;
import android.util.Pair;
import com.google.gson.reflect.TypeToken;
@ -10,6 +11,7 @@ import org.joinmastodon.android.model.BaseModel;
import org.joinmastodon.android.model.Token;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -27,7 +29,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
private String path;
private String method;
private Object requestBody;
private Map<String, String> queryParams;
private List<Pair<String, String>> queryParams;
Class<T> respClass;
TypeToken<T> respTypeToken;
Call okhttpCall;
@ -86,8 +88,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
protected void addQueryParameter(String key, String value){
if(queryParams==null)
queryParams=new HashMap<>();
queryParams.put(key, value);
queryParams=new ArrayList<>();
queryParams.add(new Pair<>(key, value));
}
protected void addHeader(String key, String value){
@ -106,8 +108,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
.authority(domain)
.path(getPathPrefix()+path);
if(queryParams!=null){
for(Map.Entry<String, String> param:queryParams.entrySet()){
builder.appendQueryParameter(param.getKey(), param.getValue());
for(Pair<String, String> param:queryParams){
builder.appendQueryParameter(param.first, param.second);
}
}
return builder.build();

View File

@ -0,0 +1,18 @@
package org.joinmastodon.android.api.requests.accounts;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Relationship;
import java.util.List;
import androidx.annotation.NonNull;
public class GetAccountRelationships extends MastodonAPIRequest<List<Relationship>>{
public GetAccountRelationships(@NonNull List<String> ids){
super(HttpMethod.GET, "/accounts/relationships", new TypeToken<>(){});
for(String id:ids)
addQueryParameter("id[]", id);
}
}

View File

@ -7,8 +7,10 @@ import org.joinmastodon.android.model.Status;
import java.util.List;
import androidx.annotation.NonNull;
public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
public GetAccountStatuses(String id, String maxID, String minID, int limit){
public GetAccountStatuses(String id, String maxID, String minID, int limit, @NonNull Filter filter){
super(HttpMethod.GET, "/accounts/"+id+"/statuses", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
@ -16,5 +18,16 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
switch(filter){
case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {}
case MEDIA -> addQueryParameter("only_media", "true");
}
}
public enum Filter{
DEFAULT,
INCLUDE_REPLIES,
MEDIA
}
}

View File

@ -189,6 +189,10 @@ public class AccountSessionManager{
.execNoAuth(instance.uri);
}
public boolean isSelf(String id, Account other){
return getAccount(id).self.id.equals(other.id);
}
public Instance getAuthenticatingInstance(){
return authenticatingInstance;
}

View File

@ -0,0 +1,63 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class AccountTimelineFragment extends StatusListFragment{
private Account user;
private GetAccountStatuses.Filter filter;
public AccountTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_no_refresh);
}
public static AccountTimelineFragment newInstance(String accountID, Account profileAccount, GetAccountStatuses.Filter filter, boolean load){
AccountTimelineFragment f=new AccountTimelineFragment();
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
args.putString("filter", filter.toString());
if(!load)
args.putBoolean("noAutoLoad", true);
args.putBoolean("__is_tab", true);
f.setArguments(args);
return f;
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
filter=GetAccountStatuses.Filter.valueOf(getArguments().getString("filter"));
if(!getArguments().getBoolean("noAutoLoad"))
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
}
}

View File

@ -21,6 +21,7 @@ import org.parceler.Parcels;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
@ -106,7 +107,7 @@ public class HomeFragment extends AppKitFragment{
@Override
public boolean wantsLightStatusBar(){
return true;
return currentTab!=R.id.tab_profile;
}
@Override
@ -119,10 +120,15 @@ public class HomeFragment extends AppKitFragment{
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0);
homeTimelineFragment.onApplyWindowInsets(topOnlyInsets);
searchFragment.onApplyWindowInsets(topOnlyInsets);
notificationsFragment.onApplyWindowInsets(topOnlyInsets);
profileFragment.onApplyWindowInsets(topOnlyInsets);
}
private Fragment fragmentForTab(@IdRes int tab){
@ -147,5 +153,6 @@ public class HomeFragment extends AppKitFragment{
lf.loadData();
}
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
}
}

View File

@ -1,37 +1,387 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Matrix;
import android.graphics.Outline;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CoverImageView;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ProfileFragment extends StatusListFragment{
private Account user;
public class ProfileFragment extends LoaderFragment{
private ImageView avatar;
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel, postsCount, postsLabel;
private Button actionButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
private CoverOverlayGradientDrawable coverGradient=new CoverOverlayGradientDrawable();
private Matrix coverMatrix=new Matrix();
private float titleTransY;
private Account account;
private String accountID;
private Relationship relationship;
private int statusBarHeight;
public ProfileFragment(){
super(R.layout.loader_fragment_overlay_toolbar);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
setTitle("@"+user.acct);
if(!getArguments().getBoolean("noAutoLoad"))
accountID=getArguments().getString("account");
setHasOptionsMenu(true);
if(!getArguments().getBoolean("noAutoLoad", false))
loadData();
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count)
.setCallback(new SimpleCallback<>(this){
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View content=inflater.inflate(R.layout.fragment_profile, container, false);
avatar=content.findViewById(R.id.avatar);
cover=content.findViewById(R.id.cover);
avatarBorder=content.findViewById(R.id.avatar_border);
name=content.findViewById(R.id.name);
username=content.findViewById(R.id.username);
bio=content.findViewById(R.id.bio);
followersCount=content.findViewById(R.id.followers_count);
followersLabel=content.findViewById(R.id.followers_label);
followingCount=content.findViewById(R.id.following_count);
followingLabel=content.findViewById(R.id.following_label);
postsCount=content.findViewById(R.id.posts_count);
postsLabel=content.findViewById(R.id.posts_label);
actionButton=content.findViewById(R.id.profile_action_btn);
pager=content.findViewById(R.id.pager);
scrollView=content.findViewById(R.id.scroller);
tabbar=content.findViewById(R.id.tabbar);
refreshLayout=content.findViewById(R.id.refresh_layout);
avatar.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(25));
}
});
avatar.setClipToOutline(true);
pager.setOffscreenPageLimit(4);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
bindHeaderView();
dataLoaded();
loadRelationship();
}
scrollView.setScrollableChildSupplier(this::getScrollableRecyclerView);
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
Toolbar toolbar=getToolbar();
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-toolbar.getLayoutParams().height-statusBarHeight-V.dp(38);
coverGradient.setTopPadding(statusBarHeight+toolbar.getLayoutParams().height);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
};
sizeWrapper.addView(content);
tabbar.setTabTextColors(getResources().getColor(R.color.gray_500), getResources().getColor(R.color.gray_800));
tabbar.setTabTextSize(V.dp(16));
new TabLayoutMediator(tabbar, pager, new TabLayoutMediator.TabConfigurationStrategy(){
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.posts_and_replies;
case 2 -> R.string.media;
case 3 -> R.string.profile_about;
default -> throw new IllegalStateException();
});
}
}).attach();
cover.setForeground(coverGradient);
return sizeWrapper;
}
@Override
protected void doLoadData(){
}
@Override
public void onRefresh(){
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
updateToolbar();
// To avoid the callback triggering on first layout with position=0 before anything is instantiated
pager.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
pager.getViewTreeObserver().removeOnPreDrawListener(this);
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, !result.isEmpty());
public void onPageSelected(int position){
if(position==0)
return;
BaseRecyclerFragment<?> page=getFragmentForPage(position);
if(!page.loaded && !page.isDataLoading())
page.loadData();
}
});
return true;
}
});
scrollView.setOnScrollChangeListener(this::onScrollChanged);
titleTransY=getToolbar().getLayoutParams().height;
if(toolbarTitleView!=null){
toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY);
}
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
updateToolbar();
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
statusBarHeight=insets.getSystemWindowInsetTop();
((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=statusBarHeight;
refreshLayout.setProgressViewEndTarget(true, statusBarHeight+refreshLayout.getProgressCircleDiameter()+V.dp(24));
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
private void bindHeaderView(){
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(account.avatar, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(account.header, 1000, 1000));
name.setText(account.displayName);
username.setText('@'+account.acct);
bio.setText(HtmlParser.parse(account.note, account.emojis));
followersCount.setText(UiUtils.abbreviateNumber(account.followersCount));
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
postsCount.setText(UiUtils.abbreviateNumber(account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, account.followersCount));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, account.followingCount));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, account.statusesCount));
if(AccountSessionManager.getInstance().isSelf(accountID, account)){
actionButton.setText(R.string.edit_profile);
}else{
actionButton.setVisibility(View.GONE);
}
}
private void updateToolbar(){
getToolbar().setBackgroundColor(0);
if(toolbarTitleView!=null){
toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY);
}
}
@Override
public boolean wantsLightStatusBar(){
return false;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(relationship==null)
return;
inflater.inflate(R.menu.profile, menu);
menu.findItem(R.id.mention).setTitle(getString(R.string.mention_user, account.displayName));
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.displayName));
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.displayName));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.displayName));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.displayName));
String domain=account.getDomain();
if(domain!=null)
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, domain));
else
menu.findItem(R.id.block_domain).setVisible(false);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
}
return true;
}
@Override
protected int getToolbarResource(){
return R.layout.profile_toolbar;
}
private void loadRelationship(){
new GetAccountRelationships(Collections.singletonList(account.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationship=result.get(0);
invalidateOptionsMenu();
actionButton.setVisibility(View.VISIBLE);
actionButton.setText(relationship.following ? R.string.button_following : R.string.button_follow);
}
@Override
public void onError(ErrorResponse error){
}
})
.exec(accountID);
}
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
int topBarsH=getToolbar().getHeight()+statusBarHeight;
if(scrollY>avatar.getTop()-topBarsH){
float avaAlpha=Math.max(1f-((scrollY-(avatar.getTop()-topBarsH))/(float)V.dp(38)), 0f);
avatar.setAlpha(avaAlpha);
avatarBorder.setAlpha(avaAlpha);
}else{
avatar.setAlpha(1f);
avatarBorder.setAlpha(1f);
}
if(scrollY>cover.getHeight()-topBarsH){
cover.setTranslationY(scrollY-(cover.getHeight()-topBarsH));
cover.setTranslationZ(V.dp(10));
cover.setTransform(cover.getHeight()/2f-topBarsH/2f, 1f);
}else{
cover.setTranslationY(0f);
cover.setTranslationZ(0f);
cover.setTransform(scrollY/2f, 1f);
}
coverGradient.setTopOffset(scrollY);
cover.invalidate();
titleTransY=getToolbar().getHeight();
if(scrollY>name.getTop()-topBarsH){
titleTransY=Math.max(0f, titleTransY-(scrollY-(name.getTop()-topBarsH)));
}
if(toolbarTitleView!=null){
toolbarTitleView.setTranslationY(titleTransY);
toolbarSubtitleView.setTranslationY(titleTransY);
}
}
private BaseRecyclerFragment<?> getFragmentForPage(int page){
return switch(page){
case 0 -> postsFragment;
case 1 -> postsWithRepliesFragment;
case 2 -> mediaFragment;
default -> throw new IllegalStateException();
};
}
private RecyclerView getScrollableRecyclerView(){
return getFragmentForPage(pager.getCurrentItem()).getView().findViewById(R.id.list);
}
private class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
FrameLayout view=new FrameLayout(getActivity());
view.setId(View.generateViewId());
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new SimpleViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
Fragment fragment=switch(position){
case 0 -> postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
case 1 -> postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
case 2 -> mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
default -> throw new IllegalArgumentException();
};
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
}
@Override
public int getItemCount(){
return 3;
}
@Override
public int getItemViewType(int position){
return position;
}
}
private class SimpleViewHolder extends RecyclerView.ViewHolder{
public SimpleViewHolder(@NonNull View itemView){
super(itemView);
}
}
}

View File

@ -1,5 +1,7 @@
package org.joinmastodon.android.model;
import android.text.TextUtils;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
@ -147,6 +149,15 @@ public class Account extends BaseModel{
moved.postprocess();
}
public boolean isLocal(){
return !acct.contains("@");
}
public String getDomain(){
String[] parts=acct.split("@", 2);
return parts.length==1 ? null : parts[1];
}
@Override
public String toString(){
return "Account{"+

View File

@ -0,0 +1,39 @@
package org.joinmastodon.android.model;
import org.joinmastodon.android.api.RequiredField;
public class Relationship extends BaseModel{
@RequiredField
public String id;
public boolean following;
public boolean requested;
public boolean endorsed;
public boolean followedBy;
public boolean muting;
public boolean mutingNotifications;
public boolean showingReblogs;
public boolean notifying;
public boolean blocking;
public boolean domainBlocking;
public boolean blockedBy;
public String note;
@Override
public String toString(){
return "Relationship{"+
"id='"+id+'\''+
", following="+following+
", requested="+requested+
", endorsed="+endorsed+
", followedBy="+followedBy+
", muting="+muting+
", mutingNotifications="+mutingNotifications+
", showingReblogs="+showingReblogs+
", notifying="+notifying+
", blocking="+blocking+
", domainBlocking="+domainBlocking+
", blockedBy="+blockedBy+
", note='"+note+'\''+
'}';
}
}

View File

@ -5,7 +5,6 @@ import android.app.Fragment;
import android.graphics.Outline;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;

View File

@ -0,0 +1,58 @@
package org.joinmastodon.android.ui.drawables;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.V;
public class CoverOverlayGradientDrawable extends Drawable{
private LinearGradient gradient=new LinearGradient(0f, 0f, 0f, 100f, 0xB0000000, 0, Shader.TileMode.CLAMP);
private Matrix gradientMatrix=new Matrix();
private int topPadding, topOffset;
private Paint paint=new Paint();
public CoverOverlayGradientDrawable(){
paint.setShader(gradient);
}
@Override
public void draw(@NonNull Canvas canvas){
Rect bounds=getBounds();
gradientMatrix.setScale(1f, (bounds.height()-V.dp(40)-topPadding)/100f);
gradientMatrix.postTranslate(0, topPadding+topOffset);
gradient.setLocalMatrix(gradientMatrix);
canvas.drawRect(bounds, paint);
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
public void setTopPadding(int topPadding){
this.topPadding=topPadding;
}
public void setTopOffset(int topOffset){
this.topOffset=topOffset;
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joinmastodon.android.ui.tabs;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.V;
import static org.joinmastodon.android.ui.utils.UiUtils.lerp;
/**
* An implementation of {@link TabIndicatorInterpolator} that translates the left and right sides of
* a selected tab indicator independently to make the indicator grow and shrink between
* destinations.
*/
class ElasticTabIndicatorInterpolator extends TabIndicatorInterpolator {
/** Fit a linear 0F - 1F curve to an ease out sine (decelerating) curve. */
private static float decInterp(@FloatRange(from = 0.0, to = 1.0) float fraction) {
// Ease out sine
return (float) Math.sin((fraction * Math.PI) / 2.0);
}
/** Fit a linear 0F - 1F curve to an ease in sine (accelerating) curve. */
private static float accInterp(@FloatRange(from = 0.0, to = 1.0) float fraction) {
// Ease in sine
return (float) (1.0 - Math.cos((fraction * Math.PI) / 2.0));
}
@Override
void setIndicatorBoundsForOffset(
TabLayout tabLayout,
View startTitle,
View endTitle,
float offset,
@NonNull Drawable indicator) {
// The indicator should be positioned somewhere between start and end title. Override the
// super implementation and adjust the indicator's left and right bounds independently.
RectF startIndicator = calculateIndicatorWidthForTab(tabLayout, startTitle);
RectF endIndicator = calculateIndicatorWidthForTab(tabLayout, endTitle);
float leftFraction;
float rightFraction;
final boolean isMovingRight = startIndicator.left < endIndicator.left;
// If the selection indicator should grow and shrink during the animation, interpolate
// the left and right bounds of the indicator using separate easing functions.
// The side in which the indicator is moving should always be the accelerating
// side.
if (isMovingRight) {
leftFraction = accInterp(offset);
rightFraction = decInterp(offset);
} else {
leftFraction = decInterp(offset);
rightFraction = accInterp(offset);
}
indicator.setBounds(
lerp((int) startIndicator.left, (int) endIndicator.left, leftFraction),
indicator.getBounds().top,
lerp((int) startIndicator.right, (int) endIndicator.right, rightFraction),
indicator.getBounds().bottom);
}
}

View File

@ -0,0 +1,16 @@
package org.joinmastodon.android.ui.tabs;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
class MaterialResources{
public static Drawable getDrawable(Context context, TypedArray a, int attr){
return a.getDrawable(attr);
}
public static ColorStateList getColorStateList(Context context, TypedArray a, int attr){
return a.getColorStateList(attr);
}
}

View File

@ -0,0 +1,169 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joinmastodon.android.ui.tabs;
import static org.joinmastodon.android.ui.utils.UiUtils.lerp;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.annotation.Dimension;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.V;
/**
* A class used to manipulate the {@link SlidingTabIndicator}'s indicator {@link Drawable} at any
* point at or between tabs.
*
* <p>By default, this class will size the indicator according to {@link
* TabLayout#isTabIndicatorFullWidth()} and linearly move the indicator between tabs.
*
* <p>Subclasses can override {@link #setIndicatorBoundsForTab(TabLayout, View, Drawable)} and
* {@link #setIndicatorBoundsForOffset(TabLayout, View, View, float, Drawable)} (TabLayout, View,
* View, float, Drawable)} to define how the indicator should be drawn for a single tab or at any
* point between two tabs.
*
* <p>Additionally, subclasses can use the provided helpers {@link
* #calculateIndicatorWidthForTab(TabLayout, View)} and {@link
* #calculateTabViewContentBounds(TabView, int)} to capture the bounds of the tab or tab's content.
*/
class TabIndicatorInterpolator {
@Dimension(unit = Dimension.DP)
private static final int MIN_INDICATOR_WIDTH = 24;
/**
* A helper method that calculates the bounds of a {@link TabView}'s content.
*
* <p>For width, if only text label is present, calculates the width of the text label. If only
* icon is present, calculates the width of the icon. If both are present, the text label bounds
* take precedence. If both are present and inline mode is enabled, the sum of the bounds of the
* both the text label and icon are calculated. If neither are present or if the calculated
* difference between the left and right bounds is less than 24dp, then left and right bounds are
* adjusted such that the difference between them is equal to 24dp.
*
* <p>For height, this method calculates the combined height of the icon (if present) and label
* (if present).
*
* @param tabView {@link TabView} for which to calculate left and right content bounds.
* @param minWidth the min width between the returned RectF's left and right bounds. Useful if
* enforcing a min width of the indicator.
*/
static RectF calculateTabViewContentBounds(
@NonNull TabLayout.TabView tabView, @Dimension(unit = Dimension.DP) int minWidth) {
int tabViewContentWidth = tabView.getContentWidth();
int tabViewContentHeight = tabView.getContentHeight();
int minWidthPx = (int) V.dp(minWidth);
if (tabViewContentWidth < minWidthPx) {
tabViewContentWidth = minWidthPx;
}
int tabViewCenterX = (tabView.getLeft() + tabView.getRight()) / 2;
int tabViewCenterY = (tabView.getTop() + tabView.getBottom()) / 2;
int contentLeftBounds = tabViewCenterX - (tabViewContentWidth / 2);
int contentTopBounds = tabViewCenterY - (tabViewContentHeight / 2);
int contentRightBounds = tabViewCenterX + (tabViewContentWidth / 2);
int contentBottomBounds = tabViewCenterY + (tabViewCenterX / 2);
return new RectF(contentLeftBounds, contentTopBounds, contentRightBounds, contentBottomBounds);
}
/**
* A helper method to calculate the left and right bounds of an indicator when {@code tab} is
* selected.
*
* <p>This method accounts for {@link TabLayout#isTabIndicatorFullWidth()}'s value. If true, the
* returned left and right bounds will span the full width of {@code tab}. If false, the returned
* bounds will span the width of the {@code tab}'s content.
*
* @param tabLayout The tab's parent {@link TabLayout}
* @param tab The view of the tab under which the indicator will be positioned
* @return A {@link RectF} containing the left and right bounds that the indicator should span
* when {@code tab} is selected.
*/
static RectF calculateIndicatorWidthForTab(TabLayout tabLayout, @Nullable View tab) {
if (tab == null) {
return new RectF();
}
// If the indicator should fit to the tab's content, calculate the content's widtd
if (!tabLayout.isTabIndicatorFullWidth() && tab instanceof TabLayout.TabView) {
return calculateTabViewContentBounds((TabLayout.TabView) tab, MIN_INDICATOR_WIDTH);
}
// Return the entire width of the tab
return new RectF(tab.getLeft(), tab.getTop(), tab.getRight(), tab.getBottom());
}
/**
* Called whenever {@code indicator} should be drawn to show the given {@code tab} as selected.
*
* <p>This method should update the bounds of indicator to be correctly positioned to indicate
* {@code tab} as selected.
*
* @param tabLayout The {@link TabLayout} parent of the tab and indicator being drawn.
* @param tab The tab that should be marked as selected
* @param indicator The drawable to be drawn to indicate the selected tab. Update the drawable's
* bounds, color, etc to mark the given tab as selected.
*/
void setIndicatorBoundsForTab(TabLayout tabLayout, View tab, @NonNull Drawable indicator) {
RectF startIndicator = calculateIndicatorWidthForTab(tabLayout, tab);
indicator.setBounds(
(int) startIndicator.left,
indicator.getBounds().top,
(int) startIndicator.right,
indicator.getBounds().bottom);
}
/**
* Called whenever the {@code indicator} should be drawn between two destinations and the {@link
* Drawable}'s bounds should be changed. When {@code offset} is 0.0, the tab {@code indicator}
* should indicate that the {@code startTitle} tab is selected. When {@code offset} is 1.0, the
* tab {@code indicator} should indicate that the {@code endTitle} tab is selected. When offset is
* between 0.0 and 1.0, the {@code indicator} is moving between the startTitle and endTitle and
* the indicator should reflect this movement.
*
* <p>By default, this class will move the indicator linearly between tab destinations.
*
* @param tabLayout The TabLayout parent of the indicator being drawn.
* @param startTitle The title that should be indicated as selected when offset is 0.0.
* @param endTitle The title that should be indicated as selected when offset is 1.0.
* @param offset The fraction between startTitle and endTitle where the indicator is for a given
* frame
* @param indicator The drawable to be drawn to indicate the selected tab. Update the drawable's
* bounds, color, etc as {@code offset} changes to show the indicator in the correct position.
*/
void setIndicatorBoundsForOffset(
TabLayout tabLayout,
View startTitle,
View endTitle,
@FloatRange(from = 0.0, to = 1.0) float offset,
@NonNull Drawable indicator) {
RectF startIndicator = calculateIndicatorWidthForTab(tabLayout, startTitle);
// Linearly interpolate the indicator's position, using it's left and right bounds, between the
// two destinations.
RectF endIndicator = calculateIndicatorWidthForTab(tabLayout, endTitle);
indicator.setBounds(
lerp((int) startIndicator.left, (int) endIndicator.left, offset),
indicator.getBounds().top,
lerp((int) startIndicator.right, (int) endIndicator.right, offset),
indicator.getBounds().bottom);
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joinmastodon.android.ui.tabs;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import org.joinmastodon.android.R;
/**
* TabItem is a special 'view' which allows you to declare tab items for a {@link TabLayout} within
* a layout. This view is not actually added to TabLayout, it is just a dummy which allows setting
* of a tab items's text, icon and custom layout. See TabLayout for more information on how to use
* it.
*
* @attr ref com.google.android.material.R.styleable#TabItem_android_icon
* @attr ref com.google.android.material.R.styleable#TabItem_android_text
* @attr ref com.google.android.material.R.styleable#TabItem_android_layout
* @see TabLayout
*/
//TODO(b/76413401): make class final after the widget migration
public class TabItem extends View {
//TODO(b/76413401): make package private after the widget migration
public final CharSequence text;
//TODO(b/76413401): make package private after the widget migration
public final Drawable icon;
//TODO(b/76413401): make package private after the widget migration
public final int customLayout;
public TabItem(Context context) {
this(context, null);
}
public TabItem(Context context, AttributeSet attrs) {
super(context, attrs);
final TypedArray a =
context.obtainStyledAttributes(attrs, R.styleable.TabItem);
text = a.getText(R.styleable.TabItem_android_text);
icon = a.getDrawable(R.styleable.TabItem_android_icon);
customLayout = a.getResourceId(R.styleable.TabItem_android_layout, 0);
a.recycle();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,315 @@
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joinmastodon.android.ui.tabs;
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_DRAGGING;
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE;
import static androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_SETTLING;
import androidx.recyclerview.widget.RecyclerView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager2.widget.ViewPager2;
import java.lang.ref.WeakReference;
/**
* A mediator to link a TabLayout with a ViewPager2. The mediator will synchronize the ViewPager2's
* position with the selected tab when a tab is selected, and the TabLayout's scroll position when
* the user drags the ViewPager2. TabLayoutMediator will listen to ViewPager2's OnPageChangeCallback
* to adjust tab when ViewPager2 moves. TabLayoutMediator listens to TabLayout's
* OnTabSelectedListener to adjust VP2 when tab moves. TabLayoutMediator listens to RecyclerView's
* AdapterDataObserver to recreate tab content when dataset changes.
*
* <p>Establish the link by creating an instance of this class, make sure the ViewPager2 has an
* adapter and then call {@link #attach()} on it. Instantiating a TabLayoutMediator will only create
* the mediator object, {@link #attach()} will link the TabLayout and the ViewPager2 together. When
* creating an instance of this class, you must supply an implementation of {@link
* TabConfigurationStrategy} in which you set the text of the tab, and/or perform any styling of the
* tabs that you require. Changing ViewPager2's adapter will require a {@link #detach()} followed by
* {@link #attach()} call. Changing the ViewPager2 or TabLayout will require a new instantiation of
* TabLayoutMediator.
*/
public final class TabLayoutMediator {
@NonNull private final TabLayout tabLayout;
@NonNull private final ViewPager2 viewPager;
private final boolean autoRefresh;
private final boolean smoothScroll;
private final TabConfigurationStrategy tabConfigurationStrategy;
@Nullable private RecyclerView.Adapter<?> adapter;
private boolean attached;
@Nullable private TabLayoutOnPageChangeCallback onPageChangeCallback;
@Nullable private TabLayout.OnTabSelectedListener onTabSelectedListener;
@Nullable private RecyclerView.AdapterDataObserver pagerAdapterObserver;
/**
* A callback interface that must be implemented to set the text and styling of newly created
* tabs.
*/
public interface TabConfigurationStrategy {
/**
* Called to configure the tab for the page at the specified position. Typically calls {@link
* TabLayout.Tab#setText(CharSequence)}, but any form of styling can be applied.
*
* @param tab The Tab which should be configured to represent the title of the item at the given
* position in the data set.
* @param position The position of the item within the adapter's data set.
*/
void onConfigureTab(@NonNull TabLayout.Tab tab, int position);
}
public TabLayoutMediator(
@NonNull TabLayout tabLayout,
@NonNull ViewPager2 viewPager,
@NonNull TabConfigurationStrategy tabConfigurationStrategy) {
this(tabLayout, viewPager, /* autoRefresh= */ true, tabConfigurationStrategy);
}
public TabLayoutMediator(
@NonNull TabLayout tabLayout,
@NonNull ViewPager2 viewPager,
boolean autoRefresh,
@NonNull TabConfigurationStrategy tabConfigurationStrategy) {
this(tabLayout, viewPager, autoRefresh, /* smoothScroll= */ true, tabConfigurationStrategy);
}
public TabLayoutMediator(
@NonNull TabLayout tabLayout,
@NonNull ViewPager2 viewPager,
boolean autoRefresh,
boolean smoothScroll,
@NonNull TabConfigurationStrategy tabConfigurationStrategy) {
this.tabLayout = tabLayout;
this.viewPager = viewPager;
this.autoRefresh = autoRefresh;
this.smoothScroll = smoothScroll;
this.tabConfigurationStrategy = tabConfigurationStrategy;
}
/**
* Link the TabLayout and the ViewPager2 together. Must be called after ViewPager2 has an adapter
* set. To be called on a new instance of TabLayoutMediator or if the ViewPager2's adapter
* changes.
*
* @throws IllegalStateException If the mediator is already attached, or the ViewPager2 has no
* adapter.
*/
public void attach() {
if (attached) {
throw new IllegalStateException("TabLayoutMediator is already attached");
}
adapter = viewPager.getAdapter();
if (adapter == null) {
throw new IllegalStateException(
"TabLayoutMediator attached before ViewPager2 has an " + "adapter");
}
attached = true;
// Add our custom OnPageChangeCallback to the ViewPager
onPageChangeCallback = new TabLayoutOnPageChangeCallback(tabLayout);
viewPager.registerOnPageChangeCallback(onPageChangeCallback);
// Now we'll add a tab selected listener to set ViewPager's current item
onTabSelectedListener = new ViewPagerOnTabSelectedListener(viewPager, smoothScroll);
tabLayout.addOnTabSelectedListener(onTabSelectedListener);
// Now we'll populate ourselves from the pager adapter, adding an observer if
// autoRefresh is enabled
if (autoRefresh) {
// Register our observer on the new adapter
pagerAdapterObserver = new PagerAdapterObserver();
adapter.registerAdapterDataObserver(pagerAdapterObserver);
}
populateTabsFromPagerAdapter();
// Now update the scroll position to match the ViewPager's current item
tabLayout.setScrollPosition(viewPager.getCurrentItem(), 0f, true);
}
/**
* Unlink the TabLayout and the ViewPager. To be called on a stale TabLayoutMediator if a new one
* is instantiated, to prevent holding on to a view that should be garbage collected. Also to be
* called before {@link #attach()} when a ViewPager2's adapter is changed.
*/
public void detach() {
if (autoRefresh && adapter != null) {
adapter.unregisterAdapterDataObserver(pagerAdapterObserver);
pagerAdapterObserver = null;
}
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
viewPager.unregisterOnPageChangeCallback(onPageChangeCallback);
onTabSelectedListener = null;
onPageChangeCallback = null;
adapter = null;
attached = false;
}
/**
* Returns whether the {@link TabLayout} and the {@link ViewPager2} are linked together.
*/
public boolean isAttached() {
return attached;
}
@SuppressWarnings("WeakerAccess")
void populateTabsFromPagerAdapter() {
tabLayout.removeAllTabs();
if (adapter != null) {
int adapterCount = adapter.getItemCount();
for (int i = 0; i < adapterCount; i++) {
TabLayout.Tab tab = tabLayout.newTab();
tabConfigurationStrategy.onConfigureTab(tab, i);
tabLayout.addTab(tab, false);
}
// Make sure we reflect the currently set ViewPager item
if (adapterCount > 0) {
int lastItem = tabLayout.getTabCount() - 1;
int currItem = Math.min(viewPager.getCurrentItem(), lastItem);
if (currItem != tabLayout.getSelectedTabPosition()) {
tabLayout.selectTab(tabLayout.getTabAt(currItem));
}
}
}
}
/**
* A {@link ViewPager2.OnPageChangeCallback} class which contains the necessary calls back to the
* provided {@link TabLayout} so that the tab position is kept in sync.
*
* <p>This class stores the provided TabLayout weakly, meaning that you can use {@link
* ViewPager2#registerOnPageChangeCallback(ViewPager2.OnPageChangeCallback)} without removing the
* callback and not cause a leak.
*/
private static class TabLayoutOnPageChangeCallback extends ViewPager2.OnPageChangeCallback {
@NonNull private final WeakReference<TabLayout> tabLayoutRef;
private int previousScrollState;
private int scrollState;
TabLayoutOnPageChangeCallback(TabLayout tabLayout) {
tabLayoutRef = new WeakReference<>(tabLayout);
reset();
}
@Override
public void onPageScrollStateChanged(final int state) {
previousScrollState = scrollState;
scrollState = state;
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
TabLayout tabLayout = tabLayoutRef.get();
if (tabLayout != null) {
// Only update the text selection if we're not settling, or we are settling after
// being dragged
boolean updateText =
scrollState != SCROLL_STATE_SETTLING || previousScrollState == SCROLL_STATE_DRAGGING;
// Update the indicator if we're not settling after being idle. This is caused
// from a setCurrentItem() call and will be handled by an animation from
// onPageSelected() instead.
boolean updateIndicator =
!(scrollState == SCROLL_STATE_SETTLING && previousScrollState == SCROLL_STATE_IDLE);
tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
}
}
@Override
public void onPageSelected(final int position) {
TabLayout tabLayout = tabLayoutRef.get();
if (tabLayout != null
&& tabLayout.getSelectedTabPosition() != position
&& position < tabLayout.getTabCount()) {
// Select the tab, only updating the indicator if we're not being dragged/settled
// (since onPageScrolled will handle that).
boolean updateIndicator =
scrollState == SCROLL_STATE_IDLE
|| (scrollState == SCROLL_STATE_SETTLING
&& previousScrollState == SCROLL_STATE_IDLE);
tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
}
}
void reset() {
previousScrollState = scrollState = SCROLL_STATE_IDLE;
}
}
/**
* A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back to the
* provided {@link ViewPager2} so that the tab position is kept in sync.
*/
private static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener {
private final ViewPager2 viewPager;
private final boolean smoothScroll;
ViewPagerOnTabSelectedListener(ViewPager2 viewPager, boolean smoothScroll) {
this.viewPager = viewPager;
this.smoothScroll = smoothScroll;
}
@Override
public void onTabSelected(@NonNull TabLayout.Tab tab) {
viewPager.setCurrentItem(tab.getPosition(), smoothScroll);
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
// No-op
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
// No-op
}
}
private class PagerAdapterObserver extends RecyclerView.AdapterDataObserver {
PagerAdapterObserver() {}
@Override
public void onChanged() {
populateTabsFromPagerAdapter();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
populateTabsFromPagerAdapter();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
populateTabsFromPagerAdapter();
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
populateTabsFromPagerAdapter();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
populateTabsFromPagerAdapter();
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
populateTabsFromPagerAdapter();
}
}
}

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.ui.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
@ -43,6 +44,16 @@ public class UiUtils{
}
}
@SuppressLint("DefaultLocale")
public static String abbreviateNumber(int n){
if(n<1000)
return String.format("%,d", n);
else if(n<1_000_000)
return String.format("%,.1fK", n/1000f);
else
return String.format("%,.1fM", n/1_000_000f);
}
/**
* Android 6.0 has a bug where start and end compound drawables don't get tinted.
* This works around it by setting the tint colors directly to the drawables.
@ -64,4 +75,9 @@ public class UiUtils{
public static void runOnUiThread(Runnable runnable){
mainHandler.post(runnable);
}
/** Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}. */
public static int lerp(int startValue, int endValue, float fraction) {
return startValue + Math.round(fraction * (endValue - startValue));
}
}

View File

@ -0,0 +1,37 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.ImageView;
import androidx.annotation.Nullable;
public class CoverImageView extends ImageView{
private float imageTranslationY, imageScale=1f;
public CoverImageView(Context context){
super(context);
}
public CoverImageView(Context context, @Nullable AttributeSet attrs){
super(context, attrs);
}
public CoverImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas){
canvas.save();
canvas.translate(0, imageTranslationY);
super.onDraw(canvas);
canvas.restore();
}
public void setTransform(float transY, float scale){
imageTranslationY=transY;
imageScale=scale;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import java.util.function.Supplier;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class NestedRecyclerScrollView extends CustomScrollView{
private Supplier<RecyclerView> scrollableChildSupplier;
public NestedRecyclerScrollView(Context context){
super(context);
}
public NestedRecyclerScrollView(Context context, AttributeSet attrs){
super(context, attrs);
}
public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyleAttr){
super(context, attrs, defStyleAttr);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
final RecyclerView rv = (RecyclerView) target;
if ((dy < 0 && isScrolledToTop(rv)) || (dy > 0 && !isScrolledToBottom())) {
scrollBy(0, dy);
consumed[1] = dy;
return;
}
super.onNestedPreScroll(target, dx, dy, consumed);
}
@Override
public boolean onNestedPreFling(View target, float velX, float velY) {
final RecyclerView rv = (RecyclerView) target;
if ((velY < 0 && isScrolledToTop(rv)) || (velY > 0 && !isScrolledToBottom())) {
fling((int) velY);
return true;
}
return super.onNestedPreFling(target, velX, velY);
}
private boolean isScrolledToBottom() {
return !canScrollVertically(1);
}
private boolean isScrolledToTop(RecyclerView rv) {
final LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
return lm.findFirstVisibleItemPosition() == 0
&& lm.findViewByPosition(0).getTop() == 0;
}
public void setScrollableChildSupplier(Supplier<RecyclerView> scrollableChildSupplier){
this.scrollableChildSupplier=scrollableChildSupplier;
}
@Override
protected boolean onScrollingHitEdge(float velocity){
if(velocity>0){
RecyclerView view=scrollableChildSupplier.get();
if(view!=null){
return view.fling(0, (int)velocity);
}
}
return false;
}
}

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M9.277 16.221c0.293 0.293 0.293 0.768 0 1.06-0.293 0.294-0.768 0.293-1.061 0l-4.997-5.003c-0.292-0.293-0.292-0.768 0-1.06L8.217 6.22c0.293-0.293 0.768-0.293 1.06 0C9.57 6.513 9.57 6.987 9.278 7.28L5.557 11h7.842c1.595 0 2.81 0.242 3.889 0.764l0.246 0.126c1.109 0.593 1.983 1.467 2.576 2.576 0.61 1.14 0.89 2.418 0.89 4.135 0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75c0-1.484-0.228-2.52-0.713-3.428-0.453-0.847-1.113-1.507-1.96-1.96-0.838-0.448-1.786-0.676-3.094-0.709L13.4 12.5H5.562l3.715 3.721z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white"/>
<size android:height="2dp"/>
</shape>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:left="2dp"
android:right="2dp">
<shape
android:shape="rectangle">
<solid android:color="@android:color/white"/>
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="3dp"
android:topRightRadius="3dp"/>
<size android:height="3dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape
android:shape="rectangle">
<solid android:color="@android:color/white"/>
<size android:height="2dp"/>
</shape>
</item>
</selector>

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="27dp"/>
<stroke android:width="2dp" android:color="@color/gray_25"/>
</shape>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="12dp" android:right="12dp">
<shape
android:shape="rectangle">
<solid android:color="@android:color/white"/>
<size android:height="2dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:scaleType="centerInside"/>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"/>

View File

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.RecursiveSwipeRefreshLayout 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:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.joinmastodon.android.ui.views.NestedRecyclerScrollView
android:id="@+id/scroller"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="23dp"
android:clipToPadding="false">
<org.joinmastodon.android.ui.views.CoverImageView
android:id="@+id/cover"
android:layout_width="match_parent"
android:layout_height="229dp"
android:background="#808080"
android:scaleType="centerCrop"/>
<View
android:id="@+id/avatar_border"
android:layout_width="102dp"
android:layout_height="102dp"
android:layout_below="@id/cover"
android:layout_alignParentStart="true"
android:layout_marginTop="-40dp"
android:layout_marginStart="14dp"
android:background="@drawable/profile_ava_bg"/>
<ImageView
android:id="@+id/avatar"
android:layout_width="98dp"
android:layout_height="98dp"
android:layout_below="@id/cover"
android:layout_alignParentStart="true"
android:layout_marginStart="16dp"
android:layout_marginTop="-38dp"
tools:src="#0f0" />
<LinearLayout
android:id="@+id/following_btn"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_alignParentEnd="true"
android:layout_below="@id/cover"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:padding="4dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/following_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/following_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
<LinearLayout
android:id="@+id/followers_btn"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_toStartOf="@id/following_btn"
android:layout_below="@id/cover"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:padding="4dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/followers_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/followers_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
<LinearLayout
android:id="@+id/posts_btn"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_toStartOf="@id/followers_btn"
android:layout_below="@id/cover"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:padding="4dp"
android:orientation="vertical"
android:gravity="center_horizontal">
<TextView
android:id="@+id/posts_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_large"
tools:text="123"/>
<TextView
android:id="@+id/posts_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_title_small"
tools:text="following"/>
</LinearLayout>
<Button
android:id="@+id/profile_action_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@id/following_btn"
android:layout_margin="16dp"
tools:text="Edit Profile"/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/avatar"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/profile_action_btn"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_headline_small"
tools:text="Eugen"/>
<TextView
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_marginStart="16dp"
android:layout_toStartOf="@id/profile_action_btn"
android:textAppearance="@style/m3_title_medium"
android:textColor="@color/light_ui_action_button"
tools:text="\@Gargron"/>
<org.joinmastodon.android.ui.views.LinkedTextView
android:id="@+id/bio"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/username"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:textAppearance="@style/m3_body_large"
tools:text="Founder, CEO and lead developer @Mastodon, Germany." />
</RelativeLayout>
<org.joinmastodon.android.ui.tabs.TabLayout
android:id="@+id/tabbar"
android:layout_width="match_parent"
android:layout_height="38dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
app:tabPaddingStart="12dp"
app:tabPaddingEnd="12dp"
app:tabMinWidth="0dp"
app:tabIndicator="@drawable/tab_indicator_inset"
app:tabIndicatorAnimationMode="elastic"
app:tabMode="scrollable"
app:tabGravity="start"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.NestedRecyclerScrollView>
</me.grishka.appkit.views.RecursiveSwipeRefreshLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/appkit_loader_root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?android:windowBackground">
<FrameLayout
android:id="@+id/appkit_loader_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/profile_toolbar"/>
<include layout="@layout/loading"
android:id="@+id/loading"/>
<ViewStub android:layout="?errorViewLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/error"
android:visibility="gone"/>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/content_stub"/>
</FrameLayout>
</me.grishka.appkit.views.FragmentRootLinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:elevation="3dip"
android:popupTheme="?android:attr/actionBarPopupTheme"
android:subtitleTextAppearance="?android:attr/subtitleTextAppearance"
android:theme="@style/Theme.Mastodon.Toolbar.Profile"
android:titleTextAppearance="?android:attr/titleTextAppearance" />

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content_wrap"
android:layout_width="match_parent"
android:layout_height="match_parent">
<me.grishka.appkit.views.UsableRecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:clipToPadding="false"/>
<ViewStub android:layout="?emptyViewLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/empty"/>
</FrameLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/mention" android:title="@string/mention_user" android:icon="@drawable/ic_fluent_arrow_reply_24_regular" android:showAsAction="always"/>
<item android:id="@+id/share" android:title="@string/share_user"/>
<item android:id="@+id/mute" android:title="@string/mute_user"/>
<item android:id="@+id/block" android:title="@string/block_user"/>
<item android:id="@+id/report" android:title="@string/report_user"/>
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
</menu>

View File

@ -18,6 +18,7 @@
<color name="gray_500">#667085</color>
<color name="gray_800_alpha50">#80282C37</color>
<color name="light_ui_action_button">#606984</color>
<color name="text_primary">@color/gray_800</color>
<color name="text_secondary">@color/gray_500</color>

View File

@ -33,4 +33,37 @@
<string name="discard">Discard</string>
<string name="cancel">Cancel</string>
<string name="publishing">Your toot is being tooted</string>
<plurals name="followers">
<item quantity="one">follower</item>
<item quantity="other">followers</item>
</plurals>
<plurals name="following">
<item quantity="one">following</item>
<item quantity="other">following</item>
</plurals>
<plurals name="posts">
<item quantity="one">post</item>
<item quantity="other">posts</item>
</plurals>
<string name="posts">Posts</string>
<string name="posts_and_replies">Posts and Replies</string>
<string name="media">Media</string>
<string name="profile_about">About</string>
<string name="button_follow">Follow</string>
<string name="button_following">Following</string>
<string name="edit_profile">Edit Profile</string>
<string name="mention_user">Mention %s</string>
<string name="share_user">Share %s</string>
<string name="mute_user">Mute %s</string>
<string name="unmute_user">Unmute %s</string>
<string name="block_user">Block %s</string>
<string name="unblock_user">Unblock %s</string>
<string name="report_user">Report %s</string>
<string name="block_domain">Block %s</string>
<string name="unblock_domain">Unblock %s</string>
<plurals name="x_posts">
<item quantity="one">%,d post</item>
<item quantity="other">%,d posts</item>
</plurals>
</resources>

View File

@ -23,6 +23,13 @@
<item name="android:textColorSecondary">@color/gray_800</item>
</style>
<style name="Theme.Mastodon.Toolbar.Profile">
<item name="android:textColorPrimary">@color/gray_50</item>
<item name="android:textColorSecondary">@color/gray_50</item>
<item name="android:drawableTint">@color/gray_50</item>
<item name="android:popupTheme">@style/Theme.Mastodon</item>
</style>
<style name="Widget.Mastodon.Button" parent="android:Widget.Material.Button">
<item name="android:textAllCaps">false</item>
<item name="android:background">@drawable/bg_button</item>
@ -92,4 +99,15 @@
<item name="android:textColor">@color/text_secondary</item>
<item name="android:textSize">14dp</item>
</style>
<style name="m3_title_large">
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">22dp</item>
<item name="android:textColor">@color/text_primary</item>
</style>
<style name="m3_headline_small">
<item name="android:textSize">24dp</item>
<item name="android:textColor">@color/gray_800</item>
</style>
</resources>

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<!-- Style to use for TabLayout in the theme. -->
<attr name="tabStyle" format="reference"/>
<!-- Style to use for Secondary TabLayout in the theme. -->
<attr name="tabSecondaryStyle" format="reference"/>
<declare-styleable name="TabLayout">
<!-- Color of the indicator used to show the currently selected tab. -->
<attr name="tabIndicatorColor" format="color"/>
<!-- {@deprecated Instead, set the intrinsic size of the custom drawable provided to the
tabIndicator attribute in order to change the indicator height. For example, this can be
done by setting the <size> property in a <shape> resource.} -->
<attr name="tabIndicatorHeight" format="dimension"/>
<!-- Position in the Y axis from the starting edge that tabs should be positioned from. -->
<attr name="tabContentStart" format="dimension"/>
<!-- Reference to a background to be applied to tabs. -->
<attr name="tabBackground" format="reference"/>
<!-- Reference to a drawable to use as selection indicator for tabs. If this attribute is not
specified, indicator defaults to a line along the bottom of the tab. -->
<attr name="tabIndicator" format="reference"/>
<!-- Gravity constant for tab selection indicator. -->
<attr name="tabIndicatorGravity">
<!-- Align indicator to the bottom of this tab layout. -->
<enum name="bottom" value="0"/>
<!-- Align indicator along the center of this tab layout. -->
<enum name="center" value="1"/>
<!-- Align indicator to the top of this tab layout. -->
<enum name="top" value="2"/>
<!-- Stretch indicator to match the height and width of a tab item in this layout. -->
<enum name="stretch" value="3"/>
</attr>
<!-- Duration in milliseconds for the animation of the selection indicator from one tab item
to another. -->
<attr name="tabIndicatorAnimationDuration" format="integer"/>
<!-- Whether the selection indicator width should fill the full width of the tab item,
or if it should be fitted to the content of the tab text label. If no text label is
present, it will be set to the width of the icon or to a minimum width of 24dp. -->
<attr name="tabIndicatorFullWidth" format="boolean"/>
<!-- The animation mode used to animate the selection indicator between
destinations. -->
<attr name="tabIndicatorAnimationMode">
<!-- Animate the selection indicator's left and right bounds in step with
each other. -->
<enum name="linear" value="0"/>
<!-- Animate the selection indicator's left and right bounds out of step
with each other, decelerating the front and accelerating the back.
This causes the indicator to look like it stretches between destinations
an then shrinks back down to fit the size of it's target tab. -->
<enum name="elastic" value="1"/>
</attr>
<!-- The behavior mode for the Tabs in this layout -->
<attr name="tabMode">
<enum name="scrollable" value="0"/>
<enum name="fixed" value="1"/>
<enum name="auto" value="2"/>
</attr>
<!-- Gravity constant for tabs. -->
<attr name="tabGravity">
<enum name="fill" value="0"/>
<enum name="center" value="1"/>
<enum name="start" value="2"/>
</attr>
<!-- Whether to display tab labels horizontally inline with icons, or underneath icons. -->
<attr name="tabInlineLabel" format="boolean"/>
<!-- The minimum width for tabs. -->
<attr name="tabMinWidth" format="dimension"/>
<!-- The maximum width for tabs. -->
<attr name="tabMaxWidth" format="dimension"/>
<!-- A reference to a TextAppearance style to be applied to tabs. -->
<attr name="tabTextAppearance" format="reference"/>
<!-- The default text color to be applied to tabs. -->
<attr name="tabTextColor" format="color"/>
<!-- {@deprecated Instead, provide a ColorStateList to the tabTextColor attribute with a
selected color set.} -->
<attr name="tabSelectedTextColor" format="color"/>
<!-- The preferred padding along the start edge of tabs. -->
<attr name="tabPaddingStart" format="dimension"/>
<!-- The preferred padding along the top edge of tabs. -->
<attr name="tabPaddingTop" format="dimension"/>
<!-- The preferred padding along the end edge of tabs. -->
<attr name="tabPaddingEnd" format="dimension"/>
<!-- The preferred padding along the bottom edge of tabs. -->
<attr name="tabPaddingBottom" format="dimension"/>
<!-- The preferred padding along all edges of tabs. -->
<attr name="tabPadding" format="dimension"/>
<!-- Tint to apply to tab icons, if present. This can be a color state list or a color. -->
<attr name="tabIconTint" format="color"/>
<!-- Blending mode to apply to tab icons. -->
<attr name="tabIconTintMode">
<enum name="src_over" value="3"/>
<enum name="src_in" value="5"/>
<enum name="src_atop" value="9"/>
<enum name="multiply" value="14"/>
<enum name="screen" value="15"/>
<enum name="add" value="16"/>
</attr>
<!-- Ripple color for the tabs. This may be a color state list, if the desired ripple color
should be stateful.-->
<attr name="tabRippleColor" format="color"/>
<!-- Whether to use unbounded ripple effect for tabs, or if ripple should instead be bound to
tab item bounds. -->
<attr name="tabUnboundedRipple" format="boolean"/>
</declare-styleable>
<declare-styleable name="TabItem">
<!-- Text to display in the tab. -->
<attr name="android:text"/>
<!-- Icon to display in the tab. -->
<attr name="android:icon"/>
<!-- A reference to a layout resource to be displayed in the tab. -->
<attr name="android:layout"/>
</declare-styleable>
</resources>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<dimen name="design_tab_scrollable_min_width">72dp</dimen>
<dimen name="design_tab_max_width">264dp</dimen>
<dimen name="design_tab_text_size">14sp</dimen>
<dimen name="design_tab_text_size_2line">12sp</dimen>
</resources>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<integer name="design_tab_indicator_anim_duration_ms">300</integer>
<integer name="mtrl_tab_indicator_anim_duration_ms">250</integer>
</resources>

View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources>
<style name="Widget.Design.TabLayout" parent="Base.Widget.Design.TabLayout">
<item name="tabGravity">fill</item>
<item name="tabMode">fixed</item>
<item name="tabIndicatorFullWidth">true</item>
</style>
<style name="Base.Widget.Design.TabLayout" parent="android:Widget">
<item name="android:background">@null</item>
<item name="tabIconTint">@null</item>
<item name="tabMaxWidth">@dimen/design_tab_max_width</item>
<item name="tabIndicatorAnimationDuration">@integer/design_tab_indicator_anim_duration_ms</item>
<item name="tabIndicatorColor">?android:attr/colorAccent</item>
<item name="tabIndicatorGravity">bottom</item>
<item name="tabIndicatorAnimationMode">linear</item>
<item name="tabIndicator">@drawable/mtrl_tabs_default_indicator</item>
<item name="tabPaddingStart">12dp</item>
<item name="tabPaddingEnd">12dp</item>
<item name="tabTextAppearance">@style/TextAppearance.Design.Tab</item>
<item name="tabTextColor">@null</item>
<item name="tabRippleColor">?android:attr/colorControlHighlight</item>
<item name="tabUnboundedRipple">false</item>
</style>
<style name="TextAppearance.Design.Tab" parent="android:TextAppearance.Material.Button">
<item name="android:textSize">@dimen/design_tab_text_size</item>
<!-- <item name="android:textColor">@color/mtrl_tabs_legacy_text_color_selector</item>-->
<item name="android:textAllCaps">false</item>
</style>
<style name="Widget.MaterialComponents.TabLayout" parent="Widget.Design.TabLayout">
<!-- <item name="enforceMaterialTheme">true</item>-->
<!-- <item name="enforceTextAppearance">true</item>-->
<!-- <item name="android:background">?android:attr/colorSurface</item>-->
<!-- <item name="tabIconTint">@color/mtrl_tabs_icon_color_selector</item>-->
<item name="tabIndicatorAnimationDuration">@integer/mtrl_tab_indicator_anim_duration_ms</item>
<item name="tabIndicatorColor">?android:attr/colorPrimary</item>
<item name="tabTextAppearance">?android:attr/textAppearanceButton</item>
<!-- <item name="tabTextColor">@color/mtrl_tabs_icon_color_selector</item>-->
<!-- <item name="tabRippleColor">@color/mtrl_tabs_ripple_color</item>-->
<item name="tabUnboundedRipple">true</item>
</style>
<style name="Widget.MaterialComponents.TabLayout.Colored">
<item name="android:background">?android:attr/colorPrimary</item>
<!-- <item name="tabIconTint">@color/mtrl_tabs_icon_color_selector_colored</item>-->
<item name="tabIndicatorColor">?android:attr/colorAccent</item>
<!-- <item name="tabTextColor">@color/mtrl_tabs_icon_color_selector_colored</item>-->
<!-- <item name="tabRippleColor">@color/mtrl_tabs_colored_ripple_color</item>-->
</style>
<style name="Widget.MaterialComponents.TabLayout.PrimarySurface" parent="Widget.MaterialComponents.TabLayout.Colored"/>
<!-- Styles for M3 Tabs -->
<style name="Base.Widget.Material3.TabLayout" parent="Widget.MaterialComponents.TabLayout">
<!-- <item name="enforceTextAppearance">false</item>-->
<!-- <item name="tabIconTint">@color/m3_tabs_icon_color</item>-->
<!-- <item name="tabTextAppearance">?android:attr/textAppearanceLabelLarge</item>-->
<!-- <item name="tabTextColor">@color/m3_tabs_icon_color</item>-->
<item name="tabIndicator">@drawable/m3_tabs_rounded_line_indicator</item>
<item name="tabIndicatorAnimationMode">elastic</item>
<item name="tabIndicatorColor">?android:attr/colorPrimary</item>
<!-- <item name="tabRippleColor">@color/m3_tabs_ripple_color</item>-->
<item name="tabIndicatorFullWidth">false</item>
</style>
<style name="Widget.Material3.TabLayout" parent="Base.Widget.Material3.TabLayout"/>
<!-- Styles for M3 Tabs used on an elevatable surface. -->
<style name="Base.Widget.Material3.TabLayout.OnSurface" parent="Widget.Material3.TabLayout">
<item name="android:background">@android:color/transparent</item>
</style>
<style name="Widget.Material3.TabLayout.OnSurface" parent="Base.Widget.Material3.TabLayout.OnSurface"/>
<!-- Style for M3 secondary tabs, which are used as an alternate when primary tabs are already
present in the UI. This style does not have a bottom divider, which is added in v21 because
the drawable cannot use theme colors pre-21. -->
<style name="Base.Widget.Material3.TabLayout.Secondary" parent="Widget.Material3.TabLayout">
<item name="tabIndicator">@drawable/m3_tabs_line_indicator</item>
<item name="tabIndicatorFullWidth">true</item>
</style>
<style name="Widget.Material3.TabLayout.Secondary" parent="Base.Widget.Material3.TabLayout.Secondary"/>
</resources>