Files
morns-keyboard-basic/srcs/juloo.keyboard2/Keyboard2View.java
T

651 lines
22 KiB
Java

package juloo.keyboard2;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.LinearGradient;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.inputmethodservice.InputMethodService;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Handler;
import android.os.Looper;
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;
import java.util.List;
import java.util.Locale;
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 KeyboardData.Key _shift_key;
/** Used to add fake pointers. */
private KeyboardData.Key _compose_key;
private Pointers _pointers;
private Pointers.Modifiers _mods;
/** Long-pressing the usage gauge key opens the usage page; a tap stays inert. */
private final Handler _gaugeHandler = new Handler(Looper.getMainLooper());
private Runnable _gaugeLongPress;
private boolean _gaugePressed;
private static int _currentWhat = 0;
private Config _config;
private float _keyWidth;
private float _mainLabelSize;
private float _subLabelSize;
private float _marginRight;
private float _marginLeft;
private float _marginBottom;
private int _insets_left = 0;
private int _insets_right = 0;
private int _insets_bottom = 0;
private Theme _theme;
private Theme.Computed _tc;
private static RectF _tmpRect = new RectF();
private static final Paint GAUGE_PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);
private static final Path GAUGE_PATH = new Path();
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_key = _keyboard.findKeyWithValue(KeyValue.SHIFT);
_compose_key = _keyboard.findKeyWithValue(KeyValue.COMPOSE);
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, KeyValue.SHIFT, latched, lock);
}
/** Called from [KeyEventHandler]. */
public void set_compose_pending(boolean pending)
{
set_fake_ptr_latched(_compose_key, KeyValue.COMPOSE, pending, false);
}
/** Called from [Keybard2.onUpdateSelection]. */
public void set_selection_state(boolean selection_state)
{
if (_config.editor_config.selection_mode_enabled)
set_fake_ptr_latched(KeyboardData.Key.EMPTY,
KeyValue.SELECTION_MODE, selection_state, true);
}
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:
cancelGaugePress();
_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 && UsageGauge.isGaugeKey(key))
startGaugePress();
else if (key != null)
_pointers.onTouchDown(tx, ty, event.getPointerId(p), key);
break;
case MotionEvent.ACTION_MOVE:
if (_gaugePressed)
{
KeyboardData.Key moved = getKeyAtPosition(event.getX(0), event.getY(0));
if (moved == null || !UsageGauge.isGaugeKey(moved))
cancelGaugePress(); // slid off the gauge: no longer a press on it
}
for (p = 0; p < event.getPointerCount(); p++)
_pointers.onTouchMove(event.getX(p), event.getY(p), event.getPointerId(p));
break;
case MotionEvent.ACTION_CANCEL:
cancelGaugePress();
_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) * _tc.row_height;
if (ty < y)
return row;
}
return null;
}
private KeyboardData.Key getKeyAtPosition(float tx, float ty)
{
KeyboardData.Row row = getRowAtPosition(ty);
float x = _marginLeft;
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;
_marginLeft = Math.max(_config.horizontal_margin, _insets_left);
_marginRight = Math.max(_config.horizontal_margin, _insets_right);
_marginBottom = _config.margin_bottom + _insets_bottom;
_keyWidth = (width - _marginLeft - _marginRight) / _keyboard.keysWidth;
_tc = new Theme.Computed(_theme, _config, _keyWidth, _keyboard);
// Compute the size of labels based on the width or the height of keys. The
// margin around keys is taken into account. Keys normal aspect ratio is
// assumed to be 3/2 for a 10 columns layout. It's generally more, the
// width computation is useful when the keyboard is unusually high.
float labelBaseSize = Math.min(
_tc.row_height - _tc.vertical_margin,
(width / 10 - _tc.horizontal_margin) * 3/2
) * _config.characterSize;
_mainLabelSize = labelBaseSize * _config.labelTextSize;
_subLabelSize = labelBaseSize * _config.sublabelTextSize;
int height =
(int)(_tc.row_height * _keyboard.keysHeight
+ _config.marginTop + _marginBottom);
setMeasuredDimension(width, height);
}
Rect _cached_exclusion_rect = new Rect();
List<Rect> _cached_exclusion_rects = Arrays.asList(_cached_exclusion_rect);
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom)
{
if (!changed)
return;
// Since SDK 30, this is done automatically:
// https://android.googlesource.com/platform/frameworks/base/+/android11-release/core/java/android/inputmethodservice/InputMethodService.java#852
if (VERSION.SDK_INT == 29)
{
// Disable the back-gesture on the keyboard area
_cached_exclusion_rect.set(
left + (int)_marginLeft,
top + (int)_config.marginTop,
right - (int)_marginRight,
bottom - (int)_marginBottom);
setSystemGestureExclusionRects(_cached_exclusion_rects);
}
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets wi)
{
// LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS is set in [Keyboard2#updateSoftInputWindowLayoutParams] for SDK_INT >= 35.
if (VERSION.SDK_INT < 35)
return wi;
int insets_types =
WindowInsets.Type.systemBars()
| WindowInsets.Type.displayCutout();
Insets insets = wi.getInsets(insets_types);
_insets_left = insets.left;
_insets_right = insets.right;
_insets_bottom = insets.bottom;
return WindowInsets.CONSUMED;
}
/** 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)
{
float y = _tc.margin_top;
for (KeyboardData.Row row : _keyboard.rows)
{
y += row.shift * _tc.row_height;
float x = _marginLeft + _tc.margin_left;
float keyH = row.height * _tc.row_height - _tc.vertical_margin;
for (KeyboardData.Key k : row.keys)
{
x += k.shift * _keyWidth;
float keyW = _keyWidth * k.width - _tc.horizontal_margin;
boolean isKeyDown = _pointers.isKeyDown(k);
Theme.Computed.Key tc_key;
if (isKeyDown)
tc_key = _tc.key_activated;
else
switch (k.role)
{
case Action: tc_key = _tc.key_action; break;
case Space_bar: tc_key = _tc.key_space_bar; break;
case Suggestion: tc_key = _tc.key_suggestion; break;
default:
case Normal: tc_key = _tc.key; break;
}
drawKeyFrame(canvas, x, y, keyW, keyH, tc_key);
if (UsageGauge.isGaugeKey(k))
drawUsageGauge(canvas, x, y, keyW, keyH, tc_key);
else if (k.keys[0] != null)
drawLabel(canvas, k.keys[0], keyW / 2f + x, y, keyH, isKeyDown, tc_key);
for (int i = 1; i < 9; i++)
{
if (k.keys[i] != null)
drawSubLabel(canvas, k.keys[i], x, y, keyW, keyH, i, isKeyDown, tc_key);
}
if (!UsageGauge.isGaugeKey(k))
drawIndication(canvas, k, x, y, keyW, keyH, _tc);
x += _keyWidth * k.width;
}
y += row.height * _tc.row_height;
}
}
/** Holding the gauge key opens the usage page. Short taps do nothing: the key never types. */
private void startGaugePress()
{
cancelGaugePress();
_gaugePressed = true;
_gaugeLongPress = new Runnable()
{
public void run()
{
if (!_gaugePressed)
return;
_gaugePressed = false;
_gaugeLongPress = null;
openUsagePage();
}
};
// Same delay the keyboard uses for every other long press, so it feels like the rest of it.
_gaugeHandler.postDelayed(_gaugeLongPress, _config != null ? _config.longPressTimeout : 600L);
}
private void cancelGaugePress()
{
_gaugePressed = false;
if (_gaugeLongPress != null)
{
_gaugeHandler.removeCallbacks(_gaugeLongPress);
_gaugeLongPress = null;
}
}
private void openUsagePage()
{
try
{
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(UsageGauge.USAGE_PAGE_URL));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
}
catch (Exception e)
{
// No browser, or the launcher refused: the keyboard must never crash over a long press.
}
}
private void drawUsageGauge(Canvas canvas, float x, float y, float keyW,
float keyH, Theme.Computed.Key tc)
{
UsageGauge.Reading reading = UsageGauge.cachedReading();
boolean stale = !_config.usageGaugeEnabled
|| UsageGauge.isStaleNow(_config.usageGaugeToken);
float inset = Math.max(1.f, tc.border_width);
RectF bounds = new RectF(x + inset, y + inset, x + keyW - inset, y + keyH - inset);
GAUGE_PATH.reset();
GAUGE_PATH.addRoundRect(bounds, tc.border_radius, tc.border_radius, Path.Direction.CW);
canvas.save();
canvas.clipPath(GAUGE_PATH);
if (stale || reading == null)
{
GAUGE_PAINT.setShader(null);
GAUGE_PAINT.setColor(0xff55585e);
canvas.drawRect(bounds, GAUGE_PAINT);
}
else
{
float level = UsageGauge.fillFraction(reading);
int hue = UsageGauge.hueColor(level);
GAUGE_PAINT.setColor(tc.bg_paint.getColor());
canvas.drawRect(bounds, GAUGE_PAINT);
if (level > 0)
{
float fillTop = bounds.bottom - bounds.height() * level;
int darker = blendColor(hue, 0xff000000, 0.22f);
GAUGE_PAINT.setShader(new LinearGradient(0, bounds.bottom, 0, fillTop,
darker, hue, Shader.TileMode.CLAMP));
canvas.drawRect(bounds.left, fillTop, bounds.right, bounds.bottom, GAUGE_PAINT);
GAUGE_PAINT.setShader(null);
}
}
String label = reading == null ? "—" : String.format(Locale.US, "%.1f", reading.spentTodayUsd);
float size = Math.min(_subLabelSize, keyW * 0.40f);
GAUGE_PAINT.setTextSize(size);
GAUGE_PAINT.setTypeface(android.graphics.Typeface.DEFAULT);
GAUGE_PAINT.setTextAlign(Paint.Align.CENTER);
// Stale hides the number behind a dash; a partial day keeps the number but dims it, so a
// floor can never be read as the day's total. A low balance stays red: that reading is live.
int labelColor = stale ? 0xff92949a :
(UsageGauge.isLowBalance(reading) ? 0xffe53935 :
(reading.partial ? 0xff92949a : _theme.labelColor));
if (!stale)
{
int luminance = (android.graphics.Color.red(labelColor) * 299
+ android.graphics.Color.green(labelColor) * 587
+ android.graphics.Color.blue(labelColor) * 114) / 1000;
GAUGE_PAINT.setStyle(Paint.Style.STROKE);
GAUGE_PAINT.setStrokeWidth(Math.max(1.f, size * 0.12f));
GAUGE_PAINT.setColor(luminance > 140 ? 0xff202020 : 0xfff5f5f5);
canvas.drawText(label, bounds.centerX(),
(keyH - GAUGE_PAINT.ascent() - GAUGE_PAINT.descent()) / 2f + y, GAUGE_PAINT);
GAUGE_PAINT.setStyle(Paint.Style.FILL);
}
GAUGE_PAINT.setColor(labelColor);
canvas.drawText(label, bounds.centerX(),
(keyH - GAUGE_PAINT.ascent() - GAUGE_PAINT.descent()) / 2f + y, GAUGE_PAINT);
canvas.restore();
}
private static int blendColor(int from, int to, float amount)
{
return android.graphics.Color.rgb(
(int)(android.graphics.Color.red(from) * (1 - amount) + android.graphics.Color.red(to) * amount),
(int)(android.graphics.Color.green(from) * (1 - amount) + android.graphics.Color.green(to) * amount),
(int)(android.graphics.Color.blue(from) * (1 - amount) + android.graphics.Color.blue(to) * amount));
}
@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,
Theme.Computed.Key tc)
{
float r = tc.border_radius;
float w = tc.border_width;
float padding = w / 2.f;
_tmpRect.set(x + padding, y + padding, x + keyW - padding, y + keyH - padding);
canvas.drawRoundRect(_tmpRect, r, r, tc.bg_paint);
if (w > 0.f)
{
float overlap = r - r * 0.85f + w; // sin(45°)
drawBorder(canvas, x, y, x + overlap, y + keyH, tc.border_left_paint, tc);
drawBorder(canvas, x + keyW - overlap, y, x + keyW, y + keyH, tc.border_right_paint, tc);
drawBorder(canvas, x, y, x + keyW, y + overlap, tc.border_top_paint, tc);
drawBorder(canvas, x, y + keyH - overlap, x + keyW, y + keyH, tc.border_bottom_paint, tc);
}
}
/** 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, Paint paint, Theme.Computed.Key tc)
{
float r = tc.border_radius;
canvas.save();
canvas.clipRect(clipl, clipt, clipr, clipb);
canvas.drawRoundRect(_tmpRect, r, r, paint);
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;
}
return _theme.pressedColor;
}
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, Theme.Computed.Key tc)
{
kv = modifyKey(kv, _mods);
if (kv == null)
return;
float textSize = scaleTextSize(kv, true);
// DeepSeek peak/off-peak: the space bar's joystick glyph doubles as the rate
// indicator (neon blue = off-peak, neon pink = peak).
int color = kv.getKind() == KeyValue.Kind.Editing
&& KeyValue.Editing.SPACE_BAR.equals(kv.getEditing())
? DeepSeekPeak.colorFor(System.currentTimeMillis())
: labelColor(kv, isKeyDown, false);
// The emoji badge (0xE01D, Alt's top-right corner) sits in a corner slot,
// which labelColor() would dim to secondaryLabelColor. Draw it in labelColor
// so it matches the rest of the keyboard's labels.
String glyph = kv.getString();
if (glyph != null && glyph.length() == 1 && glyph.charAt(0) == 0xE01D)
color = _theme.labelColor;
Paint p = tc.label_paint(kv.hasFlagsAny(KeyValue.FLAG_KEY_FONT), color, 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,
Theme.Computed.Key tc)
{
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, false);
Paint p = tc.sublabel_paint(kv.hasFlagsAny(KeyValue.FLAG_KEY_FONT), labelColor(kv, isKeyDown, true), textSize, a);
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, Theme.Computed tc)
{
if (k.indication == null || k.indication.equals(""))
return;
Paint p = tc.indication_paint;
p.setTextSize(_subLabelSize);
canvas.drawText(k.indication, 0, k.indication.length(),
x + keyW / 2f, (keyH - p.ascent() - p.descent()) * 4/5 + y, p);
}
private float scaleTextSize(KeyValue k, boolean main_label)
{
float smaller_font = k.hasFlagsAny(KeyValue.FLAG_SMALLER_FONT) ? 0.75f : 1.f;
float label_size = main_label ? _mainLabelSize : _subLabelSize;
return label_size * smaller_font;
}
}