Handler.postDelayed()精确延迟指定时间的原理

使用Handler.postDelayed()时的疑问

使用handler发送消息时有两种方式,post(Runnable r)post(Runnable r, long delayMillis)都是将指定Runnable(包装成PostMessage)加入到MessageQueue中,然后Looper不断从MessageQueue中读取Message进行处理。

然而我在使用的时候就一直有一个疑问,类似Looper这种「轮询」的工作方式,如果在每次读取时判断时间,是无论如何都会有误差的。但是在测试中发现Delay的误差并没有大于我使用System.out.println(System.currentTimeMillis())所产生的误差,几乎可以忽略不计,那么Android是怎么做到的呢?

Handler.postDelayed()的调用路径

一步一步跟一下Handler.postDelayed()的调用路径:

  1. Handler.postDelayed(Runnable r, long delayMillis)
  2. Handler.sendMessageDelayed(getPostMessage(r), delayMillis)
  3. Handler.sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis)
  4. Handler.enqueueMessage(queue, msg, uptimeMillis)
  5. MessageQueue.enqueueMessage(msg, uptimeMillis)

最后发现Handler没有自己处理Delay,而是交给了MessageQueue处理,我们继续跟进去看看MessageQueue又做了什么:

msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
    // New head, wake up the event queue if blocked.
    msg.next = p;
    mMessages = msg;
    needWake = mBlocked;
} else {
    ...
}

MessageQueue中组织Message的结构就是一个简单的单向链表,只保存了链表头部的引用(果然只是个Queue啊)。在enqueueMessage()的时候把应该执行的时间(上面Hanlder调用路径的第三步延迟已经加上了现有时间,所以叫when)设置到msg里面,并没有进行处理……WTF?

继续跟进去看看Looper是怎么读取MessageQueue的,在loop()方法内:

for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return;
    }
    ...
}

原来调用的是MessageQueue.next(),还贴心地注释了这个方法可能会阻塞,点进去看看:

for (;;) {
    if (nextPollTimeoutMillis != 0) {
        Binder.flushPendingCommands();
    }

    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
        // Try to retrieve the next message.  Return if found.
        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
        Message msg = mMessages;
        if (msg != null && msg.target == null) {
            // Stalled by a barrier.  Find the next asynchronous message in the queue.
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous());
        }
        if (msg != null) {
            if (now < msg.when) {
                // Next message is not ready.  Set a timeout to wake up when it is ready.
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
            } else {
                // Got a message.
                mBlocked = false;
                if (prevMsg != null) {
                    prevMsg.next = msg.next;
                } else {
                    mMessages = msg.next;
                }
                msg.next = null;
                if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                msg.markInUse();
                return msg;
            }
        } else {
            // No more messages.
            nextPollTimeoutMillis = -1;
        }
        ...
    }
}

可以看到,在这个方法内,如果头部的这个Message是有延迟而且延迟时间没到的(now < msg.when),会计算一下时间(保存为变量nextPollTimeoutMillis),然后在循环开始的时候判断如果这个Message有延迟,就调用nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞。nativePollOnce()的作用类似与object.wait(),只不过是使用了Native的方法对这个线程精确时间的唤醒。

精确延时的问题到这里就算是基本解决了,不过我又产生了一个新的疑问:如果Message会阻塞MessageQueue的话,那么先postDelay10秒一个Runnable A,消息队列会一直阻塞,然后我再post一个Runnable B,B岂不是会等A执行完了再执行?正常使用时显然不是这样的,那么问题出在哪呢?

再来一步一步顺一下Looper、Handler、MessageQueue的调用执行逻辑,重新看到MessageQueue.enqueueMessage()的时候发现,似乎刚才遗漏了什么东西:

msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
    // New head, wake up the event queue if blocked.
    msg.next = p;
    mMessages = msg;
    needWake = mBlocked;
} else {
    ...
}
...
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
    nativeWake(mPtr);
}

这个needWake变量和nativeWake()方法似乎是唤醒线程啊?继续看看mBlocked是什么:

Message next() {
    for (;;) {
        ...
        if (msg != null) {
            ...
        } else {
            // Got a message.
            mBlocked = false;
            ...
        }
        ...
    }
    ...
    if (pendingIdleHandlerCount <= 0) {
        // No idle handlers to run.  Loop and wait some more.
        mBlocked = true;
        continue;
    }
    ...
}

就是这里了,在next()方法内部,如果有阻塞(没有消息了或者只有Delay的消息),会把mBlocked这个变量标记为true,在下一个Message进队时会判断这个message的位置,如果在队首就会调用nativeWake()方法唤醒线程!

现在整个调用流程就比较清晰了,以刚刚的问题为例:

  1. postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;
  2. 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;
  3. MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
  4. Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;
  5. 直到阻塞时间到或者下一次有Message进队;

这样,基本上就能保证Handler.postDelayed()发布的消息能在相对精确的时间被传递给Looper进行处理而又不会阻塞队列了。

Round-E Problem-C of Google's APAC Test 2016 解题思路

Problem: Not So Random

这道题是2016年 Google 校招 APAC Test 的其中一题,也是2016年5月13日Google在北大宣讲会的 Mock Interview Part 的范例题:

There is a certain “random number generator” (RNG) which takes one nonnegative integer as input and generates another nonnegative integer as output. But you know that the RNG is really not very random at all! It uses a fixed number K, and always performs one of the following three operations:

  • with probability A/100: return the bitwise AND of the input and K
  • with probability B/100: return the bitwise OR of the input and K
  • with probability C/100: return the bitwise XOR of the input and K (You may assume that the RNG is truly random in the way that it chooses the operation each time, based on the values of A, B, and C.)

You have N copies of this RNG, and you have arranged them in series such that output from one machine will be the input for the next machine in the series. If > you provide X as an input to the first machine, what will be the expected value of the output of the final machine in the series?

Input

The first line of the input gives the number of test cases, T. T test cases follow; each consists of one line with six integers N, X, K, A, B, and C. Respectively, these denote the number of machines, the initial input, the fixed number with which all the bitwise operations will be performed (on every machine), and 100 times the probabilities of the bitwise AND, OR, and XOR operations.

Output

For each test case, output one line containing “Case #x: y”, where x is the test case number (starting from 1) and y is the expected value of the final output. y will be considered correct if it is within an absolute or relative error of 10^-9 of the correct answer. See the FAQ for an explanation of what that means, and what formats of real numbers we accept.

Limits

1 ≤ T ≤ 50.

0 ≤ A ≤ 100.

0 ≤ B ≤ 100.

0 ≤ C ≤ 100.

A+B+C = 100.

Small dataset

1 ≤ N ≤ 10.

0 ≤ X ≤ 10^4.

0 ≤ K ≤ 10^4.

Large dataset

1 ≤ N ≤ 10^5.

0 ≤ X ≤ 10^9.

0 ≤ K ≤ 10^9.

Sample

Input Output
3  
1 5 5 10 50 40 Case #1: 3.0000000000
2 5 5 10 50 40 Case #2: 3.6000000000
10 15 21 70 20 10 Case #3: 15.6850579098

In sample test case #1, the final output will be 5 if AND or OR happens and 0 if XOR happens. So the probability of getting 5 is (0.1 + 0.5) and the probability of getting 0 is 0.4. So the expected final output is 5 * 0.6 + 0 * 0.4 = 3.

In sample test case #2, the final output will be 5 with probability 0.72, and 0 otherwise.

位运算

首先理一下题意,把N个RNG串起来,求最后输出结果的期望值。这里很容易注意到,RNG内部的操作(与、或、异或)都是位运算,而一个二进制整数的期望可以用每一位比特的期望乘以所在位的权重计算得到,那么本题就可以简化为只考虑单个比特在系统内的流程来得到单个比特位的期望值。

单个RNG

只考虑一个bit的话,X和K的值组合只有4种,可以列出下面的操作结果表:

A B C
 0 & 0 = 0   0 | 0 = 0   0 ^ 0 = 0 
 0 & 1 = 0   0 | 0 = 1   0 ^ 1 = 1 
 1 & 0 = 0   0 | 0 = 1   1 ^ 0 = 1 
 1 & 1 = 1   1 | 1 = 1   1 ^ 1 = 0 

注意到这里虽然有A、B、C三种可能性,但是输入和输出都是0和1,是不是可以尝试一下用转移矩阵来做呢?

因为输入是X、输出是0或1,而K是相对固定的、每个RNG的K值都相同。因此,对照上面的结果表分别计算K=0和K=1时的转移矩阵:

k = 0:

[ 100 ,  0 ]

[ A , B+C ]

k = 1:

[ A , B + C ]

[ C , A + B ]

(即k=1,输入为0时,输出A%为0、(B+C)%为1;输入为1时,输出C%为0,(A+B)%为1)

这样,我们就将RNG内部当作了一个黑箱,将内部的三种操作简化成了输入输出的转移矩阵。

多个RNG串联

有了单个RNG的转移矩阵,K值又不变,就可以分别计算K=0和K=1时多个RNG的串联结果了,将N-1个矩阵相乘即可。

转移矩阵的定义:

/**
 * 使用double是因为题意要求输出结果精度达到10^-9
 */
private static class Matrix {
    private double a, b, c, d;
    public Matrix(double a, double b, double c, double d) {
        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
    }
}

矩阵的乘法:

 private static Matrix multiply(Matrix m1, Matrix m2) {
    double a = (m1.a * m2.a + m1.b * m2.c) / 100;
    double b = (m1.a * m2.b + m1.b * m2.d) / 100;
    double c = (m1.c * m2.a + m1.d * m2.c) / 100;
    double d = (m1.c * m2.b + m1.d * m2.d) / 100;
    return new Matrix(a, b, c, d);
}

矩阵相乘:

Matrix bitZero = new Matrix(100, 0, A, B + C);
Matrix bitOne = new Matrix(A, B + C, C, A + B);
Matrix bitZeroAfterK = bitZero;
Matrix bitOneAfterK = bitOne;
for (int n = 1; n < N; n++) {
    // Maybe some Fast-Matrix-Multiplication will do it faster.
    bitZeroAfterK = multiply(bitZeroAfterK, bitZero);
    bitOneAfterK = multiply(bitOneAfterK, bitOne);
}

这样,得到系统完整的转移矩阵后,通过判断每一位bit的X和K的值,带入矩阵求出结果每一位bit的概率和期望,加权相加即可得到最后结果:

double result = 0;
for (int i = 31; i >= 0; i--) {
    result *= 2;
    Matrix matrix = isLastZero(K, i) ? bitZeroAfterK : bitOneAfterK;
    double chanceOfOne = (isLastZero(X, i) ? matrix.b : matrix.d) / 100;
    result += chanceOfOne;
}
print(t, result);

完整的代码见我的Github:company/google/NotSoRandom.java

优化方案

考虑到本题的时间复杂度与X和K的大小无关,那么主要耗时的部分是这N-1个矩阵的乘法,是不是可以使用矩阵的快速乘法(例如Strassen算法)来进一步优化时间消耗,不过我怀疑这个算法对2x2的矩阵能有多大的优化空间,我这里偷懒就没再继续研究了-。-

Gson:Google的JSON解析库进阶使用

简介

Gson是Google发布的一个开源Java类库,能够很方便的在Java对象和JSON字符串之间进行序列化和反序列化。目前主流进行JSON解析的开源库主要有Fastjson、Jackson、Gson等,各有优劣,关于它们的对比分析见我在知乎上的这个回答:java中处理JSON的开源工具都有些什么?那个比较好用?在这篇文章中主要介绍一下Gson的进阶用法。

基本用法

Gson的基本用法非常简单,假如你有这样一个JSON文件:

{
    "name": "username",
    "age": 20,
    "admin": true
}

你只需要定义这样一个Java类:

public class User {
    private String name;
    private int age;
    private boolean admin;

    // IDE自动生成的Getter和Setter
}

然后,序列化和反序列化都只需要一句话就能搞定:

// 序列化
String string = new Gson().toJson(user);
// 反序列化
User user = new Gson().fromeJson(string, User.class);

泛型的反序列化

如果整个JSON字符串不是一个JSONObject而是JSONArray,使用上面的语句尝试反序列化时就会报错,使用类似List.class的代码无法通过编译,事实上这和Java泛型的实现机制有关系,Java的泛型对象的类型不能直接通过类型本身获取到。

在这里,Gson提供了TypeToken来实现得到泛型的实际类型,例如:

// 序列化
String string = new Gson().toJson(userList);
// 反序列化
List<User> userList = new Gson().fromeJson(string, new TypeToken<List<User>>(){}.getType());

这个TypeToken的实现过程比较精彩,对于理解Java的泛型实现有非常大的帮助,有兴趣的同学可以阅读一下TypeToken的源码

变量名的对应

通常使用JSON的场景是从服务器端获取数据,如果服务器编写接口的人编码风格与你不一致,直接使用Gson转换而来的对象的成员变量和方法会显得非常丑陋。例如如果接口使用下划线风格命名一个变量last_login_time,那么你在调用这个变量的方法就成了user.getLast_login_time(),这是强迫症所不能忍的。

幸好Google提供了非常方便的注解功能供接口变量和Java成员变量之间的映射,你只需要这么写:

public class User {
    private String name;
    private int age;
    private boolean admin;

    @SerializedName("last_login_time")
    private String lastLoginTime;

    // IDE自动生成的Getter和Setter
}

这样Gson就能自动将JSON中的last_login_time映射为Java类中的lastLoginTime变量了,在get和set的时候也是漂亮的驼峰命名法了:user.getLastLoginTime()

ps: 这个技巧在接口编写者英语不好的时候特别有用(逃

控制变量是否序列化

在实际使用过程中,会有各种情况导致我们的Java类与和接口JSON变量不同。比如说在本地我们需要在User类中定义一个loginTimes变量来记录登录的次数,这个变量是接口中没有的,我们序列化User传给服务器时也不希望有这个变量,如何处理这种情况呢?

Gson提供了@Expose注解来进行控制成员变量的序列化和非序列化,这个注解有两个变量:serializedeserialize,默认都是true。需要注意的是若要使这个注解生效,必须使用GsonBuilder.excludeFieldsWithoutExposeAnnotation()方法来构建Gson对象。

如果我们在对象中使用如下注解:

public class User {
    @Expose
    private String name;
    @Expose(serialize = false)
    private int age;
    @Expose(serialize = false, deserialize = false)
    private boolean admin;

    // IDE自动生成的Getter和Setter
}

Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();

那么在反序列化时只会给name和age字段赋值,而序列化时只输出name字段。

@Expose注解类似,Gson还提供了@Since注解来进行版本控制,使用GsonBuilder构建时指定版本后高于该版本的字段在序列化和反序列化时都将被忽略:

public class User {
    @Since(1.0)
    private String name;
    @Since(1.1)
    private int age;
    @Since(1.1)
    private boolean admin;

    // IDE自动生成的Getter和Setter
}

Gson gson = new GsonBuilder().setVersion(1.0).create();

此时,age和admin字段由于版本号高于Gson对象指定的1.0版本,在转换过程中会被自动忽略,也可以达到控制变量是否序列化的目的。

Android:实现类似QQ、微信的表情输入键盘

终于有时间继续写博客了,上一篇博客中提到的使用「adjustPan」模式来实现表情输入键盘的思路,在这一段时间的使用过程中出现了很多兼容性问题,各种机型(有无虚拟按键)、系统版本(高度返回值是否包含状态栏)返回的软键盘高度与实际的软键盘高度不同,监听的调用方式也不尽相同,最后导致界面出现错位。起始回头仔细想想,让软键盘覆盖表情键盘这种办法从代码角度看确实很傻。。。不够优雅。

研究了一段时间、翻看了一些开源项目的实现原理,特别是著名的「四次元」,给了我很大启发,我重写了一遍依赖库,给北邮人论坛客户端更新后,再没有人反馈表情键盘有问题了,下面简单谈谈实现的思路。

demo

SoftInputMode 模式

首先,Android系统在界面上弹出软键盘时会将整个Activity的高度压缩,即默认的SoftInputMode是「AdjustResize」,直觉上表情键盘的高度应该设置得和软键盘相同,显示表情键盘时的同时将软键盘收起。这种思路比较直观,比「AdjustPan」模式好的地方在于表情键盘只有两种状态(Visible/Gone),而不是三种(Visible/Invisible/Gone),处理起来逻辑上会更简单。

表情键盘的高度

如果按照上面的思路直接写,应该是这样:

emotionButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (mEmotionLayout.isShown()) {
            hideEmotionLayout();
        } else {
            showEmotionLayout();
        }
    }
});

代码很简单,几乎没有别的逻辑判断,同样的,我们在显示表情键盘之前,需要判断一下软键盘的高度,将其设置给表情键盘:

private void showEmotionLayout() {
    int softInputHeight = getSupportSoftInputHeight();
    if (softInputHeight == 0) {
        softInputHeight = sp.getInt(SHARE_PREFERENCE_TAG, 400);
    }
    hideSoftInput();
    mEmotionLayout.getLayoutParams().height = softInputHeight;
    mEmotionLayout.setVisibility(View.VISIBLE);
}

private void hideEmotionLayout(boolean showSoftInput) {
    if (mEmotionLayout.isShown()) {
        mEmotionLayout.setVisibility(View.GONE);
        if (showSoftInput) {
            showSoftInput();
        }
    }
}

界面跳动

但是运行起来会发现一个问题,点击按钮打开表情键盘时,输入框会上下跳动一下。分析一下原因,点击按钮后,先收起了软键盘,当前Activity的高度变高,输入框回到了界面底部;再打开表情键盘时,输入框又被顶上来,所有看起来点击按钮后输入框会上下跳动一下。无论是先隐藏软键盘还是先打开表情键盘都会有这个问题。

如果这时候去纠结隐藏软键盘和打开表情键盘如何同步的话就会走进一个牛角尖,去处理不同机型之间的兼容性问题了。其实解决思路非常简单,输入框不是会上下跳么,那固定它的位置不就好了?

举个例子,如果整个界面的根布局是LinearLayout,那么一个控件的位置其实是由它上面所有控件的高度决定的,如果它上面所有控件的高度都不变化,那即使整个Activity的高度变化(开/关软键盘)也不会影响这个控件的位置,也就不会发生跳动了。

我们假设有这样两个方法lockContentHeight()unlockContentHeight(),用来锁定和解锁输入框上面的所有控件的高度,那么点击按钮的监听就应该这样写:

@Override
public void onClick(View v) {
    if (mEmotionLayout.isShown()) {
        lockContentHeight();
        hideEmotionLayout(true);
        unlockContentHeight();
    } else {
        if (isSoftInputShown()) {
            lockContentHeight();
            showEmotionLayout();
            unlockContentHeight();
        } else {
            showEmotionLayout();
        }
    }
}

为了更好的表现形式,这里的unlockContentHeight()可以替换成unlockContentHeightDelayed(),即延迟一会(例如200ms)再解锁高度,留出播放软键盘收回动画的时间。

锁定和解锁高度

现在整个问题的关键就在于锁定和解锁控件的高度了,那么如何实现lockContentHeight()unlockContentHeight()这两个函数呢?我们仍以根布局是LinearLayout为例,一个典型的界面布局如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        />

    <include
        layout="@layout/reply_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</LinearLayout>

其中ListView的layout_height为0dp、layout_weight为1,这样这个ListView就会自动充满整个布局,这里ListView可以替换成任意控件(比如一个RelativeLayout,内部有更复杂的布局)。

当你需要锁定这个ListView的高度时:

private void lockContentHeight() {
    LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentView.getLayoutParams();
    params.height = mContentView.getHeight();
    params.weight = 0.0F;
}

将weight置0,然后将height设置为当前的height,在父控件(LinearLayout)的高度变化时它的高度也不再会变化。而解锁高度时这样做:

private void unlockContentHeightDelayed() {
    mEditText.postDelayed(new Runnable() {
        @Override
        public void run() {
            ((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;
        }
    }, 200L);
}

在上面的函数中解锁高度其实只有一句话:LinearLayout.LayoutParams.weight = 1.0F;,在Java代码里动态更改LayoutParam的weight,会导致父控件重新onLayout(),从而达到改变控件的高度的目的。

总结

整体思路基本上就是:

  1. 点击表情按钮
  2. 锁定内容高度
  3. 收起软键盘
  4. 显示表情键盘
  5. 解锁内容高度

是不是比上一篇博客中分析三种显示状态,监听软键盘变化而做同步变化的方法要简单明了很多?

详细代码见我的Github:dss886/Android-EmotionInputDetector

Android:表情输入键盘实现探索

本篇文章思路最终的实现效果还不是很完美,更好的实现方法见Android: 类似QQ、微信的表情输入键盘实现思路一文。

需求

最近在写北邮人论坛客户端时,有一个需求是实现像手机QQ、微信那样的表情输入键盘,效果图:

demo

表情键盘本身并不难做,无非就是一个带SlidingTab的ViewPager,困扰我的地方在于,如何正确处理系统软键盘与表情键盘之间的显隐关系。

Google了一下,大概有这么几种思路:

第一种:动态改变SoftInputMode

这篇博文是国内网上转载比较多的方法,软键盘显示时将SoftInputMode设置为「stateVisible|adjustResize」,表情键盘显示时调整为「adjustPan」。

但在我实际使用过程中效果并不理想,一是我需要在一个ListView的底部实现表情键盘,这样动态更改SoftInputMode会导致ListView上下跳动;二是切换到别的界面再切换回来时软键盘的显隐状态偶尔会有冲突,最终我放弃了这种方法。

第二种:Dialog

Emoticons-Keyboard这个项目的实现方法是直接在软键盘上覆盖显示一个Dialog,避开了大部分的显示逻辑操作,思路非常独特,可惜我编译运行后发现显示效果并不好,除了动画效果,最大的问题仍然是是从别的界面切换过来时,与软键盘的显示有冲突

基本思路

上面提到的两个项目给了我很大的启发,我反复尝试了微信、微博、手机QQ等应用的表情键盘逻辑,发现它们切换键盘并不会导致ListView跳动,如果没有别的什么黑科技的话,基本可以断定使用的SoftInputMode就是adjustPan。(SoftInputMode各个属性值的意义

既然是adjustPan就好说了,软键盘显示的时候不会导致ListView跳动,那么Activity的底部必然有一个跟软键盘相同高度的View被软键盘覆盖了,这个View其实就是表情输入键盘,这样点击表情按钮的时候只需要显示隐藏软键盘,背后的表情框就显示出来了。

思路有了,接下来就是梳理一下所需要的技术点:

  • 如何检测软键盘高度(用于动态设置表情键盘的高度)?
  • 在代码中如何手动显示/隐藏软键盘?
  • 如何防止从别的界面切换过来时,软键盘状态改变了有可能导致的显示冲突?

如果这三个问题解决了,需求就基本实现了。

检测软键盘的高度

直接上代码:

private int getSupportSoftInputHeight() {
    Rect r = new Rect();
    mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r);
    int screenHeight = mContext.getWindow().getDecorView().getRootView().getHeight();
    int softInputHeight = screenHeight - r.bottom;
    if (Build.VERSION.SDK_INT >= 20) {
        // When SDK Level >= 20 (Android L), the softInputHeight will contain the height of softButtonsBar (if has)
        softInputHeight = softInputHeight - getSoftButtonsBarHeight();
    }
    return softInputHeight;
}

这里的原理是通过当前的Activity拿到其RootView的高度,减去Activity本身实际的高度,就等于软键盘的高度了。但在实际应用过程中发现,某些Android版本下,没有显示软键盘时减出来的高度总是144,而不是零,经过反复研究,最后发现这个高度是包括了虚拟按键栏的,所以在API Level高于18时,我们需要减去底部虚拟按键栏的高度(如果有的话)。

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private int getSoftButtonsBarHeight() {
    DisplayMetrics metrics = new DisplayMetrics();
    mContext.getWindowManager().getDefaultDisplay().getMetrics(metrics);
    int usableHeight = metrics.heightPixels;
    mContext.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
    int realHeight = metrics.heightPixels;
    if (realHeight > usableHeight) {
        return realHeight - usableHeight;
    } else {
        return 0;
    }
}

将高度设置给表情键盘就比较简单了:

LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) mEmotionLayout.getLayoutParams();
linearParams.height = getSupportSoftInputHeight();

在代码中手动显示、隐藏软键盘

也是直接上代码了,这两个方法也比较容易查到:

private void showSoftInput() {
    mInputManager.showSoftInput(mEditText, 0);
}

private void hideSoftInput() {
    mInputManager.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
}

解决切换程序时的显示冲突

在默认状态(StateUnspecified)下,在程序内打开软键盘然后点击Home键或多任务键切换出去时,软键盘会收起。再次进入程序界面也不会打开,前文提到的两个项目就是在这种情况下会出现问题。如何保证软键盘和表情键盘的同步,直观反应就是监听软键盘的高度变化,查了一下,果然可以监听:

mEditText.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        int softInputHeight = getSupportSoftInputHeight();
        if (softInputHeight != lastSoftInputHeight) {
            // do Something
        }
    }
});

实际测试中,这个函数在运行时会调用很多次,我们只需要在高度变化时做处理即可。

states

如上图,一共有三种状态,表情键盘的状态分别为:gone、invisible和visible。分别判断这三个状态之间的转化关系,然后动态的设置Visiblity即可:

public void onGlobalLayout() {
    int softInputHeight = getSupportSoftInputHeight();
    if (softInputHeight != lastSoftInputHeight) {
        if (softInputHeight <= 0) {
            lastSoftInputHeight = softInputHeight;
            if (!notHideEmojiLayout) {
                mEmotionLayout.setVisibility(View.GONE);
            } else {
                notHideEmojiLayout = false;
            }
        } else {
            lastSoftInputHeight = softInputHeight;
            LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) mEmotionLayout.getLayoutParams();
            linearParams.height = softInputHeight;
            mEmotionLayout.setVisibility(View.INVISIBLE);
            if (linearParams.height == softInputHeight) {
                mEmotionLayout.setVisibility(View.INVISIBLE);
            } else {
                linearParams.height = softInputHeight;
            }

            sp.edit().putInt(SHARE_PREFERENCE_TAG, softInputHeight).apply();
        }
    }
}

一点小Bug

由于Android设备的多样性,软键盘高度不一致,所以需要动态的设置表情键盘的高度,然而程序在第一次软键盘弹出后才能检测到软键盘高度,但这时由于表情键盘高度与软键盘不一致,会导致显示有点异常。所以程序会将检测到的高度保存到SharedPreference中,在Activity加载时读出高度即可。

不过即使是这样,在整个程序第一次进入这个界面时还是会显示异常,暂时的解决办法是在其他软键盘弹出的页面检测一次软键盘高度

如果你有更好的办法,请留言交流~