Android自定义安全软键盘开发详细步骤

源码地址:GitHub:  https://github.com/SValence/SafeKeyboard

       

 在一些比较敏感的输入场合,我们不能调用系统的软键盘进行输入信息,这时候就需要自定义一个软键盘来完成输入工作,由此就需要下面 Android 安全软键盘开发 的工作,本文主要从项目需求出发,梳理开发流程,并给出一个实际开发的例子作为参考。

一、Android 安全软键盘开发流程
1.建立软键盘样式

       即在项目res文件夹中创建所需要的各种软键盘的样式(比如说:字母、数字、符号 等)。

2.创建layout布局文件

       在布局文件中给软键盘创建container,以便显示软键盘

 

3.自定义KeyboardView

       自定义一个KeyboardView 并继承自KeyboardView,在自定义的KeyboardView中绘制特殊按键,包括按键的点击背景,图片,文字 等。

4.自定义一个普通java类,一般取名为 **Keyboard.java

       把软键盘加载到container中,即在布局文件里预留的存放软键盘 的container。

       在类的内部实现软键盘的输入控制,键盘转换控制,软键盘的显示与隐藏控制 等。

       在需要用到软键盘的Activity中实例化该Keyboard 类,并传入必要的数据和信息。

先上一个效果图:

二、Android 安全软键盘开发实例(仅作参考)
1.创建项目后, 在layout布局文件中设计基本布局,参考如下

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <EditText
            android:id="@+id/normalEditText"
            android:layout_width="match_parent"
            android:layout_height="50sp"
            android:layout_marginEnd="10sp"
            android:layout_marginStart="10sp"
            android:hint="@string/keyboard_system"
            android:inputType="text" />
        <EditText
            android:id="@+id/safeEditText"
            android:layout_width="match_parent"
            android:layout_height="50sp"
            android:layout_marginEnd="10sp"
            android:layout_marginStart="10sp"
            android:hint="@string/keyboard_demo_new"
            android:inputType="textVisiblePassword" />
    </LinearLayout>
    <LinearLayout
        android:id="@+id/keyboardViewPlace"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:orientation="vertical" />
</RelativeLayout>
        这里使用一个线性布局作为存放自定义软键盘的容器,说明一下,一般使用到自定义软键盘的页面的布局不会很复杂,无非就是想要在输入关键数据比如密码时才使用到软键盘,而软键盘一般都是在手机等移动设备屏幕的最下方显示,以方便输入,所以这里使用了RelativeLayout方便显示,当然也可以使用现在google推行的ConstraintLayout布局来做,这个根据个人喜好。所以在原有的项目里加入一个存放软键盘的容器,也不会给原有的项目带来很大的麻烦或者影响,所以这种做法,我个人认为还是可取的.

3. 在项目res文件夹下创建xml文件夹,并创建出自己需要的软键盘布局,比如说:数字,字母,符号等,一般来说这三个已经足够用了下面是三个例子

3.1 字母布局文件 keyboard_letter.xml

<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:horizontalGap="1%p"
    android:keyHeight="@dimen/key_height"
    android:keyWidth="10%p"
    android:verticalGap="@dimen/key_vertical_gap">
    <Row>
        <Key android:codes="113" android:keyEdgeFlags="left" android:keyLabel="q" android:keyWidth="8.9%p" />
        <Key android:codes="119" android:keyLabel="w" android:keyWidth="8.9%p" />
        <Key android:codes="101" android:keyLabel="e" android:keyWidth="8.9%p" />
        <Key android:codes="114" android:keyLabel="r" android:keyWidth="8.9%p" />
        <Key android:codes="116" android:keyLabel="t" android:keyWidth="8.9%p" />
        <Key android:codes="121" android:keyLabel="y" android:keyWidth="8.9%p" />
        <Key android:codes="117" android:keyLabel="u" android:keyWidth="8.9%p" />
        <Key android:codes="105" android:keyLabel="i" android:keyWidth="8.9%p" />
        <Key android:codes="111" android:keyLabel="o" android:keyWidth="8.9%p" />
        <Key android:codes="112" android:keyEdgeFlags="right" android:keyLabel="p" android:keyWidth="8.9%p" />
    </Row>
    <Row>
        <Key android:codes="97" android:horizontalGap="5.5%p" android:keyEdgeFlags="left" android:keyLabel="a" android:keyWidth="9%p" />
        <Key android:codes="115" android:keyLabel="s" android:keyWidth="9%p" />
        <Key android:codes="100" android:keyLabel="d" android:keyWidth="9%p" />
        <Key android:codes="102" android:keyLabel="f" android:keyWidth="9%p" />
        <Key android:codes="103" android:keyLabel="g" android:keyWidth="9%p" />
        <Key android:codes="104" android:keyLabel="h" android:keyWidth="9%p" />
        <Key android:codes="106" android:keyLabel="j" android:keyWidth="9%p" />
        <Key android:codes="107" android:keyLabel="k" android:keyWidth="9%p" />
        <Key android:codes="108" android:keyEdgeFlags="right" android:keyLabel="l" android:keyWidth="9%p" />
    </Row>
    <Row>
        <Key android:codes="-1" android:isModifier="true" android:isSticky="true" android:keyEdgeFlags="left" android:keyWidth="13%p" />
        <Key android:codes="122" android:horizontalGap="1.5%p" android:keyLabel="z" android:keyWidth="9%p" />
        <Key android:codes="120" android:keyLabel="x" android:keyWidth="9%p" />
        <Key android:codes="99" android:keyLabel="c" android:keyWidth="9%p" />
        <Key android:codes="118" android:keyLabel="v" android:keyWidth="9%p" />
        <Key android:codes="98" android:keyLabel="b" android:keyWidth="9%p" />
        <Key android:codes="110" android:keyLabel="n" android:keyWidth="9%p" />
        <Key android:codes="109" android:keyLabel="m" android:keyWidth="9%p" />
        <Key android:codes="-5" android:horizontalGap="1.5%p" android:isRepeatable="true" android:keyWidth="13%p" />
    </Row>
    <Row android:rowEdgeFlags="bottom">
        <Key android:codes="-2" android:keyLabel="123" android:keyWidth="19%p" />
        <Key android:codes="32" android:isRepeatable="false" android:keyLabel="space" android:keyWidth="58%p" />
        <Key android:codes="100860" android:keyEdgeFlags="right" android:keyLabel="#+=" android:keyWidth="19%p" />
    </Row>
</Keyboard>
 

3.2 字符键盘布局文件 keyboard_symbol.xml

 

<?xml version="1.0" encoding="utf-8"?>
<Keyboard
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:keyHeight="@dimen/key_num_height"
    android:keyWidth="10%p"
    android:horizontalGap="1%p"
    android:verticalGap="@dimen/key_vertical_gap">
    <Row>
        <Key android:keyWidth="8.9%p" android:codes="33" android:keyLabel="!" android:keyEdgeFlags="left" />
        <Key android:keyWidth="8.9%p" android:codes="64" android:keyLabel="@string/keyboard_key1" /><!--@-->
        <Key android:keyWidth="8.9%p" android:codes="35" android:keyLabel="#" />
        <Key android:keyWidth="8.9%p" android:codes="36" android:keyLabel="$" />
        <Key android:keyWidth="8.9%p" android:codes="37" android:keyLabel="%" />
        <Key android:keyWidth="8.9%p" android:codes="94" android:keyLabel="^" />
        <Key android:keyWidth="8.9%p" android:codes="38" android:keyLabel="@string/keyboard_key2" /><!--&-->
        <Key android:keyWidth="8.9%p" android:codes="42" android:keyLabel="*" />
        <Key android:keyWidth="8.9%p" android:codes="40" android:keyLabel="(" />
        <Key android:keyWidth="8.9%p" android:codes="41" android:keyLabel=")" android:keyEdgeFlags="right" />
    </Row>
    <Row>
        <Key android:keyWidth="8.9%p" android:codes="39"
             android:keyEdgeFlags="left" android:keyLabel="'" />
        <Key android:keyWidth="8.9%p" android:codes="34" android:keyLabel="@string/keyboard_key3" /> <!--"-->
        <Key android:keyWidth="8.9%p" android:codes="61" android:keyLabel="=" />
        <Key android:keyWidth="8.9%p" android:codes="95" android:keyLabel="_" />
        <Key android:keyWidth="8.9%p" android:codes="58" android:keyLabel=":" />
        <Key android:keyWidth="8.9%p" android:codes="59" android:keyLabel=";" />
        <Key android:keyWidth="8.9%p" android:codes="63" android:keyLabel="\?" />
        <Key android:keyWidth="8.9%p" android:codes="126" android:keyLabel="~" />
        <Key android:keyWidth="8.9%p" android:codes="124" android:keyLabel="|" />
        <Key android:keyWidth="8.9%p" android:codes="183" android:keyEdgeFlags="right"
             android:keyLabel="·" />
    </Row>
    <Row>
        <Key android:horizontalGap="3.5%p" android:keyWidth="9%p" android:codes="43" android:keyLabel="+" />
        <Key android:keyWidth="9%p" android:codes="45" android:keyLabel="-" />
        <Key android:keyWidth="9%p" android:codes="92" android:keyLabel="\\" />
        <Key android:keyWidth="9%p" android:codes="47" android:keyLabel="/" />
        <Key android:keyWidth="9%p" android:codes="91" android:keyLabel="[" />
        <Key android:keyWidth="9%p" android:codes="93" android:keyLabel="]" />
        <Key android:keyWidth="9%p" android:codes="123" android:keyLabel="{" />
        <Key android:keyWidth="9%p" android:codes="125" android:keyLabel="}" />
        <Key android:horizontalGap="3.5%p" android:keyWidth="13%p" android:codes="-5"
             android:isRepeatable="true" />
    </Row>
    <Row android:rowEdgeFlags="bottom">
        <Key android:keyWidth="13%p" android:codes="-2" android:keyLabel="123" />
        <Key android:keyWidth="9%p" android:horizontalGap="1.5%p" android:codes="44" android:keyLabel="," />
        <Key android:keyWidth="9%p" android:codes="46" android:keyLabel="." />
        <Key android:keyWidth="9%p" android:codes="60" android:keyLabel="@string/keyboard_key4" />
        <Key android:keyWidth="9%p" android:codes="62" android:keyLabel="@string/keyboard_key5" />
        <Key android:keyWidth="9%p" android:codes="8364" android:keyLabel="€" />
        <Key android:keyWidth="9%p" android:codes="163" android:keyLabel="£" />
        <Key android:keyWidth="9%p" android:codes="165" android:keyLabel="¥" />
        <Key android:keyWidth="13%p" android:codes="100860" android:horizontalGap="1.5%p" android:keyEdgeFlags="right" android:keyLabel="ABC" />
    </Row>
</Keyboard>
3.3 数字键盘布局文件 keyboard_num.xml

<?xml version="1.0" encoding="utf-8"?>
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
    android:horizontalGap="@dimen/key_horizontal_gap"
    android:keyHeight="@dimen/key_num_height"
    android:keyWidth="30%p"
    android:verticalGap="@dimen/key_vertical_gap">
    <Row>
        <Key android:codes="49" android:keyLabel="1" />
        <Key android:codes="50" android:keyLabel="2" />
        <Key android:codes="51" android:keyLabel="3" />
    </Row>
    <Row>
        <Key android:codes="52" android:keyLabel="4" />
        <Key android:codes="53" android:keyLabel="5" />
        <Key android:codes="54" android:keyLabel="6" />
    </Row>
    <Row>
        <Key android:codes="55" android:keyLabel="7" />
        <Key android:codes="56" android:keyLabel="8" />
        <Key android:codes="57" android:keyLabel="9" />
    </Row>
    <Row>
        <Key android:codes="-2" android:keyLabel="ABC" />
        <Key android:codes="48" android:keyLabel="0" />
        <Key android:codes="-35" android:isRepeatable="true" />
    </Row>
</Keyboard>
        简单说明一下, 每个按键代表的字符的keycode一般来说都是固定的,android系统会预先存储一些keycode并占有这些数据,我们在使用的时候直接拿来使用就可以,如果想自己来控制的话,就需要避过这些已经被系统占用的code,比如说在符号键盘的最后一个code为100860的按键,我在测试的时候曾经用过10086作为code,但是发现它的作用是输出一个图形字符,而不是切换到字母键盘的作用,说明10086这个code已经被占用,那code 10086就不能继续使用。这是开发者在使用的时候要注意的一个问题

注意事项:android:keyLabel 里的字段是正常按下一个按键输出的字符,千万注意的是不要使用 android:keyOutputText 来代替,因为虽然同样是输出的字符,但是 android:keyOutputText 输出的字符并不会触发 onKey() 这个方法,而且会造成在 EditText.addTextChangedListener 事件中回调方法中 start 参数一直为 0,即EditText开始位置,这种情况会出现第一次输入正常,继续输入程序异常崩溃的现象。使用的时候要特别注意。

下面是三个键盘成型之后的预览

 

4. 整个自定义软键盘布局文件(包括按键布局和点击输入完成隐藏输入法的布局) layout_keyboard_containor.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/keyboardBackColor"
    android:orientation="vertical">
    <RelativeLayout
        android:id="@+id/keyboardHeader"
        android:layout_width="match_parent"
        android:layout_height="@dimen/keyboard_tip_height"
        android:background="@color/keyboardBackColor">
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:orientation="horizontal"
            tools:ignore="RelativeOverlap,UseCompoundDrawables">
            <ImageView
                android:layout_width="25sp"
                android:layout_height="25sp"
                android:layout_gravity="center_vertical"
                android:contentDescription="@string/description"
                android:src="@drawable/shield" />
            <TextView
                android:id="@+id/keyboardTip"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:text="@string/safe_keyboard"
                android:textColor="@color/lightGray"
                android:textSize="16sp" />
        </LinearLayout>
        <FrameLayout
            android:id="@+id/keyboardDone"
            android:layout_width="60sp"
            android:layout_height="match_parent"
            android:layout_alignParentEnd="true"
            android:layout_centerVertical="true"
            android:background="@drawable/bg_keyboard_done">
            <ImageView
                android:layout_width="30sp"
                android:layout_height="30sp"
                android:layout_gravity="center"
                android:contentDescription="@null"
                android:scaleType="centerInside"
                android:src="@drawable/keyboard_done_"
                android:textColor="@color/white"
                android:textSize="16sp" />
        </FrameLayout>
    </RelativeLayout>
    <FrameLayout
        android:id="@+id/keyboardLayer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10sp"
        android:layout_marginTop="2sp">
        <com.safe.keyboard.SafeKeyboardView
            android:id="@+id/safeKeyboardLetter"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:animateLayoutChanges="true"
            android:background="@color/keyboardBackColor"
            android:focusable="true"
            android:focusableInTouchMode="true"
            android:keyBackground="@drawable/keyboard_press_bg"
            android:keyPreviewHeight="60dp"
            android:keyPreviewLayout="@layout/keyboard_preview_layout"
            android:keyPreviewOffset="0dp"
            android:keyTextColor="@color/white" />
    </FrameLayout>
</LinearLayout>
com.safe.keyboard.SafeKeyboardView中的属性值得关注的就是 background="@color/keyboardBackColor" 和 keyPreviewLayout="@layout/keyboard_preview_layout" 以及和 keyPreview 相关的几个属性

background设置了每个按键的点击背景变化,keyPreviewLayout属性设置了当你点击一个按键时会显示出来当前点击的是哪个按键,以便提示用户是否点错,其内容也是一个布局文件,但是比较简单,一般来说就是一个设置了背景,字体大小和颜色的 TextView (下面会贴出样例的布局文件代码)

需要注意的是 keyPreviewOffset  这个属性,它决定了这个提示布局的显示位置,值越大显示位置越靠下,有兴趣的可以试一试这个属性,下面是 keyboard_preview_layout.xml 字符提示布局文件

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:background="@drawable/keyboard_preview_bg"
    android:gravity="center_horizontal"
    android:textColor="@color/keyboardNormal"
    android:textSize="35sp"
    android:textStyle="bold" />
 

5. SafeKeyboardView.java文件,这个就是自定义View

package com.safe.keyboard;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.util.AttributeSet;
import java.lang.reflect.Field;
import java.util.List;
/**
 * Created by Administrator on 2018/3/7 0007.
 */
public class SafeKeyboardView extends KeyboardView {
    private Context mContext;
    public SafeKeyboardView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.mContext = context;
    }
    public SafeKeyboardView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
    }
    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        try {
            List<Keyboard.Key> keys = getKeyboard().getKeys();
            for (Keyboard.Key key : keys) {
                if (key.codes[0] == -5 || key.codes[0] == -35 || key.codes[0] == -2
                        || key.codes[0] == 100860 || key.codes[0] == -1) {
                    drawSpecialKey(canvas, key);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void drawSpecialKey(Canvas canvas, Keyboard.Key key) {
        if (key.codes[0] == -5) {
            drawKeyBackground(R.drawable.keyboard_del_letter, canvas, key);
            drawText(canvas, key);
        } else if (key.codes[0] == -35) {
            drawKeyBackground(R.drawable.keyboard_del_num, canvas, key);
            drawText(canvas, key);
        } else if (key.codes[0] == -2 || key.codes[0] == 100860) {
            drawKeyBackground(R.drawable.keyboard_change, canvas, key);
            drawText(canvas, key);
        } else if (key.codes[0] == -1) {
            if (SafeKeyboard.isCapes()) {
                drawKeyBackground(R.drawable.keyboard_shift_up, canvas, key);
                drawText(canvas, key);
            } else {
                drawKeyBackground(R.drawable.keyboard_shift_low, canvas, key);
                drawText(canvas, key);
            }
        }
    }
    private void drawKeyBackground(int id, Canvas canvas, Keyboard.Key key) {
        Drawable drawable = mContext.getResources().getDrawable(id);
        int[] state = key.getCurrentDrawableState();
        if (key.codes[0] != 0) {
            drawable.setState(state);
        }
        drawable.setBounds(key.x, key.y, key.x + key.width, key.y + key.height);
        drawable.draw(canvas);
    }
    private void drawText(Canvas canvas, Keyboard.Key key) {
        try {
            Rect bounds = new Rect();
            Paint paint = new Paint();
            paint.setTextAlign(Paint.Align.CENTER);
            paint.setAntiAlias(true);
            paint.setColor(Color.WHITE);
            if (key.label != null) {
                String label = key.label.toString();
                Field field;
                if (label.length() > 1 && key.codes.length < 2) {
                    int labelTextSize = 0;
                    try {
                        field = KeyboardView.class.getDeclaredField(getContext().getString(R.string.mLabelTextSize));
                        field.setAccessible(true);
                        labelTextSize = (int) field.get(this);
                    } catch (NoSuchFieldException | IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    paint.setTextSize(labelTextSize);
                    paint.setTypeface(Typeface.DEFAULT_BOLD);
                } else {
                    int keyTextSize = 0;
                    try {
                        field = KeyboardView.class.getDeclaredField(getContext().getString(R.string.mLabelTextSize));
                        field.setAccessible(true);
                        keyTextSize = (int) field.get(this);
                    } catch (NoSuchFieldException | IllegalAccessException e) {
                        e.printStackTrace();
                    }
                    paint.setTextSize(keyTextSize);
                    paint.setTypeface(Typeface.DEFAULT);
                }
                paint.getTextBounds(key.label.toString(), 0, key.label.toString().length(), bounds);
                canvas.drawText(key.label.toString(), key.x + (key.width / 2), (key.y + key.height / 2) + bounds.height() / 2, paint);
            } else if (key.icon != null) {
                key.icon.setBounds(key.x + (key.width - key.icon.getIntrinsicWidth()) / 2,
                        key.y + (key.height - key.icon.getIntrinsicHeight()) / 2,
                        key.x + (key.width - key.icon.getIntrinsicWidth()) / 2 + key.icon.getIntrinsicWidth(),
                        key.y + (key.height - key.icon.getIntrinsicHeight()) / 2 + key.icon.getIntrinsicHeight());
                key.icon.draw(canvas);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
继承于 KeyboardView ,在这个自定义View中绘制一些特殊的按键,比如说 删除、大小写转换、切换键盘类型等等的按键,从而实现这些特殊按键的点击效果的转变。

6. 创建SafeKeyboard.java文件

package com.safe.keyboard;
import android.annotation.SuppressLint;
import android.content.Context;
import android.inputmethodservice.Keyboard;
import android.inputmethodservice.KeyboardView;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.Editable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
/**
 * Created by Administrator on 2018/3/7 0007.
 */
public class SafeKeyboard {
    private static final String TAG = "SafeKeyboard";
    private Context mContext;               //上下文
    private LinearLayout layout;
    private View keyContainer;              //自定义键盘的容器View
    private SafeKeyboardView keyboardView;  //键盘的View
    private Keyboard keyboardNumber;        //数字键盘
    private Keyboard keyboardLetter;        //字母键盘
    private Keyboard keyboardSymbol;        //符号键盘
    private static boolean isCapes = false;
    private boolean isShowStart = false;
    private boolean isHideStart = false;
    private int keyboardType = 1;
    private static final long HIDE_TIME = 300;
    private static final long SHOW_DELAY = 200;
    private static final long SHOW_TIME = 300;
    private static final long DELAY_TIME = 100;
    private Handler showHandler = new Handler(Looper.getMainLooper());
    private Handler hEndHandler = new Handler(Looper.getMainLooper());
    private Handler sEndHandler = new Handler(Looper.getMainLooper());
    private TranslateAnimation showAnimation;
    private TranslateAnimation hideAnimation;
    private long lastTouchTime;
    private EditText mEditText;
    SafeKeyboard(Context mContext, LinearLayout layout, EditText mEditText) {
        this.mContext = mContext;
        this.layout = layout;
        this.mEditText = mEditText;
        initKeyboard();
        initAnimation();
        addListeners();
    }
    public static boolean isCapes() {
        return isCapes;
    }
    private void initAnimation() {
        showAnimation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f,
                Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF
                , 1.0f, Animation.RELATIVE_TO_SELF, 0.0f);
        hideAnimation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f,
                Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF
                , 0.0f, Animation.RELATIVE_TO_SELF, 1.0f);
        showAnimation.setDuration(SHOW_TIME);
        hideAnimation.setDuration(HIDE_TIME);
        showAnimation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                isShowStart = true;
                // 在这里设置可见, 会出现第一次显示键盘时直接闪现出来, 没有动画效果, 后面正常
                // keyContainer.setVisibility(View.VISIBLE);
                // 动画持续时间 SHOW_TIME 结束后, 不管什么操作, 都需要执行, 把 isShowStart 值设为 false; 否则
                // 如果 onAnimationEnd 因为某些原因没有执行, 会影响下一次使用
                sEndHandler.removeCallbacks(showEnd);
                sEndHandler.postDelayed(showEnd, SHOW_TIME);
            }
            @Override
            public void onAnimationEnd(Animation animation) {
                isShowStart = false;
                keyContainer.clearAnimation();
            }
            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
        hideAnimation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                isHideStart = true;
                // 动画持续时间 HIDE_TIME 结束后, 不管什么操作, 都需要执行, 把 isHideStart 值设为 false; 否则
                // 如果 onAnimationEnd 因为某些原因没有执行, 会影响下一次使用
                hEndHandler.removeCallbacks(hideEnd);
                hEndHandler.postDelayed(hideEnd, HIDE_TIME);
            }
            @Override
            public void onAnimationEnd(Animation animation) {
                isHideStart = false;
                if (keyContainer.getVisibility() != View.GONE) {
                    keyContainer.setVisibility(View.GONE);
                }
                keyContainer.clearAnimation();
            }
            @Override
            public void onAnimationRepeat(Animation animation) {
            }
        });
    }
    @SuppressLint("ClickableViewAccessibility")
    private void initKeyboard() {
        keyContainer = LayoutInflater.from(mContext).inflate(R.layout.layout_keyboard_containor, layout, true);
        keyContainer.setVisibility(View.GONE);
        keyboardNumber = new Keyboard(mContext, R.xml.keyboard_num);            //实例化数字键盘
        keyboardLetter = new Keyboard(mContext, R.xml.keyboard_letter);         //实例化字母键盘
        keyboardSymbol = new Keyboard(mContext, R.xml.keyboard_symbol);         //实例化符号键盘
        // 由于符号键盘与字母键盘共用一个KeyBoardView, 所以不需要再为符号键盘单独实例化一个KeyBoardView
        keyboardView = keyContainer.findViewById(R.id.safeKeyboardLetter);
        keyboardView.setKeyboard(keyboardLetter);                         //给键盘View设置键盘
        keyboardView.setEnabled(true);
        keyboardView.setPreviewEnabled(false);
        keyboardView.setOnKeyboardActionListener(listener);
        FrameLayout done = keyContainer.findViewById(R.id.keyboardDone);
        done.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (isKeyboardShown()) {
                    hideKeyboard();
                }
            }
        });
        keyboardView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return event.getAction() == MotionEvent.ACTION_MOVE;
            }
        });
    }
    // 设置键盘点击监听
    private KeyboardView.OnKeyboardActionListener listener = new KeyboardView.OnKeyboardActionListener() {
        @Override
        public void onPress(int primaryCode) {
            if (keyboardType == 3) {
                keyboardView.setPreviewEnabled(false);
            } else {
                keyboardView.setPreviewEnabled(true);
                if (primaryCode == -1) {
                    keyboardView.setPreviewEnabled(false);
                } else if (primaryCode == -5) {
                    keyboardView.setPreviewEnabled(false);
                } else if (primaryCode == 32) {
                    keyboardView.setPreviewEnabled(false);
                } else if (primaryCode == -2) {
                    keyboardView.setPreviewEnabled(false);
                } else if (primaryCode == 100860) {
                    keyboardView.setPreviewEnabled(false);
                } else {
                    keyboardView.setPreviewEnabled(true);
                }
            }
        }
        @Override
        public void onRelease(int primaryCode) {
        }
        @Override
        public void onKey(int primaryCode, int[] keyCodes) {
            try {
                Editable editable = mEditText.getText();
                int start = mEditText.getSelectionStart();
                int end = mEditText.getSelectionEnd();
                if (primaryCode == Keyboard.KEYCODE_CANCEL) {
                    // 隐藏键盘
                    hideKeyboard();
                } else if (primaryCode == Keyboard.KEYCODE_DELETE || primaryCode == -35) {
                    // 回退键,删除字符
                    if (editable != null && editable.length() > 0) {
                        if (start == end) { //光标开始和结束位置相同, 即没有选中内容
                            editable.delete(start - 1, start);
                        } else { //光标开始和结束位置不同, 即选中EditText中的内容
                            editable.delete(start, end);
                        }
                    }
                } else if (primaryCode == Keyboard.KEYCODE_SHIFT) {
                    // 大小写切换
                    changeKeyboardLetterCase();
                    // 重新setKeyboard, 进而系统重新加载, 键盘内容才会变化(切换大小写)
                    keyboardType = 1;
                    switchKeyboard();
                } else if (primaryCode == Keyboard.KEYCODE_MODE_CHANGE) {
                    // 数字与字母键盘互换
                    if (keyboardType == 3) { //当前为数字键盘
                        keyboardType = 1;
                    } else {        //当前不是数字键盘
                        keyboardType = 3;
                    }
                    switchKeyboard();
                } else if (primaryCode == 100860) {
                    // 字母与符号切换
                    if (keyboardType == 2) { //当前是符号键盘
                        keyboardType = 1;
                    } else {        //当前不是符号键盘, 那么切换到符号键盘
                        keyboardType = 2;
                    }
                    switchKeyboard();
                } else {
                    // 输入键盘值
                    // editable.insert(start, Character.toString((char) primaryCode));
                    editable.replace(start, end, Character.toString((char) primaryCode));
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onText(CharSequence text) {}
        @Override
        public void swipeLeft() {}
        @Override
        public void swipeRight() {}
        @Override
        public void swipeDown() {}
        @Override
        public void swipeUp() {}
    };
    private void switchKeyboard() {
        switch (keyboardType) {
            case 1:
                keyboardView.setKeyboard(keyboardLetter);
                break;
            case 2:
                keyboardView.setKeyboard(keyboardSymbol);
                break;
            case 3:
                keyboardView.setKeyboard(keyboardNumber);
                break;
            default:
                Log.e(TAG, "ERROR keyboard type");
                break;
        }
    }
    private void changeKeyboardLetterCase() {
        List<Keyboard.Key> keyList = keyboardLetter.getKeys();
        if (isCapes) {
            for (Keyboard.Key key : keyList) {
                if (key.label != null && isUpCaseLetter(key.label.toString())) {
                    key.label = key.label.toString().toLowerCase();
                    key.codes[0] += 32;
                }
            }
        } else {
            for (Keyboard.Key key : keyList) {
                if (key.label != null && isLowCaseLetter(key.label.toString())) {
                    key.label = key.label.toString().toUpperCase();
                    key.codes[0] -= 32;
                }
            }
        }
        isCapes = !isCapes;
    }
    public void hideKeyboard() {
        keyContainer.clearAnimation();
        keyContainer.startAnimation(hideAnimation);
    }
    /**
     * 只起到延时开始显示的作用
     */
    private final Runnable showRun = new Runnable() {
        @Override
        public void run() {
            showKeyboard();
        }
    };
    private final Runnable hideEnd = new Runnable() {
        @Override
        public void run() {
            isHideStart = false;
            if (keyContainer.getVisibility() != View.GONE) {
                keyContainer.setVisibility(View.GONE);
            }
        }
    };
    private final Runnable showEnd = new Runnable() {
        @Override
        public void run() {
            isShowStart = false;
            // 在迅速点击不同输入框时, 造成自定义软键盘和系统软键盘不停的切换, 偶尔会出现停在使用系统键盘的输入框时, 没有隐藏
            // 自定义软键盘的情况, 为了杜绝这个现象, 加上下面这段代码
            if (!mEditText.isFocused()) {
                hideKeyboard();
            }
        }
    };
    private void showKeyboard() {
        keyboardView.setKeyboard(keyboardLetter);
        keyContainer.setVisibility(View.VISIBLE);
        keyContainer.clearAnimation();
        keyContainer.startAnimation(showAnimation);
    }
    private boolean isLowCaseLetter(String str) {
        String letters = "abcdefghijklmnopqrstuvwxyz";
        return letters.contains(str);
    }
    private boolean isUpCaseLetter(String str) {
        String letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        return letters.contains(str);
    }
    @SuppressLint("ClickableViewAccessibility")
    private void addListeners() {
        mEditText.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    hideSystemKeyBoard((EditText) v);
                    if (!isKeyboardShown() && !isShowStart) {
                        showHandler.removeCallbacks(showRun);
                        showHandler.postDelayed(showRun, SHOW_DELAY);
                    }
                }
                return false;
            }
        });
        mEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                boolean result = isValidTouch();
                if (v instanceof EditText) {
                    if (!hasFocus) {
                        if (result) {
                            if (isKeyboardShown() && !isHideStart) {
                                hideKeyboard();
                            }
                        } else {
                            hideKeyboard();
                        }
                    } else {
                        hideSystemKeyBoard((EditText) v);
                        if (result) {
                            if (!isKeyboardShown() && !isShowStart) {
                                showHandler.removeCallbacks(showRun);
                                showHandler.postDelayed(showRun, SHOW_DELAY);
                            }
                        } else {
                            showHandler.removeCallbacks(showRun);
                            showHandler.postDelayed(showRun, SHOW_DELAY + DELAY_TIME);
                        }
                    }
                }
            }
        });
    }
    public boolean isShow() {
        return isKeyboardShown();
    }
    //隐藏系统键盘关键代码
    private void hideSystemKeyBoard(EditText edit) {
        this.mEditText = edit;
        InputMethodManager imm = (InputMethodManager) this.mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm == null)
            return;
        boolean isOpen = imm.isActive();
        if (isOpen) {
            imm.hideSoftInputFromWindow(edit.getWindowToken(), 0);
        }
        int currentVersion = Build.VERSION.SDK_INT;
        String methodName = null;
        if (currentVersion >= 16) {
            methodName = "setShowSoftInputOnFocus";
        } else if (currentVersion >= 14) {
            methodName = "setSoftInputShownOnFocus";
        }
        if (methodName == null) {
            edit.setInputType(0);
        } else {
            try {
                Method setShowSoftInputOnFocus = EditText.class.getMethod(methodName, Boolean.TYPE);
                setShowSoftInputOnFocus.setAccessible(true);
                setShowSoftInputOnFocus.invoke(edit, Boolean.FALSE);
            } catch (NoSuchMethodException e) {
                edit.setInputType(0);
                e.printStackTrace();
            } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException e) {
                e.printStackTrace();
            }
        }
    }
    private boolean isKeyboardShown() {
        return keyContainer.getVisibility() == View.VISIBLE;
    }
    private boolean isValidTouch() {
        long thisTouchTime = SystemClock.elapsedRealtime();
        if (thisTouchTime - lastTouchTime > 500) {
            lastTouchTime = thisTouchTime;
            return true;
        }
        lastTouchTime = thisTouchTime;
        return false;
    }
}
SafeKeyboard 类是实现键盘输入等功能的核心,软键盘的输入、删除、显示、隐藏等等都需要在这里完成,其中最重要的部分代码就是:下面三个方面

6.1 SafeKeyboardView 要设置 OnKeyboardActionListener 监听,否则这个键盘就是没有意义的,并实现其中的每一个方法。最重要的就是onKey(),在onKey()方法中实现三个键盘输入类型的转换,上面的例子中是使用int类型的 keyboardType 变量来作为转换的依据。根据不同的状态作出不同的转换和显示。

其次,也是很重要的一点,如果是正常的输入字符的时候,不能单纯的使用:

editable.insert(start, Character.toString((char) primaryCode));

来把输入的字符存放到对应的EditText中,因为这是默认不选中EditText中文本情况,一旦选中文本就会造成最后的结果和期望的输入结果出现偏差,选中的文本没有被替换,只是纯粹的增加刚刚输入的字符,所以要获取EditText中光标的开始和结束位置,并把输入的字符替换掉选中的文本部分,就是使用下面的语句:

editable.replace(start, end, Character.toString((char) primaryCode));

6.2 隐藏系统默认输入法软键盘

        当我们点击到需要使用自定义软键盘的输入框EditText的时候,EditText获取焦点并弹出系统默认输入法键盘,这时候,我们就需要强制隐藏默认键盘。使用的是上面的 private void hideSystemKeyBoard(EditText edit) 方法,(感谢网络上各位开发者的分享)

6.3 自定义软键盘的显示和隐藏动画

        如果我们对于输入法的显示和隐藏没有过多的要求,那么可以直接设置软键盘容器的 Visibility 为 GONE 或者 VISIBLE,但是对于我这种有点强迫症的程序猿来说,能够把自己的项目的效果做的比较合情合理并给用户良好的体验,一直是我个人追求的事情。所以在上面的例子中增加了显示和隐藏动画,显示为从屏幕最下方滑动到固定位置,隐藏为从固定位置滑动到屏幕最下方,于是就多了Animation的使用。

整个项目中:软键盘的显示个隐藏逻辑如下:

        当目标 EditText 被点击或者获得焦点的时候,如果自定义软键盘没有显示出来并且不处于正在显示的状态,那么强制隐藏系统默认键盘,显示自定义软键盘,否则不显示。(这里正在显示状态存在的意义在于防止二次执行显示动画,由于正在显示状态动画执行的不确定的性,会造成软键盘突然移动到一定位置又回到初始位置重新移动)显示的逻辑为:符合显示条件后 使用 handler 触发 showRun ,在 Runnable 中直接 showKeyboard()方法,执行动画

       当目标 EditText 失去焦点的时候,如果自定义软键盘已经显示,并不处于正在隐藏状态,那么执行 hideKeyboard()方法,否则不执行

以上逻辑在代码中均有体现,实例项目源代码链接也会在文章结尾处贴出

BUG和解决:

1. 如果界面上有多个 EditText, 并使用不同的软键盘,那么迅速在使用系统软键盘和自定义软键盘的EditText之间迅速点击,来回切换,可能会出现点击到目标 EditText 的时候自定义软键盘没有出现的BUG,为了解决这个问题,上面的显示个隐藏逻辑中才使用了 Runnable 和 正在显示/隐藏 概念一起约束

2. 和1中问题一样,也有可能会出现迅速点击切换多次之后,焦点留在了使用默认软键盘的 EditText 中,这是系统软键盘显示,自定义软键盘也有可能显示,为了解决这个问题,在 showEnd 执行完毕后,查看当前EditText是否有焦点,没有就直接隐藏自定义软键盘,就不会出现这种问题

        以上就是关于Android自定义安全软键盘的开发过程的详细说明,有什么问题需要指正的欢迎评论交流。因为时间仓促,后期会对这篇文章进行修改。

感谢各位开发者的分享,参考项目:

1. https://www.cnblogs.com/liuyu0529/p/7793610.html 因为本人不善于UI设计,所以键盘布局基本和这个项目区别不大,根据个人喜好作了一些必要的调整

2. https://github.com/StomHong/CustomizeKeyboard

因为一些项目需要,我在网上查了一些资料,参考已有项目,对开发Android自定义软键盘重新梳理和编程工作,并且增加和优化一些功能,有需要的开发者可以根据自己的项目需要进行参考或者修改。

源码地址:GitHub:  https://github.com/SValence/SafeKeyboard
 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值