Implemented continuous scrolling for emoji keyboards

This commit is contained in:
pdroidandroid@gmail.com 2021-09-12 17:30:26 +02:00
parent b775e0d878
commit 02e2051b33
25 changed files with 190 additions and 185 deletions

View file

@ -76,6 +76,15 @@ final class DynamicGridKeyboard extends Keyboard {
throw new RuntimeException("Can't find template key: code=" + code); throw new RuntimeException("Can't find template key: code=" + code);
} }
public int getDynamicOccupiedHeight() {
final int row = (mGridKeys.size() - 1) / mColumnsNum + 1;
return row * mVerticalStep;
}
public int getColumnsCount() {
return mColumnsNum;
}
public void addPendingKey(final Key usedKey) { public void addPendingKey(final Key usedKey) {
synchronized (mLock) { synchronized (mLock) {
mPendingKeys.addLast(usedKey); mPendingKeys.addLast(usedKey);

View file

@ -21,9 +21,7 @@ import android.content.res.Resources;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import androidx.core.graphics.PaintCompat; import androidx.core.graphics.PaintCompat;
import org.dslul.openboard.inputmethod.keyboard.Key; import org.dslul.openboard.inputmethod.keyboard.Key;
@ -56,6 +54,8 @@ final class EmojiCategory {
private static final int ID_FLAGS = 9; private static final int ID_FLAGS = 9;
private static final int ID_EMOTICONS = 10; private static final int ID_EMOTICONS = 10;
private static final int MAX_LINE_COUNT_PER_PAGE = 3;
public final class CategoryProperties { public final class CategoryProperties {
public final int mCategoryId; public final int mCategoryId;
public final int mPageCount; public final int mPageCount;
@ -119,7 +119,7 @@ final class EmojiCategory {
private final SharedPreferences mPrefs; private final SharedPreferences mPrefs;
private final Resources mRes; private final Resources mRes;
private final int mMaxPageKeyCount; private final int mMaxRecentsKeyCount;
private final KeyboardLayoutSet mLayoutSet; private final KeyboardLayoutSet mLayoutSet;
private final HashMap<String, Integer> mCategoryNameToIdMap = new HashMap<>(); private final HashMap<String, Integer> mCategoryNameToIdMap = new HashMap<>();
private final int[] mCategoryTabIconId = new int[sCategoryName.length]; private final int[] mCategoryTabIconId = new int[sCategoryName.length];
@ -134,7 +134,7 @@ final class EmojiCategory {
final KeyboardLayoutSet layoutSet, final TypedArray emojiPaletteViewAttr) { final KeyboardLayoutSet layoutSet, final TypedArray emojiPaletteViewAttr) {
mPrefs = prefs; mPrefs = prefs;
mRes = res; mRes = res;
mMaxPageKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_page_key_count); mMaxRecentsKeyCount = res.getInteger(R.integer.config_emoji_keyboard_max_recents_key_count);
mLayoutSet = layoutSet; mLayoutSet = layoutSet;
for (int i = 0; i < sCategoryName.length; ++i) { for (int i = 0; i < sCategoryName.length; ++i) {
mCategoryNameToIdMap.put(sCategoryName[i], i); mCategoryNameToIdMap.put(sCategoryName[i], i);
@ -170,7 +170,7 @@ final class EmojiCategory {
mCurrentCategoryId = defaultCategoryId; mCurrentCategoryId = defaultCategoryId;
} }
if (mCurrentCategoryPageId >= getCategoryPageCount(mCurrentCategoryId)) { if (mCurrentCategoryPageId >= computeCategoryPageCount(mCurrentCategoryId)) {
mCurrentCategoryPageId = 0; mCurrentCategoryPageId = 0;
} }
} }
@ -179,7 +179,7 @@ final class EmojiCategory {
// Load a keyboard of categoryId // Load a keyboard of categoryId
getKeyboard(categoryId, 0 /* categoryPageId */); getKeyboard(categoryId, 0 /* categoryPageId */);
final CategoryProperties properties = final CategoryProperties properties =
new CategoryProperties(categoryId, getCategoryPageCount(categoryId)); new CategoryProperties(categoryId, computeCategoryPageCount(categoryId));
mShownCategories.add(properties); mShownCategories.add(properties);
} }
@ -217,11 +217,11 @@ final class EmojiCategory {
return mCurrentCategoryId; return mCurrentCategoryId;
} }
public int getCurrentCategoryPageSize() { public int getCurrentCategoryPageCount() {
return getCategoryPageSize(mCurrentCategoryId); return getCategoryPageCount(mCurrentCategoryId);
} }
public int getCategoryPageSize(final int categoryId) { public int getCategoryPageCount(final int categoryId) {
for (final CategoryProperties prop : mShownCategories) { for (final CategoryProperties prop : mShownCategories) {
if (prop.mCategoryId == categoryId) { if (prop.mCategoryId == categoryId) {
return prop.mPageCount; return prop.mPageCount;
@ -283,38 +283,21 @@ final class EmojiCategory {
return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS); return getTabIdFromCategoryId(EmojiCategory.ID_RECENTS);
} }
private int getCategoryPageCount(final int categoryId) { private int computeCategoryPageCount(final int categoryId) {
final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
return (keyboard.getSortedKeys().size() - 1) / mMaxPageKeyCount + 1; return (keyboard.getSortedKeys().size() - 1) / computeMaxKeyCountPerPage() + 1;
} }
// Returns a pair of the category id and the category page id from the view pager's page // Returns a keyboard from the recycler view's adapter position.
// position. The category page id is numbered in each category. And the view page position public DynamicGridKeyboard getKeyboardFromAdapterPosition(final int position) {
// is the position of the current shown page in the view pager which contains all pages of if (position >= 0 && position < getCurrentCategoryPageCount()) {
// all categories. return getKeyboard(mCurrentCategoryId, position);
public Pair<Integer, Integer> getCategoryIdAndPageIdFromPagePosition(final int position) {
int sum = 0;
for (final CategoryProperties properties : mShownCategories) {
final int temp = sum;
sum += properties.mPageCount;
if (sum > position) {
return new Pair<>(properties.mCategoryId, position - temp);
}
} }
Log.w(TAG, "invalid position for categoryId : " + mCurrentCategoryId);
return null; return null;
} }
// Returns a keyboard from the view pager's page position. private static Long getCategoryKeyboardMapKey(final int categoryId, final int id) {
public DynamicGridKeyboard getKeyboardFromPagePosition(final int position) {
final Pair<Integer, Integer> categoryAndId =
getCategoryIdAndPageIdFromPagePosition(position);
if (categoryAndId != null) {
return getKeyboard(categoryAndId.first, categoryAndId.second);
}
return null;
}
private static final Long getCategoryKeyboardMapKey(final int categoryId, final int id) {
return (((long) categoryId) << Integer.SIZE) | id; return (((long) categoryId) << Integer.SIZE) | id;
} }
@ -328,19 +311,20 @@ final class EmojiCategory {
if (categoryId == EmojiCategory.ID_RECENTS) { if (categoryId == EmojiCategory.ID_RECENTS) {
final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs, final DynamicGridKeyboard kbd = new DynamicGridKeyboard(mPrefs,
mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
mMaxPageKeyCount, categoryId); mMaxRecentsKeyCount, categoryId);
mCategoryKeyboardMap.put(categoryKeyboardMapKey, kbd); mCategoryKeyboardMap.put(categoryKeyboardMapKey, kbd);
return kbd; return kbd;
} }
final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]); final Keyboard keyboard = mLayoutSet.getKeyboard(sCategoryElementId[categoryId]);
final Key[][] sortedKeys = sortKeysIntoPages( final int keyCountPerPage = computeMaxKeyCountPerPage();
keyboard.getSortedKeys(), mMaxPageKeyCount); final Key[][] sortedKeysPages = sortKeysGrouped(
for (int pageId = 0; pageId < sortedKeys.length; ++pageId) { keyboard.getSortedKeys(), keyCountPerPage);
for (int pageId = 0; pageId < sortedKeysPages.length; ++pageId) {
final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs, final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS), mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
mMaxPageKeyCount, categoryId); keyCountPerPage, categoryId);
for (final Key emojiKey : sortedKeys[pageId]) { for (final Key emojiKey : sortedKeysPages[pageId]) {
if (emojiKey == null) { if (emojiKey == null) {
break; break;
} }
@ -353,37 +337,33 @@ final class EmojiCategory {
} }
} }
public int getTotalPageCountOfAllCategories() { private int computeMaxKeyCountPerPage() {
int sum = 0; final DynamicGridKeyboard tempKeyboard = new DynamicGridKeyboard(mPrefs,
for (CategoryProperties properties : mShownCategories) { mLayoutSet.getKeyboard(KeyboardId.ELEMENT_EMOJI_RECENTS),
sum += properties.mPageCount; 0, 0);
} return MAX_LINE_COUNT_PER_PAGE * tempKeyboard.getColumnsCount();
return sum;
} }
private static Comparator<Key> EMOJI_KEY_COMPARATOR = new Comparator<Key>() { private static final Comparator<Key> EMOJI_KEY_COMPARATOR = (lhs, rhs) -> {
@Override final Rect lHitBox = lhs.getHitBox();
public int compare(final Key lhs, final Key rhs) { final Rect rHitBox = rhs.getHitBox();
final Rect lHitBox = lhs.getHitBox(); if (lHitBox.top < rHitBox.top) {
final Rect rHitBox = rhs.getHitBox(); return -1;
if (lHitBox.top < rHitBox.top) { } else if (lHitBox.top > rHitBox.top) {
return -1; return 1;
} else if (lHitBox.top > rHitBox.top) {
return 1;
}
if (lHitBox.left < rHitBox.left) {
return -1;
} else if (lHitBox.left > rHitBox.left) {
return 1;
}
if (lhs.getCode() == rhs.getCode()) {
return 0;
}
return lhs.getCode() < rhs.getCode() ? -1 : 1;
} }
if (lHitBox.left < rHitBox.left) {
return -1;
} else if (lHitBox.left > rHitBox.left) {
return 1;
}
if (lhs.getCode() == rhs.getCode()) {
return 0;
}
return lhs.getCode() < rhs.getCode() ? -1 : 1;
}; };
private static Key[][] sortKeysIntoPages(final List<Key> inKeys, final int maxPageCount) { private static Key[][] sortKeysGrouped(final List<Key> inKeys, final int maxPageCount) {
final ArrayList<Key> keys = new ArrayList<>(inKeys); final ArrayList<Key> keys = new ArrayList<>(inKeys);
Collections.sort(keys, EMOJI_KEY_COMPARATOR); Collections.sort(keys, EMOJI_KEY_COMPARATOR);
final int pageCount = (keys.size() - 1) / maxPageCount + 1; final int pageCount = (keys.size() - 1) / maxPageCount + 1;

View file

@ -61,9 +61,9 @@ public final class EmojiCategoryPageIndicatorView extends View {
final float height = getHeight(); final float height = getHeight();
final float width = getWidth(); final float width = getWidth();
final float unitWidth = width / mCategoryPageSize; final float unitWidth = width / mCategoryPageSize;
final float left = unitWidth * mCurrentCategoryPageId + mOffset * unitWidth; final float left = Math.min(unitWidth * mCurrentCategoryPageId + mOffset * unitWidth, width - unitWidth);
final float top = 0.0f; final float top = 0.0f;
final float right = left + unitWidth; final float right = Math.min(left + unitWidth, width);
final float bottom = height * BOTTOM_MARGIN_RATIO; final float bottom = height * BOTTOM_MARGIN_RATIO;
canvas.drawRect(left, top, right, bottom, mPaint); canvas.drawRect(left, top, right, bottom, mPaint);
} }

View file

@ -20,17 +20,16 @@ import android.content.res.Resources;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.recyclerview.widget.RecyclerView;
import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils; import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager2.widget.ViewPager2;
final class EmojiLayoutParams { final class EmojiLayoutParams {
private static final int DEFAULT_KEYBOARD_ROWS = 4; private static final int DEFAULT_KEYBOARD_ROWS = 4;
public final int mEmojiPagerHeight; public final int mEmojiListHeight;
private final int mEmojiPagerBottomMargin; private final int mEmojiListBottomMargin;
public final int mEmojiKeyboardHeight; public final int mEmojiKeyboardHeight;
private final int mEmojiCategoryPageIdViewHeight; private final int mEmojiCategoryPageIdViewHeight;
public final int mEmojiActionBarHeight; public final int mEmojiActionBarHeight;
@ -56,16 +55,16 @@ final class EmojiLayoutParams {
+ mKeyVerticalGap; + mKeyVerticalGap;
mEmojiActionBarHeight = baseheight / DEFAULT_KEYBOARD_ROWS mEmojiActionBarHeight = baseheight / DEFAULT_KEYBOARD_ROWS
- (mKeyVerticalGap - mBottomPadding) / 2; - (mKeyVerticalGap - mBottomPadding) / 2;
mEmojiPagerHeight = defaultKeyboardHeight - mEmojiActionBarHeight mEmojiListHeight = defaultKeyboardHeight - mEmojiActionBarHeight
- mEmojiCategoryPageIdViewHeight; - mEmojiCategoryPageIdViewHeight;
mEmojiPagerBottomMargin = 0; mEmojiListBottomMargin = 0;
mEmojiKeyboardHeight = mEmojiPagerHeight - mEmojiPagerBottomMargin - 1; mEmojiKeyboardHeight = mEmojiListHeight - mEmojiListBottomMargin - 1;
} }
public void setPagerProperties(final ViewPager2 vp) { public void setEmojiListProperties(final RecyclerView vp) {
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) vp.getLayoutParams(); final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) vp.getLayoutParams();
lp.height = mEmojiKeyboardHeight; lp.height = mEmojiKeyboardHeight;
lp.bottomMargin = mEmojiPagerBottomMargin; lp.bottomMargin = mEmojiListBottomMargin;
vp.setLayoutParams(lp); vp.setLayoutParams(lp);
} }

View file

@ -113,6 +113,20 @@ final class EmojiPageKeyboardView extends KeyboardView implements
mMoreKeysKeyboardContainer = inflater.inflate(moreKeysKeyboardLayoutId, null); mMoreKeysKeyboardContainer = inflater.inflate(moreKeysKeyboardLayoutId, null);
} }
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final Keyboard keyboard = getKeyboard();
if (keyboard instanceof DynamicGridKeyboard) {
final int width = keyboard.mOccupiedWidth + getPaddingLeft() + getPaddingRight();
final int occupiedHeight =
((DynamicGridKeyboard) keyboard).getDynamicOccupiedHeight();
final int height = occupiedHeight + getPaddingTop() + getPaddingBottom();
setMeasuredDimension(width, height);
return;
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override @Override
public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) { public void setHardwareAcceleratedDrawingEnabled(final boolean enabled) {
super.setHardwareAcceleratedDrawingEnabled(enabled); super.setHardwareAcceleratedDrawingEnabled(enabled);
@ -173,9 +187,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements
Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key); Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key);
if (moreKeysKeyboard == null) { if (moreKeysKeyboard == null) {
final MoreKeysKeyboard.Builder builder = new MoreKeysKeyboard.Builder( final MoreKeysKeyboard.Builder builder = new MoreKeysKeyboard.Builder(
getContext(), key, getKeyboard(), getContext(), key, getKeyboard(), false, 0, 0, newLabelPaint(key));
true, key.getWidth(), key.getHeight(), // TODO This is cheating
newLabelPaint(key));
moreKeysKeyboard = builder.build(); moreKeysKeyboard = builder.build();
mMoreKeysKeyboardCache.put(key, moreKeysKeyboard); mMoreKeysKeyboardCache.put(key, moreKeysKeyboard);
} }
@ -382,9 +394,6 @@ final class EmojiPageKeyboardView extends KeyboardView implements
final Runnable pendingKeyDown = mPendingKeyDown; final Runnable pendingKeyDown = mPendingKeyDown;
final Key currentKey = mCurrentKey; final Key currentKey = mCurrentKey;
releaseCurrentKey(false /* withKeyRegistering */); releaseCurrentKey(false /* withKeyRegistering */);
if (key == null) {
return false;
}
final boolean isShowingMoreKeysPanel = isShowingMoreKeysPanel(); final boolean isShowingMoreKeysPanel = isShowingMoreKeysPanel();
if (isShowingMoreKeysPanel) { if (isShowingMoreKeysPanel) {
@ -402,7 +411,7 @@ final class EmojiPageKeyboardView extends KeyboardView implements
callListenerOnReleaseKey(key, true /* withRegistering */); callListenerOnReleaseKey(key, true /* withRegistering */);
} }
}, KEY_RELEASE_DELAY_TIME); }, KEY_RELEASE_DELAY_TIME);
} else { } else if (key != null) {
callListenerOnReleaseKey(key, true /* withRegistering */); callListenerOnReleaseKey(key, true /* withRegistering */);
} }

View file

@ -182,7 +182,7 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
mActiveKeyboardViews.remove(position); mActiveKeyboardViews.remove(position);
} }
final Keyboard keyboard = final Keyboard keyboard =
mEmojiCategory.getKeyboardFromPagePosition(position); mEmojiCategory.getKeyboardFromAdapterPosition(position);
holder.getKeyboardView().setKeyboard(keyboard); holder.getKeyboardView().setKeyboard(keyboard);
holder.getKeyboardView().setOnKeyEventListener(mListener); holder.getKeyboardView().setOnKeyEventListener(mListener);
//parent.addView(keyboardView); //parent.addView(keyboardView);
@ -201,10 +201,9 @@ final class EmojiPalettesAdapter extends RecyclerView.Adapter<EmojiPalettesAdapt
@Override @Override
public int getItemCount() { public int getItemCount() {
return mEmojiCategory.getTotalPageCountOfAllCategories(); return mEmojiCategory.getCurrentCategoryPageCount();
} }
static class ViewHolder extends RecyclerView.ViewHolder { static class ViewHolder extends RecyclerView.ViewHolder {
private EmojiPageKeyboardView customView; private EmojiPageKeyboardView customView;

View file

@ -21,7 +21,6 @@ import android.content.res.Resources;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Color; import android.graphics.Color;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Pair;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MotionEvent; import android.view.MotionEvent;
@ -34,6 +33,9 @@ import android.widget.TabHost.OnTabChangeListener;
import android.widget.TabWidget; import android.widget.TabWidget;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.dslul.openboard.inputmethod.compat.TabHostCompat; import org.dslul.openboard.inputmethod.compat.TabHostCompat;
import org.dslul.openboard.inputmethod.keyboard.Key; import org.dslul.openboard.inputmethod.keyboard.Key;
import org.dslul.openboard.inputmethod.keyboard.KeyboardActionListener; import org.dslul.openboard.inputmethod.keyboard.KeyboardActionListener;
@ -46,10 +48,12 @@ import org.dslul.openboard.inputmethod.latin.AudioAndHapticFeedbackManager;
import org.dslul.openboard.inputmethod.latin.R; import org.dslul.openboard.inputmethod.latin.R;
import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype; import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype;
import org.dslul.openboard.inputmethod.latin.common.Constants; import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.settings.Settings;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils; import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils;
import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils; import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils;
import androidx.viewpager2.widget.ViewPager2; import org.jetbrains.annotations.NotNull;
import static org.dslul.openboard.inputmethod.latin.common.Constants.NOT_A_COORDINATE; import static org.dslul.openboard.inputmethod.latin.common.Constants.NOT_A_COORDINATE;
@ -78,6 +82,7 @@ public final class EmojiPalettesView extends LinearLayout
private EmojiPalettesAdapter mEmojiPalettesAdapter; private EmojiPalettesAdapter mEmojiPalettesAdapter;
private final EmojiLayoutParams mEmojiLayoutParams; private final EmojiLayoutParams mEmojiLayoutParams;
private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener; private final DeleteKeyOnTouchListener mDeleteKeyOnTouchListener;
private final LinearLayoutManager mEmojiLayoutManager;
private ImageButton mDeleteKey; private ImageButton mDeleteKey;
private TextView mAlphabetKeyLeft; private TextView mAlphabetKeyLeft;
@ -85,8 +90,7 @@ public final class EmojiPalettesView extends LinearLayout
// TODO: Remove this workaround. // TODO: Remove this workaround.
private View mSpacebarIcon; private View mSpacebarIcon;
private TabHostCompat mTabHost; private TabHostCompat mTabHost;
private ViewPager2 mEmojiPager; private RecyclerView mEmojiRecyclerView;
private int mCurrentPagerPosition = 0;
private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView; private EmojiCategoryPageIndicatorView mEmojiCategoryPageIndicatorView;
private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER; private KeyboardActionListener mKeyboardActionListener = KeyboardActionListener.EMPTY_LISTENER;
@ -132,6 +136,7 @@ public final class EmojiPalettesView extends LinearLayout
R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0); R.styleable.EmojiPalettesView_categoryPageIndicatorBackground, 0);
emojiPalettesViewAttr.recycle(); emojiPalettesViewAttr.recycle();
mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener(); mDeleteKeyOnTouchListener = new DeleteKeyOnTouchListener();
mEmojiLayoutManager = new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
} }
@Override @Override
@ -164,6 +169,7 @@ public final class EmojiPalettesView extends LinearLayout
@Override @Override
protected void onFinishInflate() { protected void onFinishInflate() {
super.onFinishInflate();
mTabHost = findViewById(R.id.emoji_category_tabhost); mTabHost = findViewById(R.id.emoji_category_tabhost);
mTabHost.setup(); mTabHost.setup();
for (final EmojiCategory.CategoryProperties properties for (final EmojiCategory.CategoryProperties properties
@ -183,51 +189,41 @@ public final class EmojiPalettesView extends LinearLayout
mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this); mEmojiPalettesAdapter = new EmojiPalettesAdapter(mEmojiCategory, this);
mEmojiPager = findViewById(R.id.emoji_keyboard_pager); mEmojiRecyclerView = findViewById(R.id.emoji_keyboard_list);
mEmojiPager.setAdapter(mEmojiPalettesAdapter); mEmojiRecyclerView.setLayoutManager(mEmojiLayoutManager);
mEmojiRecyclerView.setAdapter(mEmojiPalettesAdapter);
mEmojiPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { mEmojiRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override @Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { public void onScrollStateChanged(@NonNull @NotNull RecyclerView recyclerView, int newState) {
mEmojiPalettesAdapter.onPageScrolled(); super.onScrollStateChanged(recyclerView, newState);
final Pair<Integer, Integer> newPos =
mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
final int newCategoryId = newPos.first;
final int newCategorySize = mEmojiCategory.getCategoryPageSize(newCategoryId);
final int currentCategoryId = mEmojiCategory.getCurrentCategoryId();
final int currentCategoryPageId = mEmojiCategory.getCurrentCategoryPageId();
final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageSize();
if (newCategoryId == currentCategoryId) {
mEmojiCategoryPageIndicatorView.setCategoryPageId(
newCategorySize, newPos.second, positionOffset);
} else if (newCategoryId > currentCategoryId) {
mEmojiCategoryPageIndicatorView.setCategoryPageId(
currentCategorySize, currentCategoryPageId, positionOffset);
} else if (newCategoryId < currentCategoryId) {
mEmojiCategoryPageIndicatorView.setCategoryPageId(
currentCategorySize, currentCategoryPageId, positionOffset - 1);
}
}
@Override
public void onPageSelected(int position) {
final Pair<Integer, Integer> newPos =
mEmojiCategory.getCategoryIdAndPageIdFromPagePosition(position);
setCurrentCategoryAndPageId(newPos.first /* categoryId */, newPos.second /* categoryPageId */,
false /* force */);
updateEmojiCategoryPageIdView();
mCurrentPagerPosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {
// Ignore this message. Only want the actual page selected. // Ignore this message. Only want the actual page selected.
} }
@Override
public void onScrolled(@NonNull @NotNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mEmojiPalettesAdapter.onPageScrolled();
final int offset = recyclerView.computeVerticalScrollOffset();
final int extent = recyclerView.computeVerticalScrollExtent();
final int range = recyclerView.computeVerticalScrollRange();
final float percentage = offset / (float) (range - extent);
final int currentCategorySize = mEmojiCategory.getCurrentCategoryPageCount();
final int a = (int) (percentage * currentCategorySize);
final float b = percentage * currentCategorySize - a;
mEmojiCategoryPageIndicatorView.setCategoryPageId(
currentCategorySize, a, b);
final int firstCompleteVisibleBoard = mEmojiLayoutManager.findFirstCompletelyVisibleItemPosition();
final int firstVisibleBoard = mEmojiLayoutManager.findFirstVisibleItemPosition();
mEmojiCategory.setCurrentCategoryPageId(
firstCompleteVisibleBoard > 0 ? firstCompleteVisibleBoard : firstVisibleBoard);
}
}); });
mEmojiPager.setOffscreenPageLimit(ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT); mEmojiRecyclerView.setPersistentDrawingCache(PERSISTENT_NO_CACHE);
mEmojiPager.setPersistentDrawingCache(PERSISTENT_NO_CACHE); mEmojiLayoutParams.setEmojiListProperties(mEmojiRecyclerView);
mEmojiLayoutParams.setPagerProperties(mEmojiPager);
mEmojiCategoryPageIndicatorView = mEmojiCategoryPageIndicatorView =
findViewById(R.id.emoji_category_page_id_view); findViewById(R.id.emoji_category_page_id_view);
@ -283,8 +279,10 @@ public final class EmojiPalettesView extends LinearLayout
AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
Constants.CODE_UNSPECIFIED, this); Constants.CODE_UNSPECIFIED, this);
final int categoryId = mEmojiCategory.getCategoryId(tabId); final int categoryId = mEmojiCategory.getCategoryId(tabId);
setCurrentCategoryAndPageId(categoryId, 0, false /* force */); if (categoryId != mEmojiCategory.getCurrentCategoryId()) {
updateEmojiCategoryPageIdView(); setCurrentCategoryAndPageId(categoryId, 0, false /* force */);
updateEmojiCategoryPageIdView();
}
} }
/** /**
@ -388,14 +386,17 @@ public final class EmojiPalettesView extends LinearLayout
final KeyDrawParams params = new KeyDrawParams(); final KeyDrawParams params = new KeyDrawParams();
params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr); params.updateParams(mEmojiLayoutParams.getActionBarHeight(), keyVisualAttr);
setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params); setupAlphabetKey(mAlphabetKeyLeft, switchToAlphaLabel, params);
mEmojiPager.setAdapter(mEmojiPalettesAdapter); if (mEmojiRecyclerView.getAdapter() == null) {
mEmojiPager.setCurrentItem(mCurrentPagerPosition, false); mEmojiRecyclerView.setAdapter(mEmojiPalettesAdapter);
setCurrentCategoryAndPageId(mEmojiCategory.getCurrentCategoryId(), mEmojiCategory.getCurrentCategoryPageId(),
true /* force */);
}
} }
public void stopEmojiPalettes() { public void stopEmojiPalettes() {
mEmojiPalettesAdapter.releaseCurrentKey(true /* withKeyRegistering */); mEmojiPalettesAdapter.releaseCurrentKey(true /* withKeyRegistering */);
mEmojiPalettesAdapter.flushPendingRecentKeys(); mEmojiPalettesAdapter.flushPendingRecentKeys();
mEmojiPager.setAdapter(null); mEmojiRecyclerView.setAdapter(null);
} }
public void setKeyboardActionListener(final KeyboardActionListener listener) { public void setKeyboardActionListener(final KeyboardActionListener listener) {
@ -408,19 +409,14 @@ public final class EmojiPalettesView extends LinearLayout
return; return;
} }
mEmojiCategoryPageIndicatorView.setCategoryPageId( mEmojiCategoryPageIndicatorView.setCategoryPageId(
mEmojiCategory.getCurrentCategoryPageSize(), mEmojiCategory.getCurrentCategoryPageCount(),
mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */); mEmojiCategory.getCurrentCategoryPageId(), 0.0f /* offset */);
} }
private void setCurrentCategoryAndPageId(final int categoryId, final int categoryPageId, private void setCurrentCategoryAndPageId(final int categoryId, final int categoryPageId,
final boolean force) { final boolean force) {
final int oldCategoryId = mEmojiCategory.getCurrentCategoryId(); final int oldCategoryId = mEmojiCategory.getCurrentCategoryId();
final int oldPageId = mEmojiCategory.getCurrentCategoryPageId(); final int oldCategoryPageId = mEmojiCategory.getCurrentCategoryPageId();
final boolean firstPage = categoryPageId == 0;
mEmojiCategory.setCurrentCategoryPageId(categoryPageId);
if (oldCategoryId == categoryId && (!firstPage || oldPageId == 0) && !force) {
return;
}
if (oldCategoryId == EmojiCategory.ID_RECENTS && categoryId != EmojiCategory.ID_RECENTS) { if (oldCategoryId == EmojiCategory.ID_RECENTS && categoryId != EmojiCategory.ID_RECENTS) {
// Needs to save pending updates for recent keys when we get out of the recents // Needs to save pending updates for recent keys when we get out of the recents
@ -429,13 +425,11 @@ public final class EmojiPalettesView extends LinearLayout
mEmojiPalettesAdapter.flushPendingRecentKeys(); mEmojiPalettesAdapter.flushPendingRecentKeys();
} }
mEmojiCategory.setCurrentCategoryId(categoryId); if (force || oldCategoryId != categoryId || oldCategoryPageId != categoryPageId) {
final Pair<Integer, Integer> pagerIds = mEmojiCategory.getCategoryIdAndPageIdFromPagePosition( mEmojiCategory.setCurrentCategoryId(categoryId);
mEmojiPager.getCurrentItem()); mEmojiCategory.setCurrentCategoryPageId(categoryPageId);
if (force || pagerIds.first != categoryId || (firstPage && pagerIds.second != 0)) { mEmojiPalettesAdapter.notifyDataSetChanged();
final int newPagerPageId = mEmojiCategory.getPagerPageIdFromCategoryAndPageId(categoryId, categoryPageId); mEmojiRecyclerView.scrollToPosition(categoryPageId);
final boolean smooth = pagerIds.first == categoryId && firstPage && pagerIds.second != 0;
mEmojiPager.setCurrentItem(newPagerPageId, smooth /* smoothScroll */);
} }
final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId); final int newTabId = mEmojiCategory.getTabIdFromCategoryId(categoryId);

View file

@ -59,8 +59,8 @@
</FrameLayout> </FrameLayout>
</org.dslul.openboard.inputmethod.compat.TabHostCompat> </org.dslul.openboard.inputmethod.compat.TabHostCompat>
</LinearLayout> </LinearLayout>
<androidx.viewpager2.widget.ViewPager2 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/emoji_keyboard_pager" android:id="@+id/emoji_keyboard_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" /> android:orientation="vertical" />

View file

@ -77,8 +77,9 @@
<dimen name="config_gesture_floating_preview_vertical_padding">15dp</dimen> <dimen name="config_gesture_floating_preview_vertical_padding">15dp</dimen>
<!-- Emoji keyboard --> <!-- Emoji keyboard -->
<fraction name="config_emoji_keyboard_key_width">10%p</fraction> <fraction name="config_emoji_keyboard_key_width">8.33%p</fraction>
<fraction name="config_emoji_keyboard_row_height">50%p</fraction> <fraction name="config_emoji_keyboard_row_height">40%p</fraction>
<fraction name="config_emoji_keyboard_key_letter_size">54%p</fraction> <fraction name="config_emoji_keyboard_key_letter_size">70%p</fraction>
<integer name="config_emoji_keyboard_max_page_key_count">20</integer> <fraction name="config_emoji_keyboard_key_label_size">70%p</fraction>
<integer name="config_emoji_keyboard_max_recents_key_count">32</integer>
</resources> </resources>

View file

@ -65,8 +65,9 @@
<dimen name="config_gesture_floating_preview_vertical_padding">17dp</dimen> <dimen name="config_gesture_floating_preview_vertical_padding">17dp</dimen>
<!-- Emoji keyboard --> <!-- Emoji keyboard -->
<fraction name="config_emoji_keyboard_key_width">10%p</fraction> <fraction name="config_emoji_keyboard_key_width">8.33%p</fraction>
<fraction name="config_emoji_keyboard_row_height">33%p</fraction> <fraction name="config_emoji_keyboard_row_height">40%p</fraction>
<fraction name="config_emoji_keyboard_key_letter_size">70%p</fraction> <fraction name="config_emoji_keyboard_key_letter_size">64%p</fraction>
<integer name="config_emoji_keyboard_max_page_key_count">30</integer> <fraction name="config_emoji_keyboard_key_label_size">64%p</fraction>
<integer name="config_emoji_keyboard_max_recents_key_count">36</integer>
</resources> </resources>

View file

@ -82,8 +82,9 @@
<dimen name="config_gesture_floating_preview_round_radius">3dp</dimen> <dimen name="config_gesture_floating_preview_round_radius">3dp</dimen>
<!-- Emoji keyboard --> <!-- Emoji keyboard -->
<fraction name="config_emoji_keyboard_key_width">12.5%p</fraction> <fraction name="config_emoji_keyboard_key_width">11.11%p</fraction>
<fraction name="config_emoji_keyboard_row_height">33%p</fraction> <fraction name="config_emoji_keyboard_row_height">28%p</fraction>
<fraction name="config_emoji_keyboard_key_letter_size">60%p</fraction> <fraction name="config_emoji_keyboard_key_letter_size">78%p</fraction>
<integer name="config_emoji_keyboard_max_page_key_count">24</integer> <fraction name="config_emoji_keyboard_key_label_size">78%p</fraction>
<integer name="config_emoji_keyboard_max_recents_key_count">36</integer>
</resources> </resources>

View file

@ -65,6 +65,7 @@
<!-- Emoji keyboard --> <!-- Emoji keyboard -->
<fraction name="config_emoji_keyboard_key_width">7.69%p</fraction> <fraction name="config_emoji_keyboard_key_width">7.69%p</fraction>
<fraction name="config_emoji_keyboard_row_height">33%p</fraction> <fraction name="config_emoji_keyboard_row_height">33%p</fraction>
<fraction name="config_emoji_keyboard_key_letter_size">60%p</fraction> <fraction name="config_emoji_keyboard_key_letter_size">58%p</fraction>
<integer name="config_emoji_keyboard_max_page_key_count">39</integer> <fraction name="config_emoji_keyboard_key_label_size">58%p</fraction>
<integer name="config_emoji_keyboard_max_recents_key_count">39</integer>
</resources> </resources>

View file

@ -81,7 +81,8 @@
<!-- Emoji keyboard --> <!-- Emoji keyboard -->
<fraction name="config_emoji_keyboard_key_width">10%p</fraction> <fraction name="config_emoji_keyboard_key_width">10%p</fraction>
<fraction name="config_emoji_keyboard_row_height">33%p</fraction> <fraction name="config_emoji_keyboard_row_height">30%p</fraction>
<fraction name="config_emoji_keyboard_key_letter_size">68%p</fraction> <fraction name="config_emoji_keyboard_key_letter_size">64%p</fraction>
<integer name="config_emoji_keyboard_max_page_key_count">30</integer> <fraction name="config_emoji_keyboard_key_label_size">64%p</fraction>
<integer name="config_emoji_keyboard_max_recents_key_count">39</integer>
</resources> </resources>

View file

@ -88,9 +88,10 @@
<!-- Emoji keyboard --> <!-- Emoji keyboard -->
<fraction name="config_emoji_keyboard_key_width">12.5%p</fraction> <fraction name="config_emoji_keyboard_key_width">12.5%p</fraction>
<fraction name="config_emoji_keyboard_row_height">33%p</fraction> <fraction name="config_emoji_keyboard_row_height">28%p</fraction>
<fraction name="config_emoji_keyboard_key_letter_size">68%p</fraction> <fraction name="config_emoji_keyboard_key_letter_size">78%p</fraction>
<integer name="config_emoji_keyboard_max_page_key_count">24</integer> <fraction name="config_emoji_keyboard_key_label_size">78%p</fraction>
<integer name="config_emoji_keyboard_max_recents_key_count">32</integer>
<!-- Key codes of hardware keys that can be used to toggle the Emoji layout. <!-- Key codes of hardware keys that can be used to toggle the Emoji layout.
Each array defines a comma-separated tuple containing: Each array defines a comma-separated tuple containing:

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,8 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="60%p" latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -21,7 +21,8 @@
<Keyboard <Keyboard
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="90%p" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows

View file

@ -22,7 +22,7 @@
xmlns:latin="http://schemas.android.com/apk/res-auto" xmlns:latin="http://schemas.android.com/apk/res-auto"
latin:keyWidth="@fraction/config_emoji_keyboard_key_width" latin:keyWidth="@fraction/config_emoji_keyboard_key_width"
latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size" latin:keyLetterSize="@fraction/config_emoji_keyboard_key_letter_size"
latin:keyLabelSize="60%p" latin:keyLabelSize="@fraction/config_emoji_keyboard_key_label_size"
latin:rowHeight="@fraction/config_emoji_keyboard_row_height" latin:rowHeight="@fraction/config_emoji_keyboard_row_height"
> >
<GridRows <GridRows