closes #89, closes #279
This commit is contained in:
Grishka
2023-10-08 22:03:16 +03:00
parent 6c1c5b7759
commit dff2217e80
90 changed files with 3025 additions and 243 deletions

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -75,6 +75,6 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
@Override
public void onClick(){
item.onClick.run();
item.performClick();
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}