HarmonyOS 鸿蒙Next中自定义输入法相关问题

HarmonyOS 鸿蒙Next中自定义输入法相关问题 自定义输入法,实现了一个输入预览框,想放在软键盘左上角类似系统键盘。

请问如何在别的应用也能获取到软键盘高度,或者有没有别的替代方案,以实现预览框紧贴在软键盘上面。

4 回复

【解决方案】

开发者您好,软键盘高度变化可以使用on(‘keyboardHeightChange’)监听,示例代码如下:

import { BusinessError } from '@kit.BasicServicesKit';

try {
  windowClass.on('keyboardHeightChange', (data) => {
    console.info('Succeeded in enabling the listener for keyboard height changes. Data: ' + JSON.stringify(data));
  });
} catch (exception) {
  console.error(`Failed to enable the listener for keyboard height changes. Cause code: ${exception.code}, message: ${exception.message}`);
}

更多关于HarmonyOS 鸿蒙Next中自定义输入法相关问题的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


首先要明确:Android 系统并没有提供直接获取软键盘高度的公开 API,尤其是自定义输入法(IME)场景下,跨应用获取键盘高度需要结合 IME 的生命周期和视图监听来实现,以下是两种核心方案(优先推荐方案一,更贴合 IME 场景)。

方案一:利用输入法(IME)自身特性(最优解,跨应用有效)

自定义输入法的本质是一个 InputMethodService,软键盘本身是 IME 内部的视图(通常是 KeyboardView),因此我们不需要「跨应用获取」,而是直接在 IME 内部监听自身键盘视图的布局变化,就能得到准确的键盘高度和位置,预览框直接作为 IME 视图的一部分布局,天然支持跨应用。

核心思路

  1. 自定义 IME 的布局文件,将「输入预览框」和「软键盘视图」放在同一个布局中,预览框位于键盘视图的左上角上方。
  2. 监听软键盘视图的 OnLayoutChangeListener,获取键盘的实际显示位置和高度。
  3. 通过布局参数动态调整预览框的位置,确保它始终紧贴键盘左上角上方。
  4. 利用 InputMethodServiceonWindowShown()onWindowHidden() 生命周期方法,控制预览框的显示和隐藏。

具体实现步骤

1. 编写 IME 布局文件(res/layout/ime_main.xml)

将预览框和键盘视图垂直排列,预览框默认隐藏,待键盘显示后动态调整位置:

xml

<?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="wrap_content">

    <!-- 输入预览框(目标:键盘左上角上方) -->
    <TextView
        android:id="@+id/tv_input_preview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#E0FFFFFF"
        android:padding="8dp"
        android:text="输入预览:"
        android:textColor="#000000"
        android:textSize="16sp"
        android:visibility="gone"/>

    <!-- 软键盘视图 -->
    <android.inputmethodservice.KeyboardView
        android:id="@+id/keyboard_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_input_preview"
        android:background="#F5F5F5"/>

</RelativeLayout>
2. 在 InputMethodService 中实现核心逻辑

java

import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.widget.TextView;
import android.inputmethodservice.KeyboardView;

public class MyCustomIME extends InputMethodService {
    private TextView mInputPreview;
    private KeyboardView mKeyboardView;

    @Override
    public View onCreateInputView() {
        // 加载自定义IME布局
        View inputView = getLayoutInflater().inflate(R.layout.ime_main, null);
        
        // 初始化视图
        mInputPreview = inputView.findViewById(R.id.tv_input_preview);
        mKeyboardView = inputView.findViewById(R.id.keyboard_view);
        
        // 配置软键盘(此处省略你的自定义键盘配置,如加载键盘布局、设置按键监听等)
        // mKeyboardView.setKeyboard(new Keyboard(this, R.xml.keyboard_qwerty));
        // mKeyboardView.setOnKeyboardActionListener(...);
        
        // 监听键盘视图的布局变化(核心:获取键盘高度和位置)
        addKeyboardLayoutListener();
        
        return inputView;
    }

    /**
     * 监听软键盘视图布局变化,动态调整预览框位置
     */
    private void addKeyboardLayoutListener() {
        mKeyboardView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
            @Override
            public void onLayoutChange(View v, int left, int top, int right, int bottom,
                                       int oldLeft, int oldTop, int oldRight, int oldBottom) {
                // 只有当键盘视图的高度发生变化,且当前是显示状态时,才调整预览框
                if (bottom - top > 0 && (oldBottom - oldTop != bottom - top)) {
                    // 1. 获取软键盘的实际高度(bottom - top)
                    int keyboardHeight = bottom - top;
                    
                    // 2. 获取软键盘的左上角坐标(left, top)
                    int keyboardLeft = left;
                    int keyboardTop = top;
                    
                    // 3. 调整预览框的位置:紧贴键盘左上角上方
                    // 预览框的右侧对齐键盘左侧(x坐标=keyboardLeft)
                    // 预览框的底部对齐键盘顶部(y坐标=keyboardTop - mInputPreview.getHeight())
                    View.LayoutParams params = mInputPreview.getLayoutParams();
                    if (params instanceof RelativeLayout.LayoutParams) {
                        RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) params;
                        // 水平方向:对齐键盘左侧
                        layoutParams.leftMargin = keyboardLeft;
                        // 垂直方向:位于键盘顶部上方(如果需要紧贴,直接设置layout_below即可,此处为动态适配极端情况)
                        layoutParams.topMargin = keyboardTop - mInputPreview.getHeight();
                        mInputPreview.setLayoutParams(layoutParams);
                    }
                    
                    // 4. 显示预览框
                    mInputPreview.setVisibility(View.VISIBLE);
                    
                    // 可选:更新预览框内容(如当前输入的字符)
                    // updateInputPreviewContent();
                } else if (bottom - top == 0) {
                    // 键盘隐藏时,隐藏预览框
                    mInputPreview.setVisibility(View.GONE);
                }
            }
        });
    }

    /**
     * 可选:更新输入预览框的内容
     */
    private void updateInputPreviewContent() {
        // 此处根据你的输入逻辑,更新预览框显示的内容
        // 例如:mInputPreview.setText("输入预览:" + currentInputText);
    }

    @Override
    public void onWindowShown() {
        super.onWindowShown();
        // 键盘窗口显示时,确保预览框处于就绪状态
        if (mInputPreview != null) {
            mInputPreview.setVisibility(View.INVISIBLE); // 先隐藏,待布局变化后显示
        }
    }

    @Override
    public void onWindowHidden() {
        super.onWindowHidden();
        // 键盘窗口隐藏时,隐藏预览框
        if (mInputPreview != null) {
            mInputPreview.setVisibility(View.GONE);
        }
    }
}
3. 关键说明
  • 这种方案的核心优势:预览框是 IME 自身视图的一部分,不是悬浮窗,因此不需要申请悬浮窗权限,且在所有应用中都能正常工作,不会被第三方应用拦截。
  • 无需「跨应用获取软键盘高度」,因为软键盘本身就是 IME 提供的,直接监听自身视图即可得到最准确的高度和位置。
  • 预览框的「紧贴」通过布局参数控制(layout_below 或动态设置 margin),避免出现偏移。

方案二:悬浮窗方案(替代方案,需处理权限和兼容性)

如果你的预览框无法作为 IME 内部视图(例如需要跨 IME 显示),可以使用悬浮窗实现,但需要解决两个核心问题:获取软键盘高度悬浮窗权限

核心思路

  1. 监听系统的「窗口布局变化」,通过 ViewTreeObserver.OnGlobalLayoutListener 监听屏幕中可显示区域的变化,间接计算软键盘高度。
  2. 申请悬浮窗权限(Android 6.0 及以上需要动态申请,Android 10 及以上对悬浮窗有更严格的限制)。
  3. 根据计算出的软键盘高度,动态调整悬浮窗的位置,使其紧贴软键盘左上角上方。

关键实现(获取软键盘高度)

java

import android.app.Activity;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.Window;

public class KeyboardHeightHelper {
    private Activity mActivity;
    private OnKeyboardHeightChangedListener mListener;
    private int mScreenHeight;
    private int mKeyboardHeight;

    public interface OnKeyboardHeightChangedListener {
        void onKeyboardHeightChanged(int height, boolean isShowing);
    }

    public KeyboardHeightHelper(Activity activity, OnKeyboardHeightChangedListener listener) {
        mActivity = activity;
        mListener = listener;
        initScreenHeight();
        addGlobalLayoutListener();
    }

    private void initScreenHeight() {
        mScreenHeight = mActivity.getResources().getDisplayMetrics().heightPixels;
    }

    private void addGlobalLayoutListener() {
        View rootView = mActivity.getWindow().getDecorView().getRootView();
        rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                Rect rect = new Rect();
                // 获取当前窗口的可显示区域
                mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
                
                // 计算软键盘高度:屏幕高度 - 可显示区域的底部坐标
                int keyboardHeight = mScreenHeight - rect.bottom;
                
                // 过滤无效高度(避免因状态栏、导航栏变化导致的误判)
                boolean isKeyboardShowing = keyboardHeight > mScreenHeight * 0.15;
                
                if (isKeyboardShowing && keyboardHeight != mKeyboardHeight) {
                    mKeyboardHeight = keyboardHeight;
                    mListener.onKeyboardHeightChanged(mKeyboardHeight, true);
                } else if (!isKeyboardShowing && mKeyboardHeight > 0) {
                    mListener.onKeyboardHeightChanged(0, false);
                    mKeyboardHeight = 0;
                }
            }
        });
    }
}

注意事项(悬浮窗方案的弊端)

  1. 权限问题:Android 6.0+ 需要 android.permission.SYSTEM_ALERT_WINDOW,且需要引导用户到设置页面开启「悬浮窗权限」;Android 10+ 对后台悬浮窗有严格限制,在输入法场景下可能无法正常显示。
  2. 跨应用问题:该方案依赖 Activity,而 IME 是运行在后台的 Service,无法直接在 IME 中获取其他应用的 Activity 根视图,因此跨应用兼容性极差。
  3. 精度问题:通过「屏幕高度 - 可显示区域高度」计算的软键盘高度,会受到导航栏、状态栏、应用内布局变化的影响,精度不如方案一。

总结

  1. 优先选择方案一(IME 内部视图方案):无需权限、跨应用兼容、高度精准,是实现输入法预览框的最优解,预览框作为 IME 自身视图,天然紧贴软键盘。
  2. 方案二(悬浮窗方案)仅作为替代,存在权限、兼容性、精度等问题,不推荐在输入法场景下使用。
  3. 核心要点:自定义 IME 无需跨应用获取软键盘高度,直接监听自身 KeyboardView 的布局变化即可得到准确参数,预览框通过布局绑定实现紧贴。

鸿蒙Next自定义输入法基于ArkUI框架开发,使用TypeScript/ArkTS语言实现。开发者需继承InputMethodExtensionAbility基类,通过InputMethodController管理输入法核心功能,包括键盘绘制、文本输入、候选词处理等。系统通过InputMethodAgent与输入法交互,支持软键盘和物理键盘适配。输入法数据存储推荐使用Preferences或关系型数据库。

在HarmonyOS Next中,自定义输入法若要在其他应用界面获取软键盘高度并实现预览框精准贴附,核心在于利用ArkUI的键盘安全区域(Keyboard Safe Area)能力。

推荐方案:使用键盘安全区域监听

  1. 在自定义输入法的UI中,通过keyboardAvoidanceModesafeArea属性,可以响应系统键盘的弹出与高度变化。虽然直接跨进程获取其他应用的键盘高度受限,但输入法作为系统服务,其自身UI能感知到当前全局键盘安全区域的变更。
  2. 关键实现思路:在你的输入法预览框组件上,设置position: fixed或使用层叠布局,并将其底部与系统提供的键盘安全区域顶部对齐。可以通过SafeArea组件或safeArea属性来获取这个区域信息,当键盘弹出时,安全区域会变化,你的预览框位置会自动调整。
  3. 替代方案:如果预览框是输入法UI的一部分,更直接的方式是将预览框集成在输入法软键盘的整体布局内,作为其顶部组件。这样无需外部高度信息,布局自然位于键盘上方,且随键盘同步显示/隐藏。

注意事项:HarmonyOS Next的应用沙盒机制限制了直接查询其他应用窗口信息。因此,依赖键盘安全区域的变化响应,而非主动获取具体高度数值,是更符合系统设计的方式。请确保在输入法服务的UIAbility和UI页面中正确配置了键盘避免模式。

此方法能确保你的输入预览框在不同应用中都能与软键盘保持正确的相对位置。

回到顶部