在安卓开发中,有许多对键盘上UI的自定义需求,我们就来写一个自定义键盘吧!
基础篇
首先,我们先通过系统键盘来设想,如果自己做一个自定义键盘会用什么来做,嗯...有规律的列表式分布,那肯定就是列表类的布局了!
乍看之下好像是这么回事,但是先别急,我们查阅资料可以知道Android给开发者提供了一个自定义键盘的类:KeyboardView。
翻阅源码我们可以看到,他并不是我们想象中的继承自列表类的一个view,而是继承了基本视图View,那么它是怎么实现一个键盘的构建的呢?我们先看看看最简单的自定义键盘吧。
首先,KeyboardView可以直接用在布局中的。
我们可以看到大部分都是常见的view的属性,但其中有不少key开头的属性命名,这就是KeyboardView的常用自定义属性。
keyBackground为按键的背景属性,keyPreviewHeight预览视图背景高度,这里的预览视图即按下键位后弹起的放大预览的视图窗口,keyPreviewLayout则是预览视图的ui,若不设置则是默认视图,keytextColor等属性可以通过命名得知他们各自的功能,这里就不过多赘述了。
布局中写好后,下一步思考的就是如何进行自定义的键位排版呢?
安卓还给我们提供了一个类:Keyboard。
通过构造方法我们可以看出,他需要的是一个context,以及一个xml的Id,那么这个xml的作用是什么呢?
这个xml就是用来排布键位位置和大小的,其中需要用到的三个重要的标签,分别是key Row 以及 Keyboard。
Keyboard作为起始标签与结束标签,可以在其中设置keyheight 和keyWidth两个属性,抑或是horizontalGap,表示水平偏移量,还有垂直偏移量等。
Row则表示每一行的起始标签与结束标签。
那么可想而知,key就是代表每一个键位的起始与结束标签了。key标签中比较重要的两个属性则是keylabel和codes,codes表示这个按键输入内容的ASCII码,keylabel则是显示在键盘上的文字。同时也包含有horizontalGap等偏移量属性提供给开发者使用,isRepeatable则是代表按键可以长按重复触发,需要注意的是当你重新设置了keyboard后会导致原本的长按事件失效。
key对象还带有isInside方法,从命名上来看我们就可以知晓这是用来判断用户是否按到这个key。
同时key也可以单独设置width属性。下面给一段代码实例作为参考。
xml写好以后,初始化Keyboard对象,将其设置给之前之前写在布局中的view,keyboardView.setKeyboard(k1)一个简单的自定义键盘就做好了!之后设置一个监听OnKeyboardActionListener就可以将按键的codes获取到然后转换成对应的字符串写入到文本中。
进阶篇
我们通过基础篇可以知道如何写一个简单的自定义键盘,但是实现以后发现效果并不理想,原因只有一个,UI太丑,丑到变形。
那么我们接下来看看如何去写一个UI漂亮的自定义键盘。
既然要写一个漂亮的UI,那么我们自然需要知道从哪下手才行,我们联想到之前查看源码时,发现键盘是继承自View而不是ViewGroup的,那么它的每一个按键,并不是独立的view,而是一个个Drawable,即是一张张画好的图排布在一个视图上面。
我们先来看源码中是怎么去“画”键盘的。在Keyboard中有这样一段代码。
private void loadKeyboard(Context context, XmlResourceParser parser) {
boolean inKey = false;
boolean inRow = false;
boolean leftMostKey = false;
int row = 0;
int x = 0;
int y = 0;
Key key = null;
Row currentRow = null;
Resources res = context.getResources();
boolean skipRow = false;
try {
int event;
while ((event = parser.next()) != XmlResourceParser.END_DOCUMENT) {
//起始标签
if (event == XmlResourceParser.START_TAG) {
String tag = parser.getName();
//ROW起始
if (TAG_ROW.equals(tag)) {
inRow = true;
x = 0;
currentRow = createRowFromXml(res, parser);
rows.add(currentRow);
skipRow = currentRow.mode != 0 && currentRow.mode != mKeyboardMode;
if (skipRow) {
skipToEndOfRow(parser);
inRow = false;
}
//Key起始标签
} else if (TAG_KEY.equals(tag)) {
inKey = true;
key = createKeyFromXml(res, currentRow, x, y, parser);
mKeys.add(key);
if (key.codes[0] == KEYCODE_SHIFT) {
// Find available shift key slot and put this shift key in it
for (int i = 0; i < mShiftKeys.length; i++) {
if (mShiftKeys[i] == null) {
mShiftKeys[i] = key;
mShiftKeyIndices[i] = mKeys.size()-1;
break;
}
}
mModifierKeys.add(key);
} else if (key.codes[0] == KEYCODE_ALT) {
mModifierKeys.add(key);
}
currentRow.mKeys.add(key);
} else if (TAG_KEYBOARD.equals(tag)) {
parseKeyboardAttributes(res, parser);
}
} else if (event == XmlResourceParser.END_TAG) {
//key结束标签
if (inKey) {
inKey = false;
//平移一个键位距离
x += key.gap + key.width;
if (x > mTotalWidth) {
mTotalWidth = x;
}
//row结束标签,即一行结束
} else if (inRow) {
//下移一行
inRow = false;
y += currentRow.verticalGap;
y += currentRow.defaultHeight;
row++;
} else {
// TODO: error or extend?
}
}
}
} catch (Exception e) {
Log.e(TAG, "Parse error:" + e);
e.printStackTrace();
}
mTotalHeight = y - mDefaultVerticalGap;
}
通过源码我们可以知道系统是通过读取xml文件中的内容,解析后生成一个Key对象,那么这个key对象中是否有我们想要的内容呢,我们接着往下看。
key对象中包含了两个Drawable,这就是我们想要的键位背景视图了!可以看到还有label,codes,x,y等其它属性,那么我们在xml中设置的属性应该都在其中。
而x与y则是上面计算好的,键位在整个视图坐标轴中的坐标。
那么KeyboardView是怎么将key转换成我们能看见的键位呢。
我们回到KeyboardView,来看看它的onDraw方法都做了什么。
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mDrawPending || mBuffer == null || mKeyboardChanged) {
onBufferDraw();
}
canvas.drawBitmap(mBuffer, 0, 0, null);
}
private void onBufferDraw() {
if (mBuffer == null || mKeyboardChanged) {
if (mBuffer == null || mKeyboardChanged &&
(mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
// Make sure our bitmap is at least 1x1
final int width = Math.max(1, getWidth());
final int height = Math.max(1, getHeight());
mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(mBuffer);
}
invalidateAllKeys();
mKeyboardChanged = false;
}
final Canvas canvas = mCanvas;
canvas.clipRect(mDirtyRect, Op.REPLACE);
if (mKeyboard == null) return;
final Paint paint = mPaint;
final Drawable keyBackground = mKeyBackground;
final Rect clipRegion = mClipRegion;
final Rect padding = mPadding;
final int kbdPaddingLeft = mPaddingLeft;
final int kbdPaddingTop = mPaddingTop;
final Key[] keys = mKeys;
final Key invalidKey = mInvalidatedKey;
paint.setColor(mKeyTextColor);
boolean drawSingleKey = false;
if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
// Is clipRegion completely contained within the invalidated key?
if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
drawSingleKey = true;
}
}
canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
final int keyCount = keys.length;
for (int i = 0; i < keyCount; i++) {
final Key key = keys[i];
if (drawSingleKey && invalidKey != key) {
continue;
}
int[] drawableState = key.getCurrentDrawableState();
keyBackground.setState(drawableState);
// Switch the character to uppercase if shift is pressed
String label = key.label == null? null : adjustCase(key.label).toString();
final Rect bounds = keyBackground.getBounds();
if (key.width != bounds.right ||
key.height != bounds.bottom) {
keyBackground.setBounds(0, 0, key.width, key.height);
}
canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
keyBackground.draw(canvas);
if (label != null) {
// For characters, use large font. For labels like "Done", use small font.
if (label.length() > 1 && key.codes.length < 2) {
paint.setTextSize(mLabelTextSize);
paint.setTypeface(Typeface.DEFAULT_BOLD);
} else {
paint.setTextSize(mKeyTextSize);
paint.setTypeface(Typeface.DEFAULT);
}
// Draw a drop shadow for the text
paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
// Draw the text
canvas.drawText(label,
(key.width - padding.left - padding.right) / 2
+ padding.left,
(key.height - padding.top - padding.bottom) / 2
+ (paint.getTextSize() - paint.descent()) / 2 + padding.top,
paint);
// Turn off drop shadow
paint.setShadowLayer(0, 0, 0, 0);
} else if (key.icon != null) {
final int drawableX = (key.width - padding.left - padding.right
- key.icon.getIntrinsicWidth()) / 2 + padding.left;
final int drawableY = (key.height - padding.top - padding.bottom
- key.icon.getIntrinsicHeight()) / 2 + padding.top;
canvas.translate(drawableX, drawableY);
key.icon.setBounds(0, 0,
key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
key.icon.draw(canvas);
canvas.translate(-drawableX, -drawableY);
}
canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
}
mInvalidatedKey = null;
// Overlay a dark rectangle to dim the keyboard
if (mMiniKeyboardOnScreen) {
paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
}
if (DEBUG && mShowTouchPoints) {
paint.setAlpha(128);
paint.setColor(0xFFFF0000);
canvas.drawCircle(mStartX, mStartY, 3, paint);
canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
paint.setColor(0xFF0000FF);
canvas.drawCircle(mLastX, mLastY, 3, paint);
paint.setColor(0xFF00FF00);
canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
}
mDrawPending = false;
mDirtyRect.setEmpty();
}
阅读源码后我们可以发现,keyboardview所作的事情就是将在Keyboard中计算好的关于键位的坐标xy与width,height等取出来然后一个个绘制在view上,那么思路就很明确了,Keyboard将原本xml中排版好的键位读取出来然后一个个计算出它在坐标系中的位置并生成对应的key对象,之后Keyboard则是在视图绘制的时候将key对象集合取出然后照着Keyboard计算好的数据一个个的将键位绘制到View中。
既然流程我们都知道了,之后我们需要修改键盘的UI来达到自定义的样式,那么则需要继承自KeyboardView以后将其绘制过程注释掉然后加上自己的绘制流程,就可以做到完全的自定义键盘样式了。
详细的代码就不在这里过多赘述了,看完的小伙伴大概已经心里想好该怎么写了。
感谢阅读!