a
554325746@qq.com
2019-12-31 fa104829ecef68865e5e3bce174515009c6d687b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
/*
 * Copyright (C) 2015-present, osfans
 * waxaca@163.com https://github.com/osfans
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
package com.osfans.trime;
 
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
 
/** 從YAML中加載鍵盤配置,包含多個{@link Key 按鍵}。 */
public class Keyboard {
  public static final int EDGE_LEFT = 0x01;
  public static final int EDGE_RIGHT = 0x02;
  public static final int EDGE_TOP = 0x04;
  public static final int EDGE_BOTTOM = 0x08;
  private static final int GRID_WIDTH = 10;
  private static final int GRID_HEIGHT = 5;
  private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
  private static final String TAG = Keyboard.class.getSimpleName();
  /** Number of key widths from current touch point to search for nearest keys. */
  public static float SEARCH_DISTANCE = 1.4f;
  /** 按鍵默認水平間距 */
  private int mDefaultHorizontalGap;
  /** 默認鍵寬 */
  private int mDefaultWidth;
  /** 默認鍵高 */
  private int mDefaultHeight;
  /** 默認行距 */
  private int mDefaultVerticalGap;
  /** 默認按鍵圓角半徑 */
  private float mRoundCorner;
  /** 鍵盤背景 */
  private Drawable mBackground;
  /** 鍵盤的Shift鍵是否按住 */
  private boolean mShifted;
  /** 鍵盤的Shift鍵 */
  private Key mShiftKey;
  /** Total height of the keyboard, including the padding and keys */
  private int mTotalHeight;
  /**
   * Total width of the keyboard, including left side gaps and keys, but not any gaps on the right
   * side.
   */
  private int mTotalWidth;
  /** List of keys in this keyboard */
  private List<Key> mKeys;
 
  private List<Key> mComposingKeys;
  private int mMetaState;
  /** Width of the screen available to fit the keyboard */
  private int mDisplayWidth;
  /** Keyboard mode, or zero, if none. */
  private int mAsciiMode;
 
  // Variables for pre-computing nearest keys.
  private String mLabelTransform;
  private int mCellWidth;
  private int mCellHeight;
  private int[][] mGridNeighbors;
  private int mProximityThreshold;
 
  private boolean mLock; //切換程序時記憶鍵盤
  private String mAsciiKeyboard; //英文鍵盤
 
  /**
   * Creates a keyboard from the given xml key layout file.
   *
   * @param context the application or service context
   */
  public Keyboard(Context context) {
    DisplayMetrics dm = context.getResources().getDisplayMetrics();
    mDisplayWidth = dm.widthPixels;
    /* Height of the screen */
    int mDisplayHeight = dm.heightPixels;
    //Log.v(TAG, "keyboard's display metrics:" + dm);
 
    Config config = Config.get();
    mDefaultHorizontalGap = 10;//config.getPixel("horizontal_gap");
    mDefaultVerticalGap = 10;//config.getPixel("vertical_gap");
    mDefaultWidth = (int) (mDisplayWidth * config.getDouble("key_width") / 100);
    mDefaultHeight = config.getPixel("key_height");
    mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
    mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
    mRoundCorner = 20;//config.getFloat("round_corner");
    mBackground = new ColorDrawable(Color.parseColor("#000717"));//= config.getColorDrawable("keyboard_back_color");
 
    mKeys = new ArrayList<Key>();
    mComposingKeys = new ArrayList<Key>();
  }
  /**
   * Creates a blank keyboard from the given resource file and populates it with the specified
   * characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
   *
   * <p>
   *
   * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
   * possible in each row.
   *
   * @param context the application or service context
   * @param characters the list of characters to display on the keyboard. One key will be created
   *     for each character.
   * @param columns the number of columns of keys to display. If this number is greater than the
   *     number of keys that can fit in a row, it will be ignored. If this number is -1, the
   *     keyboard will fit as many keys as possible in each row.
   * @param horizontalPadding 按鍵水平間距
   */
  public Keyboard(Context context, CharSequence characters, int columns, int horizontalPadding) {
    this(context);
    int x = 0;
    int y = 0;
    int column = 0;
    mTotalWidth = 0;
 
    final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
    for (int i = 0; i < characters.length(); i++) {
      char c = characters.charAt(i);
      if (column >= maxColumns || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
        x = 0;
        y += mDefaultVerticalGap + mDefaultHeight;
        column = 0;
      }
      final Key key = new Key(this);
      key.setX(x);
      key.setY(y);
      key.setWidth(mDefaultWidth);
      key.setHeight(mDefaultHeight);
      key.setGap(mDefaultHorizontalGap);
      key.events[0] = new Event(this, String.valueOf(c));
      column++;
      x += key.getWidth() + key.getGap();
      mKeys.add(key);
      if (x > mTotalWidth) {
        mTotalWidth = x;
      }
    }
    mTotalHeight = y + mDefaultHeight;
  }
 
  public Keyboard(Context context, String name) {
    this(context);
    Map<String, Object> m = Config.get().getKeyboard(name);
    mLabelTransform = Config.getString(m, "label_transform", "none");
    mAsciiMode = Config.getInt(m, "ascii_mode", 1);
    if (mAsciiMode == 0) mAsciiKeyboard = Config.getString(m, "ascii_keyboard");
    mLock = Config.getBoolean(m, "lock", false);
    int columns = Config.getInt(m, "columns", 30);
    int defaultWidth = (int) (Config.getDouble(m, "width", 0) * mDisplayWidth / 100);
    if (defaultWidth == 0) defaultWidth = mDefaultWidth;
    int height = Config.getPixel(m, "height", 0);
    int defaultHeight = (height > 0) ? height : mDefaultHeight;
    int rowHeight = defaultHeight;
    List<Map<String, Object>> lm = (List<Map<String, Object>>) m.get("keys");
 
    if (m.containsKey("horizontal_gap"))
      mDefaultHorizontalGap = Config.getPixel(m, "horizontal_gap");
    if (m.containsKey("vertical_gap")) mDefaultVerticalGap = Config.getPixel(m, "vertical_gap");
    if (m.containsKey("round_corner")) mRoundCorner = Config.getFloat(m, "round_corner");
    if (m.containsKey("keyboard_back_color")) {
      Drawable background = Config.getColorDrawable(m, "keyboard_back_color");
      if (background != null) mBackground = background;
    }
    int x = mDefaultHorizontalGap / 2;
    int y = mDefaultVerticalGap;
    int row = 0;
    int column = 0;
    mTotalWidth = 0;
    int key_text_offset_x,
        key_text_offset_y,
        key_symbol_offset_x,
        key_symbol_offset_y,
        key_hint_offset_x,
        key_hint_offset_y,
        key_press_offset_x,
        key_press_offset_y;
    key_text_offset_x = Config.getPixel(m, "key_text_offset_x", 0);
    key_text_offset_y = Config.getPixel(m, "key_text_offset_y", 0);
    key_symbol_offset_x = Config.getPixel(m, "key_symbol_offset_x", 0);
    key_symbol_offset_y = Config.getPixel(m, "key_symbol_offset_y", 0);
    key_hint_offset_x = Config.getPixel(m, "key_hint_offset_x", 0);
    key_hint_offset_y = Config.getPixel(m, "key_hint_offset_y", 0);
    key_press_offset_x = Config.getInt(m, "key_press_offset_x", 0);
    key_press_offset_y = Config.getInt(m, "key_press_offset_y", 0);
 
    final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
    for (Map<String, Object> mk : lm) {
      int gap = mDefaultHorizontalGap;
      int w = (int) (Config.getDouble(mk, "width", 0) * mDisplayWidth / 100);
      if (w == 0 && mk.containsKey("click")) w = defaultWidth;
      w -= gap;
      if (column >= maxColumns || x + w > mDisplayWidth) {
        x = gap / 2;
        y += mDefaultVerticalGap + rowHeight;
        column = 0;
        row++;
        if (mKeys.size() > 0) mKeys.get(mKeys.size() - 1).edgeFlags |= Keyboard.EDGE_RIGHT;
      }
      if (column == 0) {
        int heightK = Config.getPixel(mk, "height", 0);
        rowHeight = (heightK > 0) ? heightK : defaultHeight;
      }
      if (!mk.containsKey("click")) { //無按鍵事件
        x += w + gap;
        continue; //縮進
      }
 
      final Key key = new Key(this, mk);
      key.setKey_text_offset_x(Config.getPixel(mk, "key_text_offset_x", key_text_offset_x));
      key.setKey_text_offset_y(Config.getPixel(mk, "key_text_offset_y", key_text_offset_y));
      key.setKey_symbol_offset_x(Config.getPixel(mk, "key_symbol_offset_x", key_symbol_offset_x));
      key.setKey_symbol_offset_y(Config.getPixel(mk, "key_symbol_offset_y", key_symbol_offset_y));
      key.setKey_hint_offset_x(Config.getPixel(mk, "key_hint_offset_x", key_hint_offset_x));
      key.setKey_hint_offset_y(Config.getPixel(mk, "key_hint_offset_y", key_hint_offset_y));
      key.setKey_press_offset_x(Config.getInt(mk, "key_press_offset_x", key_press_offset_x));
      key.setKey_press_offset_y(Config.getInt(mk, "key_press_offset_y", key_press_offset_y));
 
      key.setX(x);
      key.setY(y);
      int right_gap = Math.abs(mDisplayWidth - x - w - gap / 2);
      //右側不留白
      key.setWidth((right_gap <= mDisplayWidth / 100) ? mDisplayWidth - x - gap / 2 : w);
      key.setHeight(rowHeight);
      key.setGap(gap);
      key.setRow(row);
      key.setColumn(column);
      column++;
      x += key.getWidth() + key.getGap();
      mKeys.add(key);
      if (x > mTotalWidth) {
        mTotalWidth = x;
      }
    }
    if (mKeys.size() > 0) mKeys.get(mKeys.size() - 1).edgeFlags |= Keyboard.EDGE_RIGHT;
    mTotalHeight = y + rowHeight + mDefaultVerticalGap;
    for (Key key : mKeys) {
      if (key.getColumn() == 0) key.edgeFlags |= Keyboard.EDGE_LEFT;
      if (key.getRow() == 0) key.edgeFlags |= Keyboard.EDGE_TOP;
      if (key.getRow() == row) key.edgeFlags |= Keyboard.EDGE_BOTTOM;
    }
  }
 
  public Key getmShiftKey() {
    return mShiftKey;
  }
 
  public void setmShiftKey(Key mShiftKey) {
    this.mShiftKey = mShiftKey;
  }
 
  public List<Key> getmComposingKeys() {
    return mComposingKeys;
  }
 
  public List<Key> getKeys() {
    return mKeys;
  }
 
  public List<Key> getComposingKeys() {
    return mComposingKeys;
  }
 
  protected int getHorizontalGap() {
    return mDefaultHorizontalGap;
  }
 
  protected void setHorizontalGap(int gap) {
    mDefaultHorizontalGap = gap;
  }
 
  protected int getVerticalGap() {
    return mDefaultVerticalGap;
  }
 
  protected void setVerticalGap(int gap) {
    mDefaultVerticalGap = gap;
  }
 
  protected int getKeyHeight() {
    return mDefaultHeight;
  }
 
  protected void setKeyHeight(int height) {
    mDefaultHeight = height;
  }
 
  protected int getKeyWidth() {
    return mDefaultWidth;
  }
 
  protected void setKeyWidth(int width) {
    mDefaultWidth = width;
  }
 
  /**
   * Returns the total height of the keyboard
   *
   * @return the total height of the keyboard
   */
  public int getHeight() {
    return mTotalHeight;
  }
 
  public int getMinWidth() {
    return mTotalWidth;
  }
 
  private boolean hasModifier(int modifiers) {
    return (mMetaState & modifiers) != 0;
  }
 
  public boolean hasModifier() {
    return mMetaState != 0;
  }
 
  public boolean toggleModifier(int mask) {
    boolean value = !hasModifier(mask);
    if (value) mMetaState |= mask;
    else mMetaState &= ~mask;
    return value;
  }
 
  public int getModifer() {
    return mMetaState;
  }
 
  private boolean setModifier(int mask, boolean value) {
    boolean b = hasModifier(mask);
    if (b == value) return false;
    if (value) mMetaState |= mask;
    else mMetaState &= ~mask;
    return true;
  }
 
  public boolean isAlted() {
    return hasModifier(KeyEvent.META_ALT_ON);
  }
 
  public boolean isShifted() {
    return hasModifier(KeyEvent.META_SHIFT_ON);
  }
 
  public boolean isCtrled() {
    return hasModifier(KeyEvent.META_CTRL_ON);
  }
 
  /**
   * 設定鍵盤的Shift鍵狀態
   *
   * @param on 是否保持Shift按下狀態
   * @param shifted 是否按下Shift
   * @return Shift鍵狀態是否改變
   */
  public boolean setShifted(boolean on, boolean shifted) {
    on = on & shifted;
    if (mShiftKey != null) mShiftKey.setOn(on);
    return setModifier(KeyEvent.META_SHIFT_ON, on || shifted);
  }
 
  public boolean resetShifted() {
    if (mShiftKey != null && !mShiftKey.isOn()) return setModifier(KeyEvent.META_SHIFT_ON, false);
    return false;
  }
 
  private void computeNearestNeighbors() {
    // Round-up so we don't have any pixels outside the grid
    mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
    mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
    mGridNeighbors = new int[GRID_SIZE][];
    int[] indices = new int[mKeys.size()];
    final int gridWidth = GRID_WIDTH * mCellWidth;
    final int gridHeight = GRID_HEIGHT * mCellHeight;
    for (int x = 0; x < gridWidth; x += mCellWidth) {
      for (int y = 0; y < gridHeight; y += mCellHeight) {
        int count = 0;
        for (int i = 0; i < mKeys.size(); i++) {
          final Key key = mKeys.get(i);
          if (key.squaredDistanceFrom(x, y) < mProximityThreshold
              || key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold
              || key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1)
                  < mProximityThreshold
              || key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold
              || key.isInside(x, y)
              || key.isInside(x + mCellWidth - 1, y)
              || key.isInside(x + mCellWidth - 1, y + mCellHeight - 1)
              || key.isInside(x, y + mCellHeight - 1)) {
            indices[count++] = i;
          }
        }
        int[] cell = new int[count];
        System.arraycopy(indices, 0, cell, 0, count);
        mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
      }
    }
  }
 
  /**
   * Returns the indices of the keys that are closest to the given point.
   *
   * @param x the x-coordinate of the point
   * @param y the y-coordinate of the point
   * @return the array of integer indices for the nearest keys to the given point. If the given
   *     point is out of range, then an array of size zero is returned.
   */
  public int[] getNearestKeys(int x, int y) {
    if (mGridNeighbors == null) computeNearestNeighbors();
    if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
      int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
      if (index < GRID_SIZE) {
        return mGridNeighbors[index];
      }
    }
    return new int[0];
  }
 
  public boolean getAsciiMode() {
    return mAsciiMode != 0;
  }
 
  public String getAsciiKeyboard() {
    return mAsciiKeyboard;
  }
 
  public boolean isLabelUppercase() {
    return mLabelTransform.contentEquals("uppercase");
  }
 
  public boolean isLock() {
    return mLock;
  }
 
  public float getRoundCorner() {
    return mRoundCorner;
  }
 
  public Drawable getBackground() {
    return mBackground;
  }
}