Compose M3 redesign: custom emoji keyboard

This commit is contained in:
Grishka 2023-05-13 04:27:12 +03:00
parent 15883f2138
commit 34a2af8429
11 changed files with 233 additions and 34 deletions

View File

@ -20,13 +20,16 @@ import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan; import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.util.Log; import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.SoundEffectConstants;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider; import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
@ -223,7 +226,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
creatingView=true; creatingView=true;
emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain); emojiKeyboard=new CustomEmojiPopupKeyboard(getActivity(), customEmojis, instanceDomain);
emojiKeyboard.setListener(this::onCustomEmojiClick); emojiKeyboard.setListener(new CustomEmojiPopupKeyboard.Listener(){
@Override
public void onEmojiSelected(Emoji emoji){
onCustomEmojiClick(emoji);
}
@Override
public void onBackspace(){
getActivity().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
getActivity().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
}
});
View view=inflater.inflate(R.layout.fragment_compose, container, false); View view=inflater.inflate(R.layout.fragment_compose, container, false);
mainLayout=view.findViewById(R.id.compose_main_ll); mainLayout=view.findViewById(R.id.compose_main_ll);
@ -269,6 +283,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override @Override
public void onIconChanged(int icon){ public void onIconChanged(int icon){
emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN); emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN);
updateNavigationBarColor(icon!=PopupKeyboard.ICON_HIDDEN);
if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){ if(autocompleteViewController.getMode()==ComposeAutocompleteViewController.Mode.EMOJIS){
contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom()); contentView.layout(contentView.getLeft(), contentView.getTop(), contentView.getRight(), contentView.getBottom());
if(icon==PopupKeyboard.ICON_HIDDEN) if(icon==PopupKeyboard.ICON_HIDDEN)
@ -281,7 +296,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
contentView=(SizeListenerLinearLayout) view; contentView=(SizeListenerLinearLayout) view;
contentView.addView(emojiKeyboard.getView()); contentView.addView(emojiKeyboard.getView());
emojiKeyboard.getView().setElevation(V.dp(2));
spoilerEdit=view.findViewById(R.id.content_warning); spoilerEdit=view.findViewById(R.id.content_warning);
spoilerWrap=view.findViewById(R.id.content_warning_wrap); spoilerWrap=view.findViewById(R.id.content_warning_wrap);
@ -608,8 +622,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Background, R.attr.colorM3Primary, 0.11f); int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Background, R.attr.colorM3Primary, 0.11f);
getToolbar().setBackgroundColor(color); getToolbar().setBackgroundColor(color);
setStatusBarColor(color); setStatusBarColor(color);
setNavigationBarColor(color);
bottomBar.setBackgroundColor(color); bottomBar.setBackgroundColor(color);
updateNavigationBarColor(emojiKeyboard.isVisible());
}
private void updateNavigationBarColor(boolean emojiKeyboardVisible){
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Background, R.attr.colorM3Primary, emojiKeyboardVisible ? 0.08f : 0.11f);
setNavigationBarColor(color);
} }
@Override @Override

View File

@ -2,14 +2,19 @@ package org.joinmastodon.android.ui;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.res.TypedArray; import android.content.res.ColorStateList;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Animatable; import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import com.squareup.otto.Subscribe; import com.squareup.otto.Subscribe;
@ -22,7 +27,6 @@ import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -45,9 +49,8 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
private ListImageLoaderWrapper imgLoader; private ListImageLoaderWrapper imgLoader;
private MergeRecyclerAdapter adapter=new MergeRecyclerAdapter(); private MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
private String domain; private String domain;
private int gridGap;
private int spanCount=6; private int spanCount=6;
private Consumer<Emoji> listener; private Listener listener;
public CustomEmojiPopupKeyboard(Activity activity, List<EmojiCategory> emojis, String domain){ public CustomEmojiPopupKeyboard(Activity activity, List<EmojiCategory> emojis, String domain){
super(activity); super(activity);
@ -62,11 +65,8 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
@Override @Override
protected void onMeasure(int widthSpec, int heightSpec){ protected void onMeasure(int widthSpec, int heightSpec){
// it's important to do this in onMeasure so the child views will be measured with correct paddings already set // it's important to do this in onMeasure so the child views will be measured with correct paddings already set
spanCount=Math.round(MeasureSpec.getSize(widthSpec)/(float)V.dp(44+20)); spanCount=Math.round((MeasureSpec.getSize(widthSpec)-V.dp(32-8))/(float)V.dp(48+8));
lm.setSpanCount(spanCount); lm.setSpanCount(spanCount);
int pad=V.dp(16);
gridGap=(MeasureSpec.getSize(widthSpec)-pad*2-V.dp(44)*spanCount)/(spanCount-1);
setPadding(pad, 0, pad-gridGap, 0);
invalidateItemDecorations(); invalidateItemDecorations();
super.onMeasure(widthSpec, heightSpec); super.onMeasure(widthSpec, heightSpec);
} }
@ -80,6 +80,7 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
} }
}); });
list.setLayoutManager(lm); list.setLayoutManager(lm);
list.setPadding(V.dp(16), 0, V.dp(16), 0);
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null); imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
for(EmojiCategory category:emojis) for(EmojiCategory category:emojis)
@ -88,22 +89,52 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
list.addItemDecoration(new RecyclerView.ItemDecoration(){ list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override @Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
outRect.right=gridGap;
if(view instanceof TextView){ // section header if(view instanceof TextView){ // section header
if(parent.getChildAdapterPosition(view)>0) outRect.left=outRect.right=V.dp(-16);
outRect.top=-gridGap; // negate the margin added by the emojis above
}else{ }else{
outRect.bottom=gridGap; EmojiViewHolder evh=(EmojiViewHolder) parent.getChildViewHolder(view);
int col=evh.positionWithinCategory%spanCount;
if(col<spanCount-1){
outRect.right=V.dp(8);
}
outRect.bottom=V.dp(8);
} }
} }
}); });
list.setBackgroundColor(UiUtils.getThemeColor(activity, android.R.attr.colorBackground));
list.setSelector(null); list.setSelector(null);
list.setClipToPadding(false);
new StickyHeadersOverlay(activity, 0).install(list);
return list; LinearLayout ll=new LinearLayout(activity);
ll.setOrientation(LinearLayout.VERTICAL);
ll.setElevation(V.dp(3));
ll.setBackgroundResource(R.drawable.bg_m3_surface1);
ll.addView(list, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
FrameLayout bottomPanel=new FrameLayout(activity);
bottomPanel.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
bottomPanel.setBackgroundResource(R.drawable.bg_m3_surface2);
ll.addView(bottomPanel, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
ImageButton hideKeyboard=new ImageButton(activity);
hideKeyboard.setImageResource(R.drawable.ic_keyboard_hide_24px);
hideKeyboard.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant)));
hideKeyboard.setBackgroundResource(R.drawable.bg_round_ripple);
hideKeyboard.setOnClickListener(v->hide());
bottomPanel.addView(hideKeyboard, new FrameLayout.LayoutParams(V.dp(36), V.dp(36), Gravity.LEFT));
ImageButton backspace=new ImageButton(activity);
backspace.setImageResource(R.drawable.ic_backspace_24px);
backspace.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(activity, R.attr.colorM3OnSurfaceVariant)));
backspace.setBackgroundResource(R.drawable.bg_round_ripple);
backspace.setOnClickListener(v->listener.onBackspace());
bottomPanel.addView(backspace, new FrameLayout.LayoutParams(V.dp(36), V.dp(36), Gravity.RIGHT));
return ll;
} }
public void setListener(Consumer<Emoji> listener){ public void setListener(Listener listener){
this.listener=listener; this.listener=listener;
} }
@ -123,7 +154,7 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
public SingleCategoryAdapter(EmojiCategory category){ public SingleCategoryAdapter(EmojiCategory category){
super(imgLoader); super(imgLoader);
this.category=category; this.category=category;
requests=category.emojis.stream().map(e->new UrlImageLoaderRequest(e.url, V.dp(44), V.dp(44))).collect(Collectors.toList()); requests=category.emojis.stream().map(e->new UrlImageLoaderRequest(e.url, V.dp(24), V.dp(24))).collect(Collectors.toList());
} }
@NonNull @NonNull
@ -134,11 +165,11 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
@Override @Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position){ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position){
if(holder instanceof EmojiViewHolder){ if(holder instanceof EmojiViewHolder evh){
((EmojiViewHolder) holder).bind(category.emojis.get(position-1)); evh.bind(category.emojis.get(position-1));
((EmojiViewHolder) holder).positionWithinCategory=position-1; evh.positionWithinCategory=position-1;
}else if(holder instanceof SectionHeaderViewHolder){ }else if(holder instanceof SectionHeaderViewHolder shvh){
((SectionHeaderViewHolder) holder).bind(TextUtils.isEmpty(category.title) ? domain : category.title); shvh.bind(TextUtils.isEmpty(category.title) ? domain : category.title);
} }
super.onBindViewHolder(holder, position); super.onBindViewHolder(holder, position);
} }
@ -164,14 +195,24 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
} }
} }
private class SectionHeaderViewHolder extends BindableViewHolder<String>{ private class SectionHeaderViewHolder extends BindableViewHolder<String> implements StickyHeadersOverlay.HeaderViewHolder{
private Drawable background;
public SectionHeaderViewHolder(){ public SectionHeaderViewHolder(){
super(activity, R.layout.item_emoji_section, list); super(activity, R.layout.item_emoji_section, list);
background=new ColorDrawable(UiUtils.alphaBlendThemeColors(activity, R.attr.colorM3Surface, R.attr.colorM3Primary, .08f));
itemView.setBackground(background);
} }
@Override @Override
public void onBind(String item){ public void onBind(String item){
((TextView)itemView).setText(item); ((TextView)itemView).setText(item);
setStickyFactor(0);
}
@Override
public void setStickyFactor(float factor){
background.setAlpha(Math.round(255*factor));
} }
} }
@ -180,8 +221,11 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
public EmojiViewHolder(){ public EmojiViewHolder(){
super(new ImageView(activity)); super(new ImageView(activity));
ImageView img=(ImageView) itemView; ImageView img=(ImageView) itemView;
img.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(44))); img.setLayoutParams(new RecyclerView.LayoutParams(V.dp(48), V.dp(48)));
img.setScaleType(ImageView.ScaleType.FIT_CENTER); img.setScaleType(ImageView.ScaleType.FIT_CENTER);
int pad=V.dp(12);
img.setPadding(pad, pad, pad, pad);
img.setBackgroundResource(R.drawable.bg_custom_emoji);
} }
@Override @Override
@ -203,7 +247,12 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
@Override @Override
public void onClick(){ public void onClick(){
listener.accept(item); listener.onEmojiSelected(item);
} }
} }
public interface Listener{
void onEmojiSelected(Emoji emoji);
void onBackspace();
}
} }

View File

@ -0,0 +1,87 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import android.view.View;
import android.widget.FrameLayout;
import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class StickyHeadersOverlay{
private static final String TAG="StickyHeadersOverlay";
private FrameLayout headerWrapper;
private Context context;
private RecyclerView parent;
private RecyclerView.ViewHolder currentHeaderHolder;
private int headerViewType;
public StickyHeadersOverlay(Context context, int headerViewType){
this.context=context;
this.headerViewType=headerViewType;
headerWrapper=new FrameLayout(context);
}
public void install(RecyclerView parent){
if(this.parent!=null)
throw new IllegalStateException();
this.parent=parent;
parent.getViewTreeObserver().addOnPreDrawListener(()->{
if(parent.getWidth()!=headerWrapper.getWidth() || parent.getHeight()!=headerWrapper.getHeight()){
headerWrapper.measure(parent.getWidth() | View.MeasureSpec.EXACTLY, parent.getHeight() | View.MeasureSpec.EXACTLY);
headerWrapper.layout(0, 0, parent.getWidth(), parent.getHeight());
}
return true;
});
parent.getOverlay().add(headerWrapper);
parent.addOnScrollListener(new RecyclerView.OnScrollListener(){
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(currentHeaderHolder==null){
currentHeaderHolder=parent.getAdapter().createViewHolder(parent, headerViewType);
headerWrapper.addView(currentHeaderHolder.itemView);
}
int firstVisiblePos=parent.getChildAdapterPosition(parent.getChildAt(0));
RecyclerView.Adapter<RecyclerView.ViewHolder> adapter=Objects.requireNonNull(parent.getAdapter());
// Go backwards from the first visible position to find the previous header
for(int i=firstVisiblePos;i>=0;i--){
if(adapter.getItemViewType(i)==headerViewType){
if(currentHeaderHolder.getAbsoluteAdapterPosition()!=i){
adapter.bindViewHolder(currentHeaderHolder, i);
}
break;
}
}
if(currentHeaderHolder instanceof HeaderViewHolder hvh){
hvh.setStickyFactor(firstVisiblePos==0 && parent.getChildAt(0).getTop()==0 ? 0 : 1);
}
// Now go forward and find the next header view to possibly offset the current one
for(int i=firstVisiblePos+1;i<adapter.getItemCount();i++){
if(adapter.getItemViewType(i)==headerViewType){
RecyclerView.ViewHolder holder=parent.findViewHolderForAdapterPosition(i);
if(holder!=null){
float factor;
if(holder.itemView.getTop()<currentHeaderHolder.itemView.getBottom()){
currentHeaderHolder.itemView.setTranslationY(holder.itemView.getTop()-currentHeaderHolder.itemView.getBottom());
factor=1f-holder.itemView.getTop()/(float)currentHeaderHolder.itemView.getBottom();
}else{
currentHeaderHolder.itemView.setTranslationY(0);
factor=0;
}
if(holder instanceof HeaderViewHolder hvh)
hvh.setStickyFactor(factor);
}
break;
}
}
}
});
}
public interface HeaderViewHolder{
void setStickyFactor(float factor);
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3Primary" android:alpha="0.05"/>
</selector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3Primary" android:alpha="0.08"/>
</selector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/m3_on_surface_variant_overlay">
<item android:id="@android:id/mask" android:gravity="center" android:width="40dp" android:height="40dp">
<shape android:shape="oval">
<solid android:color="#000"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="@color/m3_primary_alpha5"
android:tintMode="src_over">
<solid android:color="?colorM3Surface" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="@color/m3_primary_alpha8"
android:tintMode="src_over">
<solid android:color="?colorM3Surface" />
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11.4,16 L14,13.4 16.6,16 18,14.6 15.4,12 18,9.4 16.6,8 14,10.6 11.4,8 10,9.4 12.6,12 10,14.6ZM3,12 L7.35,5.85Q7.625,5.45 8.062,5.225Q8.5,5 9,5H19Q19.825,5 20.413,5.588Q21,6.175 21,7V17Q21,17.825 20.413,18.413Q19.825,19 19,19H9Q8.5,19 8.062,18.775Q7.625,18.55 7.35,18.15ZM5.45,12 L9,17Q9,17 9,17Q9,17 9,17H19Q19,17 19,17Q19,17 19,17V7Q19,7 19,7Q19,7 19,7H9Q9,7 9,7Q9,7 9,7ZM19,12V7Q19,7 19,7Q19,7 19,7Q19,7 19,7Q19,7 19,7V17Q19,17 19,17Q19,17 19,17Q19,17 19,17Q19,17 19,17Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,17H4Q3.175,17 2.588,16.413Q2,15.825 2,15V5Q2,4.175 2.588,3.587Q3.175,3 4,3H20Q20.825,3 21.413,3.587Q22,4.175 22,5V15Q22,15.825 21.413,16.413Q20.825,17 20,17ZM20,15Q20,15 20,15Q20,15 20,15V5Q20,5 20,5Q20,5 20,5H4Q4,5 4,5Q4,5 4,5V15Q4,15 4,15Q4,15 4,15ZM11,8H13V6H11ZM11,11H13V9H11ZM8,8H10V6H8ZM8,11H10V9H8ZM5,11H7V9H5ZM5,8H7V6H5ZM8,14H16V12H8ZM14,11H16V9H14ZM14,8H16V6H14ZM17,11H19V9H17ZM17,8H19V6H17ZM12,23 L8,19H16ZM4,5Q4,5 4,5Q4,5 4,5V15Q4,15 4,15Q4,15 4,15Q4,15 4,15Q4,15 4,15V5Q4,5 4,5Q4,5 4,5Z"/>
</vector>

View File

@ -2,13 +2,11 @@
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="36dp"
android:singleLine="true" android:singleLine="true"
android:ellipsize="end" android:ellipsize="end"
android:fontFamily="sans-serif-medium" android:textAppearance="@style/m3_label_large"
android:textSize="12dp" android:textColor="?colorM3OnSurfaceVariant"
android:textColor="?android:textColorSecondary" android:paddingHorizontal="16dp"
android:textAllCaps="true" android:gravity="center_vertical"
android:paddingTop="24dp"
android:paddingBottom="12dp"
tools:text="Blob whatever things"/> tools:text="Blob whatever things"/>