最近在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

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐