package juloo.keyboard2; import android.content.Context; import android.content.ContextWrapper; import android.graphics.Canvas; import android.graphics.Insets; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.inputmethodservice.InputMethodService; import android.os.Build.VERSION; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.View; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowMetrics; import java.util.Arrays; public class Keyboard2View extends View implements View.OnTouchListener, Pointers.IPointerEventHandler { private KeyboardData _keyboard; /** The key holding the shift key is used to set shift state from autocapitalisation. */ private KeyValue _shift_kv; private KeyboardData.Key _shift_key; /** Used to add fake pointers. */ private KeyValue _compose_kv; private KeyboardData.Key _compose_key; private Pointers _pointers; private Pointers.Modifiers _mods; private static int _currentWhat = 0; private Config _config; private float _keyWidth; private Theme _theme; private static RectF _tmpRect = new RectF(); enum Vertical { TOP, CENTER, BOTTOM } public Keyboard2View(Context context, AttributeSet attrs) { super(context, attrs); _theme = new Theme(getContext(), attrs); _config = Config.globalConfig(); _pointers = new Pointers(this, _config); refresh_navigation_bar(context); setOnTouchListener(this); int layout_id = (attrs == null) ? 0 : attrs.getAttributeResourceValue(null, "layout", 0); if (layout_id == 0) reset(); else setKeyboard(KeyboardData.load(getResources(), layout_id)); } private Window getParentWindow(Context context) { if (context instanceof InputMethodService) return ((InputMethodService)context).getWindow().getWindow(); if (context instanceof ContextWrapper) return getParentWindow(((ContextWrapper)context).getBaseContext()); return null; } public void refresh_navigation_bar(Context context) { if (VERSION.SDK_INT < 21) return; // The intermediate Window is a [Dialog]. Window w = getParentWindow(context); w.setNavigationBarColor(_theme.colorNavBar); if (VERSION.SDK_INT < 26) return; int uiFlags = getSystemUiVisibility(); if (_theme.isLightNavBar) uiFlags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; else uiFlags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; setSystemUiVisibility(uiFlags); } public void setKeyboard(KeyboardData kw) { _keyboard = kw; _shift_kv = KeyValue.getKeyByName("shift"); _shift_key = _keyboard.findKeyWithValue(_shift_kv); if (_shift_key == null) { _shift_kv = _shift_kv.withFlags(_shift_kv.getFlags() | KeyValue.FLAG_LOCK); _shift_key = _keyboard.findKeyWithValue(_shift_kv); } _compose_kv = KeyValue.getKeyByName("compose"); _compose_key = _keyboard.findKeyWithValue(_compose_kv); KeyModifier.set_modmap(_keyboard.modmap); reset(); } public void reset() { _mods = Pointers.Modifiers.EMPTY; _pointers.clear(); requestLayout(); invalidate(); } void set_fake_ptr_latched(KeyboardData.Key key, KeyValue kv, boolean latched, boolean lock) { if (_keyboard == null || key == null) return; _pointers.set_fake_pointer_state(key, kv, latched, lock); } /** Called by auto-capitalisation. */ public void set_shift_state(boolean latched, boolean lock) { set_fake_ptr_latched(_shift_key, _shift_kv, latched, lock); } /** Called from [KeyEventHandler]. */ public void set_compose_pending(boolean pending) { set_fake_ptr_latched(_compose_key, _compose_kv, pending, false); } public KeyValue modifyKey(KeyValue k, Pointers.Modifiers mods) { return KeyModifier.modify(k, mods); } public void onPointerDown(KeyValue k, boolean isSwipe) { updateFlags(); _config.handler.key_down(k, isSwipe); invalidate(); vibrate(); } public void onPointerUp(KeyValue k, Pointers.Modifiers mods) { // [key_up] must be called before [updateFlags]. The latter might disable // flags. _config.handler.key_up(k, mods); updateFlags(); invalidate(); } public void onPointerHold(KeyValue k, Pointers.Modifiers mods) { _config.handler.key_up(k, mods); updateFlags(); } public void onPointerFlagsChanged(boolean shouldVibrate) { updateFlags(); invalidate(); if (shouldVibrate) vibrate(); } private void updateFlags() { _mods = _pointers.getModifiers(); _config.handler.mods_changed(_mods); } @Override public boolean onTouch(View v, MotionEvent event) { int p; switch (event.getActionMasked()) { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: _pointers.onTouchUp(event.getPointerId(event.getActionIndex())); break; case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_POINTER_DOWN: p = event.getActionIndex(); float tx = event.getX(p); float ty = event.getY(p); KeyboardData.Key key = getKeyAtPosition(tx, ty); if (key != null) _pointers.onTouchDown(tx, ty, event.getPointerId(p), key); break; case MotionEvent.ACTION_MOVE: for (p = 0; p < event.getPointerCount(); p++) _pointers.onTouchMove(event.getX(p), event.getY(p), event.getPointerId(p)); break; case MotionEvent.ACTION_CANCEL: _pointers.onTouchCancel(); break; default: return (false); } return (true); } private KeyboardData.Row getRowAtPosition(float ty) { float y = _config.marginTop; if (ty < y) return null; for (KeyboardData.Row row : _keyboard.rows) { y += (row.shift + row.height) * _config.keyHeight; if (ty < y) return row; } return null; } private KeyboardData.Key getKeyAtPosition(float tx, float ty) { KeyboardData.Row row = getRowAtPosition(ty); float x = _config.horizontal_margin; if (row == null || tx < x) return null; for (KeyboardData.Key key : row.keys) { float xLeft = x + key.shift * _keyWidth; float xRight = xLeft + key.width * _keyWidth; if (tx < xLeft) return null; if (tx < xRight) return key; x = xRight; } return null; } private void vibrate() { VibratorCompat.vibrate(this, _config); } @Override public void onMeasure(int wSpec, int hSpec) { DisplayMetrics dm = getContext().getResources().getDisplayMetrics(); int width = dm.widthPixels; int height = (int)(_config.keyHeight * _keyboard.keysHeight + _config.marginTop + _config.margin_bottom); // Compatibility with display cutouts and navigation on the right if (VERSION.SDK_INT >= 30) { WindowMetrics metrics = ((WindowManager)getContext().getSystemService(Context.WINDOW_SERVICE)) .getCurrentWindowMetrics(); Insets insets = metrics.getWindowInsets().getInsetsIgnoringVisibility( WindowInsets.Type.statusBars() | WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout()); width = metrics.getBounds().width() - insets.right - insets.left; } setMeasuredDimension(width, height); _keyWidth = (width - (_config.horizontal_margin * 2)) / _keyboard.keysWidth; } @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { if (!changed) return; if (VERSION.SDK_INT >= 29) { // Disable the back-gesture on the keyboard area Rect keyboard_area = new Rect( left + (int)_config.horizontal_margin, top + (int)_config.marginTop, right - (int)_config.horizontal_margin, bottom - (int)_config.margin_bottom); setSystemGestureExclusionRects(Arrays.asList(keyboard_area)); } } /** Horizontal and vertical position of the 9 indexes. */ static final Paint.Align[] LABEL_POSITION_H = new Paint.Align[]{ Paint.Align.CENTER, Paint.Align.LEFT, Paint.Align.RIGHT, Paint.Align.LEFT, Paint.Align.RIGHT, Paint.Align.LEFT, Paint.Align.RIGHT, Paint.Align.CENTER, Paint.Align.CENTER }; static final Vertical[] LABEL_POSITION_V = new Vertical[]{ Vertical.CENTER, Vertical.TOP, Vertical.TOP, Vertical.BOTTOM, Vertical.BOTTOM, Vertical.CENTER, Vertical.CENTER, Vertical.TOP, Vertical.BOTTOM }; @Override protected void onDraw(Canvas canvas) { // Set keyboard background opacity getBackground().setAlpha(_config.keyboardOpacity); // Set keys opacity _theme.keyBgPaint.setAlpha(_config.keyOpacity); _theme.keyDownBgPaint.setAlpha(_config.keyActivatedOpacity); _theme.keyBorderPaint.setAlpha(_config.keyOpacity); float key_vertical_margin = _config.key_vertical_margin * _config.keyHeight; float key_horizontal_margin = _config.key_horizontal_margin * _keyWidth; // Add half of the key margin on the left and on the top as it's then added // on the right and on the bottom of every keys. float y = _config.marginTop + key_vertical_margin / 2; for (KeyboardData.Row row : _keyboard.rows) { y += row.shift * _config.keyHeight; float x = _config.horizontal_margin + key_horizontal_margin / 2; float keyH = row.height * _config.keyHeight - key_vertical_margin; for (KeyboardData.Key k : row.keys) { x += k.shift * _keyWidth; float keyW = _keyWidth * k.width - key_horizontal_margin; boolean isKeyDown = _pointers.isKeyDown(k); drawKeyFrame(canvas, x, y, keyW, keyH, isKeyDown); if (k.keys[0] != null) drawLabel(canvas, k.keys[0], keyW / 2f + x, y, keyH, isKeyDown); for (int i = 1; i < 9; i++) { if (k.keys[i] != null) drawSubLabel(canvas, k.keys[i], x, y, keyW, keyH, i, isKeyDown); } drawIndication(canvas, k, x, y, keyW, keyH); x += _keyWidth * k.width; } y += row.height * _config.keyHeight; } } @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); } /** Draw borders and background of the key. */ void drawKeyFrame(Canvas canvas, float x, float y, float keyW, float keyH, boolean isKeyDown) { float r = _theme.keyBorderRadius; if (_config.borderConfig) r = _config.customBorderRadius * _keyWidth; float w = (_config.borderConfig) ? _config.customBorderLineWidth : _theme.keyBorderWidth; float padding = w / 2.f; if (isKeyDown) w = _theme.keyBorderWidthActivated; _tmpRect.set(x + padding, y + padding, x + keyW - padding, y + keyH - padding); canvas.drawRoundRect(_tmpRect, r, r, isKeyDown ? _theme.keyDownBgPaint : _theme.keyBgPaint); if (w > 0.f) { _theme.keyBorderPaint.setStrokeWidth(w); float overlap = r - r * 0.85f + w; // sin(45°) drawBorder(canvas, x, y, x + overlap, y + keyH, _theme.keyBorderColorLeft); drawBorder(canvas, x + keyW - overlap, y, x + keyW, y + keyH, _theme.keyBorderColorRight); drawBorder(canvas, x, y, x + keyW, y + overlap, _theme.keyBorderColorTop); drawBorder(canvas, x, y + keyH - overlap, x + keyW, y + keyH, _theme.keyBorderColorBottom); } } /** Clip to draw a border at a time. This allows to call [drawRoundRect] several time with the same parameters but a different Paint. */ void drawBorder(Canvas canvas, float clipl, float clipt, float clipr, float clipb, int color) { Paint p = _theme.keyBorderPaint; float r = _theme.keyBorderRadius; if (_config.borderConfig) r = _config.customBorderRadius * _keyWidth; canvas.save(); canvas.clipRect(clipl, clipt, clipr, clipb); p.setColor(color); canvas.drawRoundRect(_tmpRect, r, r, p); canvas.restore(); } private int labelColor(KeyValue k, boolean isKeyDown, boolean sublabel) { if (isKeyDown) { int flags = _pointers.getKeyFlags(k); if (flags != -1) { if ((flags & Pointers.FLAG_P_LOCKED) != 0) return _theme.lockedColor; return _theme.activatedColor; } } if (k.hasFlagsAny(KeyValue.FLAG_SECONDARY | KeyValue.FLAG_GREYED)) { if (k.hasFlagsAny(KeyValue.FLAG_GREYED)) return _theme.greyedLabelColor; return _theme.secondaryLabelColor; } return sublabel ? _theme.subLabelColor : _theme.labelColor; } private void drawLabel(Canvas canvas, KeyValue kv, float x, float y, float keyH, boolean isKeyDown) { kv = modifyKey(kv, _mods); if (kv == null) return; float textSize = scaleTextSize(kv, _config.labelTextSize, keyH); Paint p = _theme.labelPaint(kv.hasFlagsAny(KeyValue.FLAG_KEY_FONT)); p.setColor(labelColor(kv, isKeyDown, false)); p.setAlpha(_config.labelBrightness); p.setTextSize(textSize); canvas.drawText(kv.getString(), x, (keyH - p.ascent() - p.descent()) / 2f + y, p); } private void drawSubLabel(Canvas canvas, KeyValue kv, float x, float y, float keyW, float keyH, int sub_index, boolean isKeyDown) { Paint.Align a = LABEL_POSITION_H[sub_index]; Vertical v = LABEL_POSITION_V[sub_index]; kv = modifyKey(kv, _mods); if (kv == null) return; float textSize = scaleTextSize(kv, _config.sublabelTextSize, keyH); Paint p = _theme.subLabelPaint(kv.hasFlagsAny(KeyValue.FLAG_KEY_FONT), a); p.setColor(labelColor(kv, isKeyDown, true)); p.setAlpha(_config.labelBrightness); p.setTextSize(textSize); float subPadding = _config.keyPadding; if (v == Vertical.CENTER) y += (keyH - p.ascent() - p.descent()) / 2f; else y += (v == Vertical.TOP) ? subPadding - p.ascent() : keyH - subPadding - p.descent(); if (a == Paint.Align.CENTER) x += keyW / 2f; else x += (a == Paint.Align.LEFT) ? subPadding : keyW - subPadding; String label = kv.getString(); int label_len = label.length(); // Limit the label of string keys to 3 characters if (label_len > 3 && kv.getKind() == KeyValue.Kind.String) label_len = 3; canvas.drawText(label, 0, label_len, x, y, p); } private void drawIndication(Canvas canvas, KeyboardData.Key k, float x, float y, float keyW, float keyH) { boolean special_font = false; String indic; int indic_length; float text_size; if (k.indication != null) { indic = k.indication; indic_length = indic.length(); text_size = keyH * _config.sublabelTextSize * _config.characterSize; } else if (k.anticircle != null) { indic = k.anticircle.getString(); // 3 character limit like regular labels indic_length = Math.min(indic.length(), 3); special_font = k.anticircle.hasFlagsAny(KeyValue.FLAG_KEY_FONT); text_size = scaleTextSize(k.anticircle, _config.sublabelTextSize, keyH); } else { return; } Paint p = _theme.indicationPaint(special_font); p.setColor(_theme.subLabelColor); p.setTextSize(text_size); canvas.drawText(indic, 0, indic_length, x + keyW / 2f, (keyH - p.ascent() - p.descent()) * 4/5 + y, p); } private float scaleTextSize(KeyValue k, float rel_size, float keyH) { float smaller_font = k.hasFlagsAny(KeyValue.FLAG_SMALLER_FONT) ? 0.75f : 1.f; return keyH * rel_size * smaller_font * _config.characterSize; } }