Profiles
This commit is contained in:
parent
cc06715aa6
commit
aa193b8921
|
@ -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'
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{"+
|
||||
|
|
|
@ -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+'\''+
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"/>
|
|
@ -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"/>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue