package com.android.demo.widget; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.Bitmap.Config; import android.graphics.Paint.FontMetrics; import android.graphics.Paint.Style; import android.graphics.Path.Direction; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.text.Editable; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.widget.EditText; import android.widget.PopupWindow; import com.android.demo.R; public class VirtualKeyboard extends View { private static final int ZOOM_RADIUS = 58; private static final int ZOOM_SIZE = 131; private static final int HANDLE_SIZE = 33; private static final long ZOOM_DELAY_TIME = 250; private static final int BUTTON_SIZE = 64; private Bitmap kbdBitmap; private ButtonDrawable btnDrawable; private ButtonDrawable btnCtrlDrawable; private ButtonDrawable btnSelectedDrawable; private ButtonDrawable btnToggleSelectedDrawable; private List<VirtualButton> buttons; private Paint letterPaint; private Rect srcRect; private Point dstPoint; private Bitmap zoomBitmap; private VirtualButton pressedButton; public Paint backgroundPaint; private int state; private PopupWindow zoom; private ZoomView zoomView; private Bitmap handleBitmap; private ToggleVirtualButton shiftBtn; private VirtualButton enterBtn; private VirtualButton symbolsBtn; private boolean zoomVisible; private Canvas kbdCanvas; public VirtualKeyboard(Context context, AttributeSet attrs) { super(context, attrs); Resources res = context.getResources(); btnDrawable = new ButtonDrawable(res, R.drawable.vk_button); btnCtrlDrawable = new ButtonDrawable(res, R.drawable.vk_button_ctrl); btnSelectedDrawable = new ButtonDrawable(res, R.drawable.vk_button_selected); btnToggleSelectedDrawable = new ButtonDrawable(res, R.drawable.vk_button_toggle_selected); BitmapDrawable zoomDrawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.vk_zoom); zoomBitmap = zoomDrawable.getBitmap(); BitmapDrawable handleDrawable = (BitmapDrawable) context.getResources().getDrawable(R.drawable.vk_zoom_handle); handleBitmap = handleDrawable.getBitmap(); buttons = new ArrayList<VirtualButton>(); letterPaint = new Paint(); letterPaint.setAntiAlias(true); letterPaint.setColor(0xff000000); letterPaint.setTypeface(Typeface.DEFAULT_BOLD); letterPaint.setTextSize(40); backgroundPaint = new Paint(); backgroundPaint.setColor(0xffffffff); backgroundPaint.setStyle(Style.FILL); srcRect = new Rect(0, 0, 2 * ZOOM_RADIUS, 2 * ZOOM_RADIUS); dstPoint = new Point(0, 0); pressedButton = null; state = 0; zoomView = new ZoomView(getContext()); zoom = new PopupWindow(zoomView, ZOOM_SIZE, ZOOM_SIZE); zoom.setAnimationStyle(android.R.style.Animation_Toast); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int w = MeasureSpec.getSize(widthMeasureSpec); int h = MeasureSpec.getSize(heightMeasureSpec); createBitmap(2 * w + BUTTON_SIZE, 2 * h + BUTTON_SIZE); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } private void createBitmap(int w, int h) { kbdBitmap = Bitmap.createBitmap(w, h, Config.ARGB_8888); kbdCanvas = new Canvas(kbdBitmap); Resources res = getResources(); VirtualButton vButton; String packageName = R.class.getPackage().getName(); XmlPullParser xpp = getResources().getXml(R.xml.layout); try { while (xpp.getEventType() != XmlPullParser.END_DOCUMENT) { if (xpp.getEventType() == XmlPullParser.START_TAG) { String name = xpp.getName(); if (!name.equals("Layout")) { String sX = xpp.getAttributeValue(null, "x"); float x = 0; if (sX != null) { x = Float.parseFloat(sX); } String sY = xpp.getAttributeValue(null, "y"); float y = 0; if (sY != null) { y = Float.parseFloat(sY); } String sWidth = xpp.getAttributeValue(null, "width"); float width = 1f; if (sWidth != null) { width = Float.parseFloat(sWidth); } String sText = xpp.getAttributeValue(null, "text"); String sDrawable = xpp.getAttributeValue(null, "drawable"); if (name.equals("ShiftButton")) { vButton = shiftBtn = new ToggleVirtualButton(x, y, width, ""); } else if (name.equals("BackspaceButton")) { vButton = new VirtualButton(x, y, width, "\b\b\b\b"); } else if (name.equals("SymbolsButton")) { vButton = symbolsBtn = new ToggleVirtualButton(x, y, width, sText); } else if (name.equals("EnterButton")) { vButton = enterBtn = new ControlVirtualButton(x, y, width, sText); } else { if (sText.length() < 4) { Log.d("VirtualKeyboard", " Button x:" + x + " y:" + y + " has text shorter than 4 characters !!"); sText = "????"; } vButton = new VirtualButton(x, y, width, sText); } if (sDrawable != null) { int id = res.getIdentifier(sDrawable, null, packageName); if (id == 0) { throw new RuntimeException("resource: " + sDrawable + " not found"); } vButton.setBitmap(id); } buttons.add(vButton); } } xpp.next(); } } catch (XmlPullParserException e) { Log.d("VirtualKeyboard", "XmlPullParserException", e); } catch (IOException e) { Log.d("VirtualKeyboard", "IOException", e); } drawLayout(); } @Override protected void onDraw(Canvas canvas) { canvas.scale(0.5f, 0.5f); canvas.translate(-BUTTON_SIZE/2, -BUTTON_SIZE/2); canvas.drawBitmap(kbdBitmap, 0, 0, null); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_MOVE) { // offset getX & getY by BUTTON_SIZE/4: bitmap padding int x = (int) event.getX() + BUTTON_SIZE/4; int y = (int) event.getY() + BUTTON_SIZE/4; srcRect.offsetTo(2 * x - ZOOM_RADIUS, 2 * y - ZOOM_RADIUS); dstPoint.set(x - ZOOM_RADIUS, y - 3 * ZOOM_RADIUS / 2); int xx = x * 2; int yy = y * 2; boolean in = false; for (VirtualButton button : buttons) { if (button.contains(xx, yy)) { in = true; if (pressedButton == button) { break; } if (pressedButton != null) { pressedButton.up(false); } pressedButton = button; pressedButton.down(); break; } } if (!in && pressedButton != null) { pressedButton.up(false); pressedButton = null; } if (pressedButton != null) { srcRect.offsetTo(2 * x - ZOOM_RADIUS, pressedButton.getRect().centerY() - ZOOM_RADIUS); dstPoint.set(x - ZOOM_RADIUS, pressedButton.getRect().top/2 - 3 * ZOOM_RADIUS / 2); } if (srcRect.left < 0) { srcRect.offset(-srcRect.left, 0); } else if (srcRect.right > kbdBitmap.getWidth()) { srcRect.offset(kbdBitmap.getWidth() - srcRect.right, 0); } if (srcRect.top < 0) { srcRect.offset(0, -srcRect.top); } else if (srcRect.bottom > kbdBitmap.getHeight()) { srcRect.offset(0, kbdBitmap.getHeight() - srcRect.bottom); } dstPoint.offset(-BUTTON_SIZE/4, -BUTTON_SIZE/4); if (y < BUTTON_SIZE/4) { // hide zoom if out of bounds zoom.dismiss(); invalidate(); return true; } if (action == MotionEvent.ACTION_DOWN) { removeCallbacks(showZoom); postDelayed(showZoom, ZOOM_DELAY_TIME); } else { if (zoomVisible && !zoom.isShowing()) { // zoom was hidden since out of bounds (y < 0) showZoom.run(); } zoom.update(getLeft() + dstPoint.x, getTop() + dstPoint.y, -1, -1); } zoomView.invalidate(); } else if (action == MotionEvent.ACTION_UP) { if (pressedButton != null) { pressedButton.up(true); if (pressedButton == shiftBtn) { state ^= 1; drawLayout(); } else if (pressedButton == symbolsBtn) { state ^= 2; drawLayout(); } else if (pressedButton == enterBtn) { // TODO implement me? } else { updateEditText(pressedButton.text.charAt(state)); } pressedButton = null; } pressedButton = null; removeCallbacks(showZoom); zoomVisible = false; zoom.dismiss(); } invalidate(); return true; } private void drawLayout() { // long t0 = System.currentTimeMillis(); kbdCanvas.drawARGB(255, 255, 255, 255); for (VirtualButton button : buttons) { button.draw(kbdCanvas); } // long t1 = System.currentTimeMillis(); // Log.d("VirtualKeyboard", "drawLayout took " + (t1-t0) + "ms"); } private void updateEditText(char c) { int start = edit.getSelectionStart(); int end = edit.getSelectionEnd(); if (start > end) { int tmp = end; end = start; start = tmp; } Editable text = edit.getText(); if (c != '\b') { text.replace(start, end, String.valueOf(c), 0, 1); start++; } else { if (start == end && start > 0) { start--; } if (start + end == 0) { // start of buffer & no selection: just return return; } text.delete(start, end); } edit.setText(text); edit.setSelection(start); } Runnable showZoom = new Runnable() { public void run() { zoomVisible = true; zoom.showAtLocation(VirtualKeyboard.this, Gravity.NO_GRAVITY, getLeft() + dstPoint.x, getTop() + dstPoint.y); } }; // tmp private EditText edit; public void setUp(EditText edit) { this.edit = edit; } // end of tmp class VirtualButton { private Rect rect; protected String text; private Bitmap bitmap; public VirtualButton(float x, float y, String letters) { this(x, y, 1, letters); } public VirtualButton(float x, float y, float w, String letters) { int left = (int) (x * BUTTON_SIZE); int top = (int) (y * BUTTON_SIZE); int right = (int) ((x + w) * BUTTON_SIZE); int bottom = (int) ((y + 1) * BUTTON_SIZE); this.rect = new Rect(left, top, right, bottom); this.rect.offset(BUTTON_SIZE/2, BUTTON_SIZE/2); this.text = letters; } public void setBitmap(int resId) { BitmapDrawable drawable = (BitmapDrawable) getResources().getDrawable(resId); bitmap = drawable.getBitmap(); } public Rect getRect() { return rect; } public void up(boolean inside) { draw(kbdCanvas, btnDrawable, text.substring(state, state+1)); } public void down() { draw(kbdCanvas, null, text.substring(state, state+1)); } public boolean contains(int x, int y) { return rect.contains(x, y); } public void draw(Canvas canvas) { draw(canvas, btnDrawable, text.substring(state, state+1)); } void draw(Canvas canvas, ButtonDrawable buttonDrawable, String t) { canvas.drawRect(rect, backgroundPaint); if (buttonDrawable != null) { Bitmap bitmap = buttonDrawable.getBitmap(rect); canvas.drawBitmap(bitmap, rect.left, rect.top, null); } else { Bitmap bitmap = btnSelectedDrawable.getBitmap(rect); canvas.drawBitmap(bitmap, rect.left, rect.top, null); } if (bitmap != null) { canvas.drawBitmap(bitmap, rect.centerX() - bitmap.getWidth() / 2, rect.centerY() - bitmap.getHeight() / 2, null); } else { float x = (rect.width() - letterPaint.measureText(t)) / 2; FontMetrics fm = letterPaint.getFontMetrics(); float y = (rect.height() - letterPaint.getTextSize()) / 2 - fm.ascent - fm.descent; // TODO fix it y += 4; canvas.drawText(t, rect.left+x, rect.top+y, letterPaint); } } } class ControlVirtualButton extends VirtualButton { public ControlVirtualButton(float x, float y, float w, String letters) { super(x, y, w, letters); } public ControlVirtualButton(float x, float y, String letters) { super(x, y, letters); } @Override public void draw(Canvas canvas) { draw(canvas, btnCtrlDrawable, text); } public void up(boolean inside) { draw(kbdCanvas, btnCtrlDrawable, text); } public void down() { draw(kbdCanvas, null, text); } } class ToggleVirtualButton extends ControlVirtualButton { private boolean down; public ToggleVirtualButton(float x, float y, float w, String letters) { super(x, y, w, letters); } public ToggleVirtualButton(float x, float y, String letters) { super(x, y, letters); } @Override public void draw(Canvas canvas) { draw(canvas, down? btnToggleSelectedDrawable : btnCtrlDrawable, text); } @Override public void up(boolean inside) { if (inside) { down = !down; } draw(kbdCanvas, down? btnToggleSelectedDrawable : btnCtrlDrawable, text); } } class ZoomView extends View { private Paint zoomPaint; private Rect rect; private Path clip; public ZoomView(Context context) { super(context); zoomPaint = new Paint(); zoomPaint.setAntiAlias(true); zoomPaint.setColor(0xff008000); zoomPaint.setStyle(Style.STROKE); rect = new Rect(2, 2, 2 + ZOOM_RADIUS * 2, 2 + ZOOM_RADIUS * 2); clip = new Path(); clip.addCircle(ZOOM_RADIUS+2, ZOOM_RADIUS+2, ZOOM_RADIUS, Direction.CW); } @Override protected void onDraw(Canvas canvas) { canvas.save(); canvas.clipPath(clip); // draw zoom zoomPaint.setAlpha(255); canvas.drawBitmap(kbdBitmap, srcRect, rect, zoomPaint); canvas.restore(); // draw zoom frame zoomPaint.setAlpha(220); canvas.drawBitmap(zoomBitmap, 0, 0, zoomPaint); // draw zoom handle zoomPaint.setAlpha(255); canvas.drawBitmap(handleBitmap, ZOOM_SIZE-HANDLE_SIZE, ZOOM_SIZE-HANDLE_SIZE, zoomPaint); } } class ButtonDrawable { private Drawable drawable; private SparseArray<Bitmap> bitmapCache; public ButtonDrawable(Resources res, int id) { drawable = res.getDrawable(id); bitmapCache = new SparseArray<Bitmap>(); } public Bitmap getBitmap(Rect rect) { int w = rect.width(); int h = rect.height(); int key = (w << 16) + h; Bitmap bitmap = bitmapCache.get(key); if (bitmap == null) { drawable.setBounds(0, 0, w, h); bitmap = Bitmap.createBitmap(w, h, Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.draw(canvas); bitmapCache.put(key, bitmap); } return bitmap; } } }