@@ -47,7 +47,7 @@ public class SearchViewHelper{
|
||||
searchEdit.setBackground(null);
|
||||
searchEdit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
searchEdit.removeCallbacks(debouncer);
|
||||
searchEdit.postDelayed(debouncer, 300);
|
||||
searchEdit.postDelayed(debouncer, 500);
|
||||
boolean newIsEmpty=e.length()==0;
|
||||
if(isEmpty!=newIsEmpty){
|
||||
isEmpty=newIsEmpty;
|
||||
|
||||
@@ -3,9 +3,12 @@ package org.joinmastodon.android.ui.adapters;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.viewholders.AvatarPileListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckboxOrRadioListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.OptionsListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SwitchListItemViewHolder;
|
||||
|
||||
@@ -13,11 +16,21 @@ import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemViewHolder<?>>{
|
||||
public class GenericListItemsAdapter<T> extends UsableRecyclerView.Adapter<ListItemViewHolder<?>> implements ImageLoaderRecyclerAdapter{
|
||||
private List<ListItem<T>> items;
|
||||
|
||||
public GenericListItemsAdapter(List<ListItem<T>> items){
|
||||
super(null);
|
||||
this.items=items;
|
||||
}
|
||||
|
||||
public GenericListItemsAdapter(ListImageLoaderWrapper imgLoader, List<ListItem<T>> items){
|
||||
super(imgLoader);
|
||||
this.items=items;
|
||||
}
|
||||
|
||||
@@ -32,6 +45,10 @@ public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemVie
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, false);
|
||||
if(viewType==R.id.list_item_radio)
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, true);
|
||||
if(viewType==R.id.list_item_options)
|
||||
return new OptionsListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_avatar_pile)
|
||||
return new AvatarPileListItemViewHolder(parent.getContext(), parent);
|
||||
|
||||
throw new IllegalArgumentException("Unexpected view type "+viewType);
|
||||
}
|
||||
@@ -51,4 +68,20 @@ public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemVie
|
||||
public int getItemViewType(int position){
|
||||
return items.get(position).getItemViewType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
ListItem<?> item=items.get(position);
|
||||
if(item instanceof AvatarPileListItem<?> avatarPileListItem)
|
||||
return avatarPileListItem.avatars.size();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
ListItem<?> item=items.get(position);
|
||||
if(item instanceof AvatarPileListItem<?> avatarPileListItem)
|
||||
return avatarPileListItem.avatars.get(image);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetStatusSourceText;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
@@ -198,6 +199,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
UiUtils.openSystemShareSheet(activity, item.status.url);
|
||||
}else if(id==R.id.translate){
|
||||
item.parentFragment.togglePostTranslation(item.status, item.parentID);
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(activity, AddAccountToListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -326,6 +332,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
report.setTitle(item.parentFragment.getString(R.string.report_user, account.displayName));
|
||||
follow.setTitle(item.parentFragment.getString(relationship!=null && relationship.following ? R.string.unfollow_user : R.string.follow_user, account.displayName));
|
||||
}
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship!=null && relationship.following);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.joinmastodon.android.ui.utils;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.IntEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.IntSupplier;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
|
||||
public class ActionModeHelper{
|
||||
public static ActionMode startActionMode(AppKitFragment fragment, IntSupplier statusBarColorSupplier, ActionMode.Callback callback){
|
||||
FragmentStackActivity activity=(FragmentStackActivity) fragment.getActivity();
|
||||
return activity.startActionMode(new ActionMode.Callback(){
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||
if(!callback.onCreateActionMode(mode, menu))
|
||||
return false;
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(activity.getWindow(), "statusBarColor", statusBarColorSupplier.getAsInt(), UiUtils.getThemeColor(activity, R.attr.colorM3Primary));
|
||||
anim.setEvaluator(new IntEvaluator(){
|
||||
@Override
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
|
||||
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
activity.invalidateSystemBarColors(fragment);
|
||||
View fakeView=new View(activity);
|
||||
// mode.setCustomView(fakeView);
|
||||
// int buttonID=activity.getResources().getIdentifier("action_mode_close_button", "id", "android");
|
||||
// View btn=activity.getWindow().getDecorView().findViewById(buttonID);
|
||||
// if(btn!=null){
|
||||
// ((ViewGroup.MarginLayoutParams)btn.getLayoutParams()).setMarginEnd(0);
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||
if(!callback.onPrepareActionMode(mode, menu))
|
||||
return false;
|
||||
for(int i=0;i<menu.size();i++){
|
||||
Drawable icon=menu.getItem(i).getIcon();
|
||||
if(icon!=null){
|
||||
icon=icon.mutate();
|
||||
icon.setTint(UiUtils.getThemeColor(activity, R.attr.colorM3OnPrimary));
|
||||
menu.getItem(i).setIcon(icon);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||
return callback.onActionItemClicked(mode, item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(activity.getWindow(), "statusBarColor", UiUtils.getThemeColor(activity, R.attr.colorM3Primary), statusBarColorSupplier.getAsInt());
|
||||
anim.setEvaluator(new IntEvaluator(){
|
||||
@Override
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
|
||||
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
|
||||
}
|
||||
});
|
||||
anim.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
activity.getWindow().setStatusBarColor(0);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
activity.invalidateSystemBarColors(fragment);
|
||||
callback.onDestroyActionMode(mode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class DropdownSubmenuController{
|
||||
protected List<Item<?>> items;
|
||||
protected LinearLayout contentView;
|
||||
protected UsableRecyclerView list;
|
||||
protected TextView backItem;
|
||||
protected final ToolbarDropdownMenuController dropdownController;
|
||||
protected MergeRecyclerAdapter mergeAdapter;
|
||||
protected ItemsAdapter itemsAdapter;
|
||||
|
||||
public DropdownSubmenuController(ToolbarDropdownMenuController dropdownController){
|
||||
this.dropdownController=dropdownController;
|
||||
}
|
||||
|
||||
protected abstract CharSequence getBackItemTitle();
|
||||
public void onDismiss(){}
|
||||
|
||||
protected void createView(){
|
||||
contentView=new LinearLayout(dropdownController.getActivity());
|
||||
contentView.setOrientation(LinearLayout.VERTICAL);
|
||||
CharSequence backTitle=getBackItemTitle();
|
||||
if(!TextUtils.isEmpty(backTitle)){
|
||||
backItem=(TextView) dropdownController.getActivity().getLayoutInflater().inflate(R.layout.item_dropdown_menu, contentView, false);
|
||||
((LinearLayout.LayoutParams) backItem.getLayoutParams()).topMargin=V.dp(8);
|
||||
backItem.setText(backTitle);
|
||||
backItem.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_arrow_back, 0, 0, 0);
|
||||
backItem.setBackground(UiUtils.getThemeDrawable(dropdownController.getActivity(), android.R.attr.selectableItemBackground));
|
||||
backItem.setOnClickListener(v->dropdownController.popSubmenuController());
|
||||
backItem.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setText(info.getText()+". "+host.getResources().getString(R.string.back));
|
||||
}
|
||||
});
|
||||
contentView.addView(backItem);
|
||||
}
|
||||
list=new UsableRecyclerView(dropdownController.getActivity());
|
||||
list.setLayoutManager(new LinearLayoutManager(dropdownController.getActivity()));
|
||||
itemsAdapter=new ItemsAdapter();
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(itemsAdapter);
|
||||
list.setAdapter(mergeAdapter);
|
||||
list.setPadding(0, backItem!=null ? 0 : V.dp(8), 0, V.dp(8));
|
||||
list.setClipToPadding(false);
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private final Paint paint=new Paint();
|
||||
{
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(V.dp(1));
|
||||
paint.setColor(UiUtils.getThemeColor(dropdownController.getActivity(), R.attr.colorM3OutlineVariant));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View view=parent.getChildAt(i);
|
||||
if(parent.getChildViewHolder(view) instanceof ItemHolder ih && ih.getItem().dividerBefore){
|
||||
paint.setAlpha(Math.round(view.getAlpha()*255));
|
||||
float y=view.getTop()-V.dp(8)-paint.getStrokeWidth()/2f;
|
||||
c.drawLine(0, y, parent.getWidth(), y, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
if(parent.getChildViewHolder(view) instanceof ItemHolder ih && ih.getItem().dividerBefore){
|
||||
outRect.top=V.dp(17);
|
||||
}
|
||||
}
|
||||
});
|
||||
contentView.addView(list, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
|
||||
public View getView(){
|
||||
if(contentView==null)
|
||||
createView();
|
||||
return contentView;
|
||||
}
|
||||
|
||||
protected final class Item<T>{
|
||||
public final String title;
|
||||
public final boolean hasSubmenu;
|
||||
public final boolean dividerBefore;
|
||||
public final T parentObject;
|
||||
public final Consumer<Item<T>> onClick;
|
||||
|
||||
public Item(String title, boolean hasSubmenu, boolean dividerBefore, T parentObject, Consumer<Item<T>> onClick){
|
||||
this.title=title;
|
||||
this.hasSubmenu=hasSubmenu;
|
||||
this.dividerBefore=dividerBefore;
|
||||
this.parentObject=parentObject;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
public Item(String title, boolean hasSubmenu, boolean dividerBefore, Consumer<Item<T>> onClick){
|
||||
this(title, hasSubmenu, dividerBefore, null, onClick);
|
||||
}
|
||||
|
||||
public Item(@StringRes int titleRes, boolean hasSubmenu, boolean dividerBefore, Consumer<Item<T>> onClick){
|
||||
this(dropdownController.getActivity().getString(titleRes), hasSubmenu, dividerBefore, null, onClick);
|
||||
}
|
||||
|
||||
private void performClick(){
|
||||
onClick.accept(this);
|
||||
}
|
||||
}
|
||||
|
||||
protected class ItemsAdapter extends RecyclerView.Adapter<ItemHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemHolder holder, int position){
|
||||
holder.bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemHolder extends BindableViewHolder<Item<?>> implements UsableRecyclerView.Clickable{
|
||||
private final TextView text;
|
||||
|
||||
public ItemHolder(){
|
||||
super(dropdownController.getActivity(), R.layout.item_dropdown_menu, list);
|
||||
text=(TextView) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Item<?> item){
|
||||
text.setText(item.title);
|
||||
text.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, item.hasSubmenu ? R.drawable.ic_arrow_right_24px : 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.performClick();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
|
||||
import org.joinmastodon.android.fragments.ManageFollowedHashtagsFragment;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class HomeTimelineHashtagsMenuController extends DropdownSubmenuController{
|
||||
private HideableSingleViewRecyclerAdapter largeProgressAdapter;
|
||||
private APIRequest<?> currentRequest;
|
||||
|
||||
public HomeTimelineHashtagsMenuController(ToolbarDropdownMenuController dropdownController){
|
||||
super(dropdownController);
|
||||
items=new ArrayList<>();
|
||||
loadHashtags();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createView(){
|
||||
super.createView();
|
||||
FrameLayout largeProgressView=new FrameLayout(dropdownController.getActivity());
|
||||
int pad=V.dp(32);
|
||||
largeProgressView.setPadding(0, pad, 0, pad);
|
||||
largeProgressView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
ProgressBar progress=new ProgressBar(dropdownController.getActivity());
|
||||
largeProgressView.addView(progress, new FrameLayout.LayoutParams(V.dp(48), V.dp(48), Gravity.CENTER));
|
||||
largeProgressAdapter=new HideableSingleViewRecyclerAdapter(largeProgressView);
|
||||
mergeAdapter.addAdapter(0, largeProgressAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return dropdownController.getActivity().getString(R.string.followed_hashtags);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(){
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
}
|
||||
|
||||
private void onTagClick(Item<Hashtag> item){
|
||||
dropdownController.dismiss();
|
||||
UiUtils.openHashtagTimeline(dropdownController.getActivity(), dropdownController.getAccountID(), item.parentObject);
|
||||
}
|
||||
|
||||
private void onManageTagsClick(){
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
Nav.go(dropdownController.getActivity(), ManageFollowedHashtagsFragment.class, args);
|
||||
}
|
||||
|
||||
private void loadHashtags(){
|
||||
currentRequest=new GetFollowedTags(null, 200)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
currentRequest=null;
|
||||
dropdownController.resizeOnNextFrame();
|
||||
largeProgressAdapter.setVisible(false);
|
||||
((List<Hashtag>) result).sort(Comparator.comparing(tag->tag.name));
|
||||
int prevSize=items.size();
|
||||
for(Hashtag tag:result){
|
||||
items.add(new Item<>("#"+tag.name, false, false, tag, HomeTimelineHashtagsMenuController.this::onTagClick));
|
||||
}
|
||||
items.add(new Item<Void>(R.string.manage_hashtags, false, true, i->onManageTagsClick()));
|
||||
itemsAdapter.notifyItemRangeInserted(prevSize, result.size()+1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
Activity activity=dropdownController.getActivity();
|
||||
if(activity!=null)
|
||||
error.showToast(activity);
|
||||
dropdownController.popSubmenuController();
|
||||
|
||||
}
|
||||
})
|
||||
.exec(dropdownController.getAccountID());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.ManageListsFragment;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class HomeTimelineListsMenuController extends DropdownSubmenuController{
|
||||
private final List<FollowList> lists;
|
||||
private final HomeTimelineMenuController.Callback callback;
|
||||
|
||||
public HomeTimelineListsMenuController(ToolbarDropdownMenuController dropdownController, HomeTimelineMenuController.Callback callback){
|
||||
super(dropdownController);
|
||||
this.lists=new ArrayList<>(callback.getLists());
|
||||
this.callback=callback;
|
||||
items=new ArrayList<>();
|
||||
for(FollowList l:lists){
|
||||
items.add(new Item<>(l.title, false, false, l, this::onListSelected));
|
||||
}
|
||||
items.add(new Item<Void>(dropdownController.getActivity().getString(R.string.manage_lists), false, true, i->{
|
||||
dropdownController.dismiss();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", dropdownController.getAccountID());
|
||||
Nav.go(dropdownController.getActivity(), ManageListsFragment.class, args);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return dropdownController.getActivity().getString(R.string.lists);
|
||||
}
|
||||
|
||||
private void onListSelected(Item<FollowList> item){
|
||||
callback.onListSelected(item.parentObject);
|
||||
dropdownController.dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HomeTimelineMenuController extends DropdownSubmenuController{
|
||||
private Callback callback;
|
||||
|
||||
public HomeTimelineMenuController(ToolbarDropdownMenuController dropdownController, Callback callback){
|
||||
super(dropdownController);
|
||||
this.callback=callback;
|
||||
items=List.of(
|
||||
new Item<Void>(R.string.timeline_following, false, false, i->{
|
||||
callback.onFollowingSelected();
|
||||
dropdownController.dismiss();
|
||||
}),
|
||||
new Item<Void>(R.string.local_timeline, false, false, i->{
|
||||
callback.onLocalSelected();
|
||||
dropdownController.dismiss();
|
||||
}),
|
||||
new Item<Void>(R.string.lists, true, true, i->dropdownController.pushSubmenuController(new HomeTimelineListsMenuController(dropdownController, callback))),
|
||||
new Item<Void>(R.string.followed_hashtags, true, false, i->dropdownController.pushSubmenuController(new HomeTimelineHashtagsMenuController(dropdownController)))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CharSequence getBackItemTitle(){
|
||||
return null;
|
||||
}
|
||||
|
||||
public interface Callback{
|
||||
void onFollowingSelected();
|
||||
void onLocalSelected();
|
||||
List<FollowList> getLists();
|
||||
void onListSelected(FollowList list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package org.joinmastodon.android.ui.viewcontrollers;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ToolbarDropdownMenuController{
|
||||
private final HostFragment fragment;
|
||||
private FrameLayout windowView;
|
||||
private FrameLayout menuContainer;
|
||||
private boolean dismissing;
|
||||
private List<DropdownSubmenuController> controllerStack=new ArrayList<>();
|
||||
private Animator currentTransition;
|
||||
|
||||
public ToolbarDropdownMenuController(HostFragment fragment){
|
||||
this.fragment=fragment;
|
||||
}
|
||||
|
||||
public void show(DropdownSubmenuController initialSubmenu){
|
||||
if(windowView!=null)
|
||||
return;
|
||||
|
||||
menuContainer=new FrameLayout(fragment.getActivity());
|
||||
menuContainer.setBackgroundResource(R.drawable.bg_m3_surface2);
|
||||
menuContainer.setOutlineProvider(OutlineProviders.roundedRect(4));
|
||||
menuContainer.setClipToOutline(true);
|
||||
menuContainer.setElevation(V.dp(6));
|
||||
View menuView=initialSubmenu.getView();
|
||||
menuView.setVisibility(View.VISIBLE);
|
||||
menuContainer.addView(menuView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
windowView=new WindowView(fragment.getActivity());
|
||||
int pad=V.dp(16);
|
||||
windowView.setPadding(pad, fragment.getToolbar().getHeight(), pad, pad);
|
||||
windowView.setClipToPadding(false);
|
||||
windowView.addView(menuContainer, new FrameLayout.LayoutParams(V.dp(200), ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP | Gravity.START));
|
||||
|
||||
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL);
|
||||
wlp.format=PixelFormat.TRANSLUCENT;
|
||||
wlp.token=fragment.getActivity().getWindow().getDecorView().getWindowToken();
|
||||
wlp.width=wlp.height=ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_ATTACHED_IN_DECOR;
|
||||
wlp.setTitle(fragment.getActivity().getString(R.string.dropdown_menu));
|
||||
fragment.getActivity().getWindowManager().addView(windowView, wlp);
|
||||
|
||||
menuContainer.setPivotX(V.dp(100));
|
||||
menuContainer.setPivotY(0);
|
||||
menuContainer.setScaleX(.8f);
|
||||
menuContainer.setScaleY(.8f);
|
||||
menuContainer.setAlpha(0f);
|
||||
menuContainer.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.alpha(1f)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.setDuration(150)
|
||||
.withLayer()
|
||||
.start();
|
||||
controllerStack.add(initialSubmenu);
|
||||
}
|
||||
|
||||
public void dismiss(){
|
||||
if(windowView==null || dismissing)
|
||||
return;
|
||||
dismissing=true;
|
||||
fragment.onDropdownWillDismiss();
|
||||
menuContainer.animate()
|
||||
.scaleX(.8f)
|
||||
.scaleY(.8f)
|
||||
.alpha(0f)
|
||||
.setInterpolator(CubicBezierInterpolator.DEFAULT)
|
||||
.setDuration(150)
|
||||
.withLayer()
|
||||
.withEndAction(()->{
|
||||
controllerStack.clear();
|
||||
fragment.getActivity().getWindowManager().removeView(windowView);
|
||||
menuContainer.removeAllViews();
|
||||
dismissing=false;
|
||||
windowView=null;
|
||||
menuContainer=null;
|
||||
fragment.onDropdownDismissed();
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
public void pushSubmenuController(DropdownSubmenuController controller){
|
||||
View prevView=menuContainer.getChildAt(menuContainer.getChildCount()-1);
|
||||
View newView=controller.getView();
|
||||
newView.setVisibility(View.VISIBLE);
|
||||
menuContainer.addView(newView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
controllerStack.add(controller);
|
||||
animateTransition(prevView, newView, true);
|
||||
}
|
||||
|
||||
public void popSubmenuController(){
|
||||
if(menuContainer.getChildCount()<=1)
|
||||
throw new IllegalStateException();
|
||||
DropdownSubmenuController controller=controllerStack.remove(controllerStack.size()-1);
|
||||
controller.onDismiss();
|
||||
View top=menuContainer.getChildAt(menuContainer.getChildCount()-1);
|
||||
View prev=menuContainer.getChildAt(menuContainer.getChildCount()-2);
|
||||
prev.setVisibility(View.VISIBLE);
|
||||
animateTransition(prev, top, false);
|
||||
}
|
||||
|
||||
private void animateTransition(View bottomView, View topView, boolean adding){
|
||||
if(currentTransition!=null)
|
||||
currentTransition.cancel();
|
||||
int origBottom=menuContainer.getBottom();
|
||||
menuContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
private final Rect tmpRect=new Rect();
|
||||
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
menuContainer.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
ObjectAnimator slideIn;
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(menuContainer, "bottom", origBottom, menuContainer.getTop()+(adding ? topView : bottomView).getHeight()),
|
||||
slideIn=ObjectAnimator.ofFloat(topView, View.TRANSLATION_X, adding ? menuContainer.getWidth() : 0, adding ? 0 : menuContainer.getWidth()),
|
||||
ObjectAnimator.ofFloat(bottomView, View.TRANSLATION_X, adding ? 0 : -menuContainer.getWidth()/4f, adding ? -menuContainer.getWidth()/4f : 0),
|
||||
ObjectAnimator.ofFloat(bottomView, View.ALPHA, adding ? 1f : 0f, adding ? 0f : 1f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
bottomView.setClipBounds(null);
|
||||
bottomView.setTranslationX(0);
|
||||
bottomView.setAlpha(1f);
|
||||
topView.setTranslationX(0);
|
||||
topView.setAlpha(1f);
|
||||
if(adding){
|
||||
bottomView.setVisibility(View.GONE);
|
||||
}else{
|
||||
menuContainer.removeView(topView);
|
||||
}
|
||||
currentTransition=null;
|
||||
}
|
||||
});
|
||||
slideIn.addUpdateListener(animation->{
|
||||
tmpRect.set(0, 0, Math.round(topView.getX()-bottomView.getX()), bottomView.getHeight());
|
||||
bottomView.setClipBounds(tmpRect);
|
||||
});
|
||||
currentTransition=set;
|
||||
set.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void resizeOnNextFrame(){
|
||||
if(currentTransition!=null)
|
||||
currentTransition.cancel();
|
||||
int origBottom=menuContainer.getBottom();
|
||||
menuContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
menuContainer.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(menuContainer, "bottom", origBottom, menuContainer.getBottom());
|
||||
anim.setDuration(300);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
anim.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
currentTransition=null;
|
||||
}
|
||||
});
|
||||
currentTransition=anim;
|
||||
anim.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Activity getActivity(){
|
||||
return fragment.getActivity();
|
||||
}
|
||||
|
||||
String getAccountID(){
|
||||
return fragment.getAccountID();
|
||||
}
|
||||
|
||||
private class WindowView extends FrameLayout{
|
||||
private final Rect tmpRect=new Rect();
|
||||
public WindowView(@NonNull Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev){
|
||||
for(int i=0;i<getChildCount();i++){
|
||||
View child=getChildAt(i);
|
||||
child.getHitRect(tmpRect);
|
||||
if(tmpRect.contains(Math.round(ev.getX()), Math.round(ev.getY())))
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
if(ev.getAction()==MotionEvent.ACTION_DOWN){
|
||||
dismiss();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(MotionEvent ev){
|
||||
if(currentTransition!=null)
|
||||
return false;
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event){
|
||||
if(event.getKeyCode()==KeyEvent.KEYCODE_BACK){
|
||||
if(event.getAction()==KeyEvent.ACTION_DOWN){
|
||||
if(controllerStack.size()>1)
|
||||
popSubmenuController();
|
||||
else
|
||||
dismiss();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
public interface HostFragment{
|
||||
// Fragment methods
|
||||
Activity getActivity();
|
||||
Resources getResources();
|
||||
Toolbar getToolbar();
|
||||
String getAccountID();
|
||||
|
||||
// Callbacks
|
||||
void onDropdownWillDismiss();
|
||||
void onDropdownDismissed();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
@@ -26,6 +27,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.AddAccountToListsFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -40,6 +42,7 @@ import org.parceler.Parcels;
|
||||
import java.util.HashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -58,12 +61,15 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
private final CheckableRelativeLayout view;
|
||||
private final View checkbox;
|
||||
private final ProgressBar actionProgress;
|
||||
private final ImageButton menuButton;
|
||||
|
||||
private final String accountID;
|
||||
private final Fragment fragment;
|
||||
private final HashMap<String, Relationship> relationships;
|
||||
|
||||
private Consumer<AccountViewHolder> onClick;
|
||||
private Predicate<AccountViewHolder> onLongClick;
|
||||
private Consumer<MenuItem> onCustomMenuItemSelected;
|
||||
private AccessoryType accessoryType;
|
||||
private boolean showBio;
|
||||
private boolean checked;
|
||||
@@ -85,6 +91,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
bio=findViewById(R.id.bio);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
actionProgress=findViewById(R.id.action_progress);
|
||||
menuButton=findViewById(R.id.options_btn);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
|
||||
avatar.setClipToOutline(true);
|
||||
@@ -94,6 +101,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
contextMenu=new PopupMenu(fragment.getActivity(), menuAnchor);
|
||||
contextMenu.inflate(R.menu.profile);
|
||||
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
|
||||
menuButton.setOnClickListener(v->showMenuFromButton());
|
||||
|
||||
setStyle(AccessoryType.BUTTON, false);
|
||||
}
|
||||
@@ -181,37 +189,13 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(float x, float y){
|
||||
if(relationships==null)
|
||||
if(onLongClick!=null && onLongClick.test(this))
|
||||
return true;
|
||||
if(accessoryType==AccessoryType.MENU || !prepareMenu())
|
||||
return false;
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
|
||||
menuAnchor.setTranslationX(x);
|
||||
menuAnchor.setTranslationY(y);
|
||||
contextMenu.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -279,6 +263,13 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
})
|
||||
.wrapProgress(fragment.getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}else if(id==R.id.add_to_list){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(fragment.getActivity(), AddAccountToListsFragment.class, args);
|
||||
}else if(onCustomMenuItemSelected!=null){
|
||||
onCustomMenuItemSelected.accept(item);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -292,6 +283,14 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
onClick=listener;
|
||||
}
|
||||
|
||||
public void setOnLongClickListener(Predicate<AccountViewHolder> onLongClick){
|
||||
this.onLongClick=onLongClick;
|
||||
}
|
||||
|
||||
public void setOnCustomMenuItemSelectedListener(Consumer<MenuItem> onCustomMenuItemSelected){
|
||||
this.onCustomMenuItemSelected=onCustomMenuItemSelected;
|
||||
}
|
||||
|
||||
public void setStyle(AccessoryType accessoryType, boolean showBio){
|
||||
if(accessoryType!=this.accessoryType){
|
||||
this.accessoryType=accessoryType;
|
||||
@@ -299,20 +298,29 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
case NONE -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
}
|
||||
case CHECKBOX -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.VISIBLE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
checkbox.setBackground(new CheckBox(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case RADIOBUTTON -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.VISIBLE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
checkbox.setBackground(new RadioButton(checkbox.getContext()).getButtonDrawable());
|
||||
}
|
||||
case BUTTON -> {
|
||||
button.setVisibility(View.VISIBLE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.GONE);
|
||||
}
|
||||
case MENU -> {
|
||||
button.setVisibility(View.GONE);
|
||||
checkbox.setVisibility(View.GONE);
|
||||
menuButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
view.setCheckable(accessoryType==AccessoryType.CHECKBOX || accessoryType==AccessoryType.RADIOBUTTON);
|
||||
@@ -321,15 +329,63 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
bio.setVisibility(showBio ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private boolean prepareMenu(){
|
||||
if(relationships==null)
|
||||
return false;
|
||||
Relationship relationship=relationships.get(item.account.id);
|
||||
if(relationship==null)
|
||||
return false;
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.share).setTitle(fragment.getString(R.string.share_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.mute).setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.block).setTitle(fragment.getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername()));
|
||||
menu.findItem(R.id.report).setTitle(fragment.getString(R.string.report_user, account.getDisplayUsername()));
|
||||
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
|
||||
if(relationship.following){
|
||||
hideBoosts.setTitle(fragment.getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername()));
|
||||
hideBoosts.setVisible(true);
|
||||
}else{
|
||||
hideBoosts.setVisible(false);
|
||||
}
|
||||
MenuItem blockDomain=menu.findItem(R.id.block_domain);
|
||||
if(!account.isLocal()){
|
||||
blockDomain.setTitle(fragment.getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
|
||||
blockDomain.setVisible(true);
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showMenuFromButton(){
|
||||
if(!prepareMenu())
|
||||
return;
|
||||
int[] xy={0, 0};
|
||||
itemView.getLocationInWindow(xy);
|
||||
int x=xy[0], y=xy[1];
|
||||
menuButton.getLocationInWindow(xy);
|
||||
menuAnchor.setTranslationX(xy[0]-x+menuButton.getWidth()/2f);
|
||||
menuAnchor.setTranslationY(xy[1]-y+menuButton.getHeight());
|
||||
contextMenu.show();
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
this.checked=checked;
|
||||
view.setChecked(checked);
|
||||
}
|
||||
|
||||
public PopupMenu getContextMenu(){
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
public enum AccessoryType{
|
||||
NONE,
|
||||
BUTTON,
|
||||
CHECKBOX,
|
||||
RADIOBUTTON
|
||||
RADIOBUTTON,
|
||||
MENU
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.ui.views.AvatarPileView;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AvatarPileListItemViewHolder extends ListItemViewHolder<AvatarPileListItem<?>> implements ImageLoaderViewHolder{
|
||||
private final AvatarPileView pile;
|
||||
|
||||
public AvatarPileListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list, parent);
|
||||
pile=new AvatarPileView(context);
|
||||
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
lp.topMargin=lp.bottomMargin=V.dp(-8);
|
||||
view.addView(pile, lp);
|
||||
view.setClipToPadding(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AvatarPileListItem<?> item){
|
||||
super.onBind(item);
|
||||
pile.setVisibleAvatarCount(item.avatars.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
pile.avatars[index].setImageDrawable(image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
pile.avatars[index].setImageResource(R.drawable.image_placeholder);
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,6 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
item.performClick();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.PopupMenu;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||
|
||||
public class OptionsListItemViewHolder extends ListItemViewHolder<ListItemWithOptionsMenu<?>>{
|
||||
private final PopupMenu menu;
|
||||
private final ImageButton menuBtn;
|
||||
|
||||
public OptionsListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list_options, parent);
|
||||
menuBtn=findViewById(R.id.options_btn);
|
||||
menu=new PopupMenu(context, menuBtn);
|
||||
menuBtn.setOnClickListener(this::onMenuBtnClick);
|
||||
|
||||
menu.setOnMenuItemClickListener(menuItem->{
|
||||
item.performItemSelected(menuItem);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void onMenuBtnClick(View v){
|
||||
menu.getMenu().clear();
|
||||
item.performConfigureMenu(menu.getMenu());
|
||||
menu.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class AvatarPileView extends LinearLayout implements CustomViewHelper{
|
||||
public final ImageView[] avatars=new ImageView[3];
|
||||
private final Paint borderPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final RectF tmpRect=new RectF();
|
||||
|
||||
public AvatarPileView(Context context){
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public AvatarPileView(Context context, @Nullable AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public AvatarPileView(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init(){
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null);
|
||||
setPaddingRelative(dp(16), 0, 0, 0);
|
||||
setClipToPadding(false);
|
||||
for(int i=0;i<avatars.length;i++){
|
||||
ImageView ava=new ImageView(getContext());
|
||||
ava.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
ava.setOutlineProvider(OutlineProviders.roundedRect(6));
|
||||
ava.setClipToOutline(true);
|
||||
ava.setImageResource(R.drawable.image_placeholder);
|
||||
ava.setPivotX(dp(16));
|
||||
ava.setPivotY(dp(32));
|
||||
ava.setRotation((avatars.length-1-i)*(-2f));
|
||||
LayoutParams lp=new LayoutParams(dp(32), dp(32));
|
||||
lp.gravity=Gravity.CENTER_VERTICAL;
|
||||
if(i<avatars.length-1)
|
||||
lp.setMarginEnd(dp(-16));
|
||||
addView(ava, lp);
|
||||
avatars[avatars.length-1-i]=ava;
|
||||
}
|
||||
borderPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
}
|
||||
|
||||
public void setVisibleAvatarCount(int count){
|
||||
for(int i=0;i<avatars.length;i++){
|
||||
avatars[i].setVisibility(i<count ? VISIBLE : INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean drawChild(Canvas canvas, View child, long drawingTime){
|
||||
tmpRect.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
|
||||
tmpRect.offset(child.getTranslationX(), child.getTranslationY());
|
||||
tmpRect.inset(dp(-2), dp(-2));
|
||||
canvas.save();
|
||||
canvas.rotate(child.getRotation(), child.getLeft()+child.getPivotX(), child.getTop()+child.getPivotY());
|
||||
canvas.drawRoundRect(tmpRect, dp(8), dp(8), borderPaint);
|
||||
canvas.restore();
|
||||
return super.drawChild(canvas, child, drawingTime);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.widget.ImageView;
|
||||
|
||||
public class FixedAspectRatioImageView extends ImageView{
|
||||
private float aspectRatio=1;
|
||||
private boolean useHeight;
|
||||
|
||||
public FixedAspectRatioImageView(Context context){
|
||||
this(context, null);
|
||||
@@ -21,8 +22,13 @@ public class FixedAspectRatioImageView extends ImageView{
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec);
|
||||
heightMeasureSpec=Math.round(width/aspectRatio) | MeasureSpec.EXACTLY;
|
||||
if(useHeight){
|
||||
int height=MeasureSpec.getSize(heightMeasureSpec);
|
||||
widthMeasureSpec=Math.round(height*aspectRatio) | MeasureSpec.EXACTLY;
|
||||
}else{
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec);
|
||||
heightMeasureSpec=Math.round(width/aspectRatio) | MeasureSpec.EXACTLY;
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
@@ -33,4 +39,12 @@ public class FixedAspectRatioImageView extends ImageView{
|
||||
public void setAspectRatio(float aspectRatio){
|
||||
this.aspectRatio=aspectRatio;
|
||||
}
|
||||
|
||||
public boolean isUseHeight(){
|
||||
return useHeight;
|
||||
}
|
||||
|
||||
public void setUseHeight(boolean useHeight){
|
||||
this.useHeight=useHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,13 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.CustomViewHelper;
|
||||
|
||||
public class FloatingHintEditTextLayout extends FrameLayout implements CustomViewHelper{
|
||||
private EditText edit;
|
||||
private View firstChild;
|
||||
private TextView label;
|
||||
private int labelTextSize;
|
||||
private int offsetY;
|
||||
@@ -71,30 +73,37 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
@Override
|
||||
protected void onFinishInflate(){
|
||||
super.onFinishInflate();
|
||||
if(getChildCount()>0 && getChildAt(0) instanceof EditText et){
|
||||
edit=et;
|
||||
if(getChildCount()>0){
|
||||
firstChild=getChildAt(0);
|
||||
if(firstChild instanceof EditText et)
|
||||
edit=et;
|
||||
}else{
|
||||
throw new IllegalStateException("First child must be an EditText");
|
||||
throw new IllegalStateException("Must contain at least one child view");
|
||||
}
|
||||
|
||||
label=new TextView(getContext());
|
||||
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize);
|
||||
// label.setTextColor(labelColors==null ? edit.getHintTextColors() : labelColors);
|
||||
origHintColors=edit.getHintTextColors();
|
||||
label.setText(edit.getHint());
|
||||
if(edit!=null){
|
||||
origHintColors=edit.getHintTextColors();
|
||||
label.setText(edit.getHint());
|
||||
}
|
||||
label.setSingleLine();
|
||||
label.setPivotX(0f);
|
||||
label.setPivotY(0f);
|
||||
label.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP);
|
||||
lp.setMarginStart(edit.getPaddingStart()+((LayoutParams)edit.getLayoutParams()).getMarginStart());
|
||||
lp.setMarginStart(firstChild.getPaddingStart()+((LayoutParams)firstChild.getLayoutParams()).getMarginStart());
|
||||
addView(label, lp);
|
||||
|
||||
hintVisible=edit.getText().length()==0;
|
||||
hintVisible=edit!=null && edit.getText().length()==0;
|
||||
if(hintVisible)
|
||||
label.setAlpha(0f);
|
||||
else
|
||||
animProgress=1;
|
||||
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged));
|
||||
if(edit!=null)
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged));
|
||||
|
||||
errorView=new LinkedTextView(getContext());
|
||||
errorView.setTextAppearance(R.style.m3_body_small);
|
||||
@@ -110,6 +119,18 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
label.setText(edit.getHint());
|
||||
}
|
||||
|
||||
public void setHint(CharSequence hint){
|
||||
label.setText(hint);
|
||||
}
|
||||
|
||||
public void setHint(@StringRes int hint){
|
||||
label.setText(hint);
|
||||
}
|
||||
|
||||
public TextView getLabel(){
|
||||
return label;
|
||||
}
|
||||
|
||||
private void onTextChanged(Editable text){
|
||||
if(errorState){
|
||||
errorView.setVisibility(View.GONE);
|
||||
@@ -244,7 +265,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
if(errorView.getVisibility()!=GONE){
|
||||
int width=MeasureSpec.getSize(widthMeasureSpec)-getPaddingLeft()-getPaddingRight();
|
||||
LayoutParams editLP=(LayoutParams) edit.getLayoutParams();
|
||||
LayoutParams editLP=(LayoutParams) firstChild.getLayoutParams();
|
||||
width-=editLP.leftMargin+editLP.rightMargin;
|
||||
errorView.measure(width | MeasureSpec.EXACTLY, MeasureSpec.UNSPECIFIED);
|
||||
LayoutParams lp=(LayoutParams) errorView.getLayoutParams();
|
||||
@@ -254,7 +275,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
lp.leftMargin=editLP.leftMargin;
|
||||
editLP.bottomMargin=errorView.getMeasuredHeight();
|
||||
}else{
|
||||
LayoutParams editLP=(LayoutParams) edit.getLayoutParams();
|
||||
LayoutParams editLP=(LayoutParams) firstChild.getLayoutParams();
|
||||
editLP.bottomMargin=0;
|
||||
}
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
@@ -355,7 +376,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
||||
protected void onBoundsChange(@NonNull Rect bounds){
|
||||
super.onBoundsChange(bounds);
|
||||
int offset=dp(12);
|
||||
wrapped.setBounds(edit.getLeft()-offset, edit.getTop()-offset, edit.getRight()+offset, edit.getBottom()+offset);
|
||||
wrapped.setBounds(firstChild.getLeft()-offset, firstChild.getTop()-offset, firstChild.getRight()+offset, firstChild.getBottom()+offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user