最近在android项目中,遇到需要android车牌键盘的需求(需要支持普通车牌,新能源,警车,军车,领事馆车,教练车以及特种车辆等车牌)
一、示例图
话不多说,分享一下android车牌键盘效果图,以及源码
1、省份选择,也可以更多里选择其他特种车辆例如数字开头的,或者“使”,“民”等
2、号码填写,过滤掉了字母O,I等不存在的号
3、可选择警、学、挂等特殊车辆后缀
二、核心代码
1、键盘控制器
package com.parkingwang.keyboard;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Toast;
import com.parkingwang.keyboard.engine.KeyboardEntry;
import com.parkingwang.keyboard.engine.NumberType;
import com.parkingwang.keyboard.view.InputView;
import com.parkingwang.keyboard.view.KeyboardView;
import com.parkingwang.keyboard.view.OnKeyboardChangedListener;
import com.parkingwang.vehiclekeyboard.R;
import java.util.LinkedHashSet;
import java.util.Set;
/**
*/
public class KeyboardInputController {
private static final String TAG = "KeyboardInputController";
private final KeyboardView mKeyboardView;
private final InputView mInputView;
private final Set<OnInputChangedListener> mOnInputChangedListeners = new LinkedHashSet<>(4);
private boolean mLockedOnNewEnergyType = false;
private boolean mDebugEnabled = true;
private boolean mSwitchVerify = true;
private MessageHandler mMessageHandler;
/**
* 使用键盘View和输入View,创建键盘输入控制器
*
* @param keyboardView 键盘View
* @param inputView 输入框View
*/
public KeyboardInputController(KeyboardView keyboardView, InputView inputView) {
mKeyboardView = keyboardView;
mInputView = inputView;
// 绑定输入框被选中的触发事件:更新键盘
mInputView.addOnFieldViewSelectedListener(new InputView.OnFieldViewSelectedListener() {
@Override
public void onSelectedAt(int index) {
final String number = mInputView.getNumber();
if (mDebugEnabled) {
Log.w(TAG, "点击输入框更新键盘, 号码:" + number + ",序号:" + index);
}
// 除非锁定新能源类型,否则都让引擎自己检测车牌类型
if (mLockedOnNewEnergyType) {
mKeyboardView.update(number, index, false, NumberType.NEW_ENERGY);
} else {
mKeyboardView.update(number, index, false, NumberType.AUTO_DETECT);
}
}
});
// 绑定键盘按键点击事件:更新输入框字符操作,输入框长度变化
mKeyboardView.addKeyboardChangedListener(syncKeyboardInputState());
// 检测键盘更新,尝试自动提交只有一位文本按键的操作
// mKeyboardView.addKeyboardChangedListener(new AutoCommit(mInputView));
// 触发键盘更新回调
mKeyboardView.addKeyboardChangedListener(triggerInputChangedCallback());
}
/**
* 使用键盘View和输入View,创建键盘输入控制器
*
* @param keyboardView 键盘View
* @param inputView 输入框View
* @return KeyboardInputController
*/
public static KeyboardInputController with(KeyboardView keyboardView, InputView inputView) {
return new KeyboardInputController(keyboardView, inputView);
}
/**
* 绑定新能源车牌类型锁定按钮实现接口。
* 当键盘切换新能源车牌时,会调用此接口相关函数来更改锁定按钮状态。
*
* @param proxy 锁定按钮代理实现接口
* @return KeyboardInputController
*/
public KeyboardInputController bindLockTypeProxy(final LockNewEnergyProxy proxy) {
// 点击按钮时,切换新能源车牌绑定状态
proxy.setOnClickListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
tryLockNewEnergyType(!mLockedOnNewEnergyType);
}
});
// proxy.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
// tryLockNewEnergyType(!mLockedOnNewEnergyType);
// }
// });
// 新能源车牌绑定状态,同步键盘更新的新能源类型
mKeyboardView.addKeyboardChangedListener(new OnKeyboardChangedListener.Simple() {
@Override
public void onKeyboardChanged(KeyboardEntry keyboard) {
// 如果键盘更新当前为新能源类型时,强制锁定为新能源类型
if (NumberType.NEW_ENERGY.equals(keyboard.currentNumberType)) {
tryLockNewEnergyType(true);
}
// 同步锁定按钮
proxy.onNumberTypeChanged(NumberType.NEW_ENERGY.equals(keyboard.currentNumberType));
}
});
return this;
}
/**
* 使用默认Toast的消息显示处理接口。
* 默认时,键盘状态切换的提示消息,通过Toast接口来显示。
*
* @return KeyboardBinder
*/
public KeyboardInputController useDefaultMessageHandler() {
return setMessageHandler(new MessageHandler() {
@Override
public void onMessageError(int message) {
Toast.makeText(mKeyboardView.getContext(), message, Toast.LENGTH_SHORT).show();
}
@Override
public void onMessageTip(int message) {
Toast.makeText(mKeyboardView.getContext(), message, Toast.LENGTH_SHORT).show();
}
});
}
/**
* 更新输入组件的车牌号码,并默认选中最后编辑位。
*
* @param number 车牌号码
*/
public void updateNumber(String number) {
updateNumberLockType(number, false);
}
/**
* 更新输入组件的车牌号码,指定是否锁定新能源类型,并默认选中最后编辑位。
*
* @param number 车牌号码
* @param lockedOnNewEnergyType 是否锁定为新能源类型
*/
public void updateNumberLockType(String number, boolean lockedOnNewEnergyType) {
final String newNumber = number == null ? "" : number;
mLockedOnNewEnergyType = lockedOnNewEnergyType;
mInputView.updateNumber(newNumber);
mInputView.performLastPendingFieldView();
}
/**
* 设置键盘提示消息回调接口
*
* @param handler 消息回调接口
* @return KeyboardBinder
*/
public KeyboardInputController setMessageHandler(MessageHandler handler) {
mMessageHandler = Objects.notNull(handler);
return this;
}
/**
* 添加输入变更回调接口
*
* @param listener 回调接口
* @return KeyboardInputController
*/
public KeyboardInputController addOnInputChangedListener(OnInputChangedListener listener) {
mOnInputChangedListeners.add(Objects.notNull(listener));
return this;
}
/**
* 移除输入变更回调接口
*
* @param listener 回调接口
* @return KeyboardInputController
*/
public KeyboardInputController removeOnInputChangedListener(OnInputChangedListener listener) {
mOnInputChangedListeners.remove(Objects.notNull(listener));
return this;
}
/**
* 设置是否在新能源和普通车牌切换的时候校验规则
* @param verify 是否校验
* @return KeyboardInputController
*/
public KeyboardInputController setSwitchVerify(boolean verify){
mSwitchVerify = verify;
return this;
}
/**
* 设置是否启用调试信息
*
* @param enabled 是否启用
* @return KeyboardInputController
*/
public KeyboardInputController setDebugEnabled(boolean enabled) {
mDebugEnabled = enabled;
return this;
}
//
private void updateInputViewItemsByNumberType(NumberType type) {
// 如果检测到的车牌号码为新能源、地方武警,需要显示第8位车牌
final boolean show;
if (NumberType.NEW_ENERGY.equals(type) || NumberType.WJ2012.equals(type) || mLockedOnNewEnergyType) {
show = true;
} else {
show = false;
}
mInputView.set8thVisibility(show);
}
private void tryLockNewEnergyType(boolean toLock) {
// not changed
if (toLock == mLockedOnNewEnergyType) {
return;
}
final boolean completed = mInputView.isCompleted();
if (toLock) {
triggerLockEnergyType(completed);
} else {// unlock
triggerUnlockEnergy(completed);
}
}
// 解锁新能源车牌
private void triggerUnlockEnergy(boolean completed) {
mLockedOnNewEnergyType = false;
// mMessageHandler.onMessageTip(R.string.pwk_now_is_normal);
final boolean lastItemSelected = mInputView.isLastFieldViewSelected();
updateInputViewItemsByNumberType(NumberType.AUTO_DETECT);
if (completed || lastItemSelected) {
mInputView.performLastPendingFieldView();
} else {
mInputView.rePerformCurrentFieldView();
}
}
// 锁定新能源车牌
private void triggerLockEnergyType(boolean completed) {
if (!mSwitchVerify || Texts.isNewEnergyType(mInputView.getNumber())) {
mLockedOnNewEnergyType = true;
// mMessageHandler.onMessageTip(R.string.pwk_now_is_energy);
updateInputViewItemsByNumberType(NumberType.NEW_ENERGY);
if (completed) {
mInputView.performNextFieldView();
} else {
mInputView.rePerformCurrentFieldView();
}
} else {
mMessageHandler.onMessageError(R.string.pwk_change_to_energy_disallow);
}
}
// 输入变更回调
private OnKeyboardChangedListener triggerInputChangedCallback() {
return new OnKeyboardChangedListener.Simple() {
@Override
public void onTextKey(String text) {
notifyChanged();
}
@Override
public void onDeleteKey() {
notifyChanged();
}
@Override
public void onConfirmKey() {
final String number = mInputView.getNumber();
for (OnInputChangedListener listener : mOnInputChangedListeners) {
listener.onCompleted(number, false);
}
}
private void notifyChanged() {
final boolean completed = mInputView.isCompleted();
final String number = mInputView.getNumber();
try {
for (OnInputChangedListener listener : mOnInputChangedListeners) {
listener.onChanged(number, completed);
}
} finally {
if (completed) {
for (OnInputChangedListener listener : mOnInputChangedListeners) {
listener.onCompleted(number, true);
}
}
}
}
};
}
private OnKeyboardChangedListener syncKeyboardInputState() {
return new OnKeyboardChangedListener.Simple() {
@Override
public void onTextKey(String text) {
mInputView.updateSelectedCharAndSelectNext(text);
}
@Override
public void onDeleteKey() {
mInputView.removeLastCharOfNumber();
}
@Override
public void onKeyboardChanged(KeyboardEntry keyboard) {
if (mDebugEnabled) {
Log.w(TAG, "键盘已更新," +
"预设号码号码:" + keyboard.presetNumber +
",最终探测类型:" + keyboard.currentNumberType
);
}
updateInputViewItemsByNumberType(keyboard.currentNumberType);
}
};
}
/**
* 锁定车牌类型代理接口
*/
public interface LockNewEnergyProxy {
/**
* 设置点击切换车牌类型的点击回调接口。通常使用Button来实现。
*
* @param listener 点击回调接口。
*/
void setOnClickListener(RadioGroup.OnCheckedChangeListener listener);
/**
* 当车牌类型发生变化时,此方法被回调。
*
* @param isNewEnergyType 当前是否为新能源类型
*/
void onNumberTypeChanged(boolean isNewEnergyType);
}
@Deprecated
public interface LockTypeProxy extends LockNewEnergyProxy {
}
/**
* 使用Button组件实现的锁定新能源车牌切换逻辑
*/
public static class ButtonProxyImpl implements LockNewEnergyProxy {
private final RadioGroup mButton;
public ButtonProxyImpl(RadioGroup button) {
mButton = button;
}
@Override
public void setOnClickListener(RadioGroup.OnCheckedChangeListener listener) {
mButton.setOnCheckedChangeListener(listener);
}
@Override
public void onNumberTypeChanged(boolean isNewEnergyType) {
if (isNewEnergyType) {
// mButton.setText(R.string.pwk_change_to_normal);
} else {
// mButton.setText(R.string.pwk_change_to_energy);
}
}
}
@Deprecated
public static class ButtonProxy extends ButtonProxyImpl {
public ButtonProxy(RadioGroup button) {
super(button);
}
}
}
2、键盘布局xml文件
<?xml version="1.0" encoding="utf-8"?>
<merge style="@style/PWKInputViewStyle"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:parentTag="com.parkingwang.keyboard.view.InputView">
<Button
android:id="@+id/number_0"
style="@style/PWKInputItemStyleKey"/>
<Button
android:id="@+id/number_1"
style="@style/PWKInputItemStyleKey"/>
<Button
android:id="@+id/number_2"
style="@style/PWKInputItemStyleKey"/>
<Button
android:id="@+id/number_3"
style="@style/PWKInputItemStyleKey"/>
<Button
android:id="@+id/number_4"
style="@style/PWKInputItemStyleKey"/>
<Button
android:id="@+id/number_5"
style="@style/PWKInputItemStyleKey"/>
<Button
android:id="@+id/number_6"
style="@style/PWKInputItemStyleKey"/>
<Button
android:id="@+id/number_7"
style="@style/PWKInputItemStyleKey"
android:visibility="gone"/>
</merge>
3、键盘布局管理器
/**
* 车牌号码类型对应的布局管理
*
* @author fdw fdw628@wkhere.com
*/
class LayoutManager {
interface LayoutProvider {
LayoutEntry get(Context ctx);
}
private final static String NAME_PROVINCE = "layout.province";
private final static String NAME_FIRST = "layout.first.spec";
private final static String NAME_LAST = "layout.last.spec";
private final static String NAME_WITH_IO = "layout.with.io";
private final static String NAME_WITHOUT_IO = "layout.without.io";
private final static String NAME_WITHOUT_IO_BACK = "layout.without.io.back";
private final Map<String, LayoutEntry> mNamedLayouts = new HashMap<>();
private final List<LayoutProvider> mProviders = new ArrayList<>(5);
LayoutManager() {
// 省份简称布局
mNamedLayouts.put(NAME_PROVINCE, createRows(
"京津晋冀蒙辽吉黑沪苏",
"浙皖闽赣鲁豫鄂湘粤桂",
"琼渝川贵云藏陕甘",
"青宁新台" + VNumberChars.MORE + "-+"
));
// 首位特殊字符布局
mNamedLayouts.put(NAME_FIRST, createRows(
"1234567890",
"QWERTYCVBN",
"ASDFGHJKL",
"ZX民使" + VNumberChars.BACK + "-+"
));
// 带IO字母+数字
mNamedLayouts.put(NAME_WITH_IO, createRows(
"1234567890",
"QWERTYUIOP",
"ASDFGHJKLM",
"ZXCVBN-+"
));
// 不带IO字母+数字 带返回按钮(军警车第三位使用)
mNamedLayouts.put(NAME_WITHOUT_IO_BACK, createRows(
"1234567890",
"QWERTYUBNP",
"ASDFGHJKLM",
"ZXCV" + VNumberChars.BACK + "-+"
));
// 末位特殊字符
mNamedLayouts.put(NAME_LAST, createRows(
"学警港澳航挂试超使领",
"1234567890",
"ABCDEFGHJK",
"WXYZ" + VNumberChars.BACK + "-+"
));
// 无IO字母+数字
mNamedLayouts.put(NAME_WITHOUT_IO, createRows(
"1234567890",
"QWERTYUPMN",
"ASDFGHJKLB",
"ZXCV" + VNumberChars.MORE + "-+"
));
mProviders.add(new ProvinceLayoutProvider());
mProviders.add(new FirstSpecLayoutProvider());
mProviders.add(new WithIOLayoutProvider());
mProviders.add(new LastSpecLayoutProvider());
mProviders.add(new WithoutIOLayoutProvider());
}
private static LayoutEntry createRows(String... rows) {
final LayoutEntry layout = new LayoutEntry(rows.length);
for (String keys : rows) {
layout.add(mkEntitiesOf(keys));
}
return layout;
}
/**
* 返回布局对象
*
* @param ctx Context
* @return 缓存布局对象的副本
*/
@NonNull
public LayoutEntry getLayout(@NonNull Context ctx) {
LayoutEntry layout = new LayoutEntry();
for (LayoutProvider provider : mProviders) {
final LayoutEntry ret = provider.get(ctx);
if (null != ret) {
layout = ret;
break;
}
}
return layout.newCopy();
}
/**
* 省份简称布局提供器。
* 1. 第1位,未知类型,非特殊状态;
* 2. 第1位,民用、新能源、新旧领事馆类型;
* 3. 第3位,武警类型;
*/
final class ProvinceLayoutProvider implements LayoutProvider {
@Override
public LayoutEntry get(Context ctx) {
if (0 == ctx.selectIndex || 2 == ctx.selectIndex) {
if (0 == ctx.selectIndex && NumberType.AUTO_DETECT.equals(ctx.numberType) && !ctx.reqSpecLayout) {
return mNamedLayouts.get(NAME_PROVINCE);
} else if (0 == ctx.selectIndex && ctx.numberType.isAnyOf(CIVIL, NEW_ENERGY, LING2012, LING2018)) {
return mNamedLayouts.get(NAME_PROVINCE);
} else if (2 == ctx.selectIndex && NumberType.WJ2012.equals(ctx.numberType)) {
if (ctx.reqSpecLayout) {
return mNamedLayouts.get(NAME_WITHOUT_IO_BACK);
} else {
return mNamedLayouts.get(NAME_PROVINCE);
}
} else {
return null;
}
} else {
return null;
}
}
}
/**
* 首位特殊字符布局提供器。
* 1. 第1位,未知类型,且进入特殊布局状态;
* 1. 第1位,武警、军队、新旧使馆类型、民航类型;
*/
final class FirstSpecLayoutProvider implements LayoutProvider {
@Override
public LayoutEntry get(Context ctx) {
if (0 == ctx.selectIndex) {
if (ctx.numberType.isAnyOf(WJ2012, PLA2012, SHI2012, SHI2017, AVIATION)) {
return mNamedLayouts.get(NAME_FIRST);
} else if (ctx.reqSpecLayout) {
return mNamedLayouts.get(NAME_FIRST);
} else {
return null;
}
} else {
return null;
}
}
}
/**
* 带IO字母+数字布局提供器。
* 1. 第4-6位;
* 2. 第2位,非民航类型;
* 3. 第3位,非武警类型;
*/
final class WithIOLayoutProvider implements LayoutProvider {
@Override
public LayoutEntry get(Context ctx) {
if (3 == ctx.selectIndex || 4 == ctx.selectIndex || 5 == ctx.selectIndex) {
return mNamedLayouts.get(NAME_WITH_IO);
} else if (1 == ctx.selectIndex && !AVIATION.equals(ctx.numberType)) {
return mNamedLayouts.get(NAME_WITH_IO);
} else if (2 == ctx.selectIndex && !WJ2012.equals(ctx.numberType)) {
return mNamedLayouts.get(NAME_WITH_IO);
} else {
return null;
}
}
}
/**
* 末位特殊字符布局提供器。
* 1. 第2位,民航车牌类型;
* 2. 第7位,进入特殊布局状态;
* 3. 第7位,新2017式大使馆、新旧领事馆类型;
*/
final class LastSpecLayoutProvider implements LayoutProvider {
@Override
public LayoutEntry get(Context ctx) {
if (1 == ctx.selectIndex) {
return mNamedLayouts.get(NAME_LAST);
} else if (6 == ctx.selectIndex) {
if (ctx.numberType.isAnyOf(SHI2017, LING2012, LING2018)) {
return mNamedLayouts.get(NAME_LAST);
} else if (ctx.reqSpecLayout) {
return mNamedLayouts.get(NAME_LAST);
} else {
return null;
}
} else {
return null;
}
}
}
/**
* 无IO字符+数字布局提供器。
* 1. 第7位,民用类型,非特殊布局状态;
* 2. 第7位,新能源、武警、军队、旧2012式大使馆、民航;
* 3. 第8位;
*/
final class WithoutIOLayoutProvider implements LayoutProvider {
@Override
public LayoutEntry get(Context ctx) {
if (6 == ctx.selectIndex) {
if (NumberType.CIVIL.equals(ctx.numberType) && !ctx.reqSpecLayout) {
return mNamedLayouts.get(NAME_WITHOUT_IO);
} else if (ctx.numberType.isAnyOf(NEW_ENERGY, WJ2012, PLA2012, SHI2012, AVIATION)) {
return mNamedLayouts.get(NAME_WITHOUT_IO);
} else {
return null;
}
} else if (7 == ctx.selectIndex) {
return mNamedLayouts.get(NAME_WITHOUT_IO);
} else {
return null;
}
}
}
}
三、github开源地址
https://github.com/Frank628/android_carnum_keybord