思想

采用Visibility + Stack来显示、隐藏组件,而非每次都构建组件。

键盘弹出会导致组件移动,不再尝试实时获取键盘高度,而是选择直接通过通道获取键盘的固定高度。

实现:

1. Kotlin

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.view.WindowManager
import android.view.Display
import android.graphics.Rect
import android.view.View
import android.view.Window
import android.os.Build
import android.os.Bundle
import android.view.WindowInsets
import android.view.ViewConfiguration;
import android.content.Context;
import android.util.DisplayMetrics
import android.view.KeyCharacterMap
import android.view.KeyEvent



//class MainActivity: FlutterActivity() {}



class MainActivity: FlutterActivity(),  EventChannel.StreamHandler {

    private val channelName = "keyboard_event_channel"

    private var eventChannel: EventChannel? = null

    private var eventSink: EventChannel.EventSink? = null

    override fun onStart() {
        super.onStart()
        window.decorView.visibility = View.VISIBLE;
        //flutterEngine!!.lifecycleChannel.appIsResumed()
    }

    override fun onRestart() {
        super.onRestart()
        window.decorView.visibility = View.VISIBLE;
        //flutterEngine!!.lifecycleChannel.appIsResumed()
    }

    override fun onResume() {
        super.onResume()
        window.decorView.visibility = View.VISIBLE;
    }


    override fun onStop() {
        super.onStop()
        window.decorView.visibility = View.GONE;
    }

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        eventChannel = EventChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
        
        eventChannel?.setStreamHandler(this)
    }

    // eventChannel
    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        eventSink = events
        // dosomething
        val rootView =  window?.decorView?.rootView
        rootView?.viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
            override fun onGlobalLayout() {
                // 屏幕高度
                val screenHeight = rootView.height
                
                // 可视化区域
                val r = Rect()
                rootView.getWindowVisibleDisplayFrame(r)

                // 屏幕高度 - 可视化区域的bottom  == 键盘高度 + 导航栏
                var keypadHeight = screenHeight - r.bottom

                // 判断是否开启导航栏
                 val isNavBarVisible = isNavigationBarVisible()

                 //println("isNavBarVisible $isNavBarVisible")

                if(isNavBarVisible) {
                    keypadHeight = keypadHeight - getNavigationBarHeight()
                }  

                val displayMetrics = getResources().displayMetrics
                val logicalKeypadHeight = keypadHeight / (displayMetrics?.density ?: 1f)

                if (keypadHeight > screenHeight * 0.15) {
                    events?.success(logicalKeypadHeight.toDouble())
                } else {
                    events?.success(0.0)
                }
            }
         })
    }
    override fun onCancel(arguments: Any?) {
        eventSink = null
    }

     private fun getNavigationBarHeight(): Int {
        val resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android")
        return if (resourceId != null && resourceId > 0) {
            getResources().getDimensionPixelSize(resourceId) ?: 0
        } else {
            0
        }
    }

    private fun isNavigationBarVisible(): Boolean {
        //println("Build.VERSION.SDK_INT ${Build.VERSION.SDK_INT}")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val windowInsets = window.decorView.rootWindowInsets
            return  windowInsets?.isVisible(WindowInsets.Type.navigationBars())?: false
        } else {
            //val decorView = window.decorView
            
            //val b1 = decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_HIDE_NAVIGATION == 0

            //val b2 = ViewConfiguration.get(context).hasPermanentMenuKey();
            

            // return b2 && b1 && b3;

            
            val hasBackKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_BACK)
            val hasHomeKey = KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_HOME)
            val hasPermanentMenuKey = ViewConfiguration.get(context).hasPermanentMenuKey();

            //println("hasBackKey $hasBackKey")
            //println("hasHomeKey $hasHomeKey")
            //println("hasPermanentMenuKey $hasPermanentMenuKey")
            
            
            val display = window.windowManager.defaultDisplay

            val realDisplayMetrics = DisplayMetrics()
            display.getRealMetrics(realDisplayMetrics)
            val realHeight = realDisplayMetrics.heightPixels
            val realWidth = realDisplayMetrics.widthPixels

            val displayMetrics = DisplayMetrics()
            display.getMetrics(displayMetrics)
            val displayHeight = displayMetrics.heightPixels
            val displayWidth = displayMetrics.widthPixels
            //println("(realWidth - displayWidth) > 0 ${(realWidth - displayWidth) > 0}")
            //println("(realHeight - displayHeight) > 0 ${(realHeight - displayHeight) > 0}")
            //println("realHeight $realHeight")
            //println("displayHeight $displayHeight")

            val temporaryHidden = activity.window.decorView.visibility and View.SYSTEM_UI_FLAG_HIDE_NAVIGATION != 0
            //println("temporaryHidden $temporaryHidden")
            if (temporaryHidden) return false
            val decorView = activity.window.decorView
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                decorView.rootWindowInsets?.let{
                    return it.stableInsetBottom != 0
                }
            }
            return true

            
            
            //return hasPermanentMenuKey ||  hasBackKey || hasHomeKey || (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0;
        }
    }

}

2.dart

import 'dart:async';

import 'package:flutter/services.dart';

typedef KeyboardHeightCallback = void Function(double height);

class KeyBoardHeight {
  static const keboardEventChannel = EventChannel('keyboard_event_channel');

  StreamSubscription? streamSubscription;

  void onKeyboardHeightChanged(KeyboardHeightCallback callback) {
    if (streamSubscription != null) {
      streamSubscription!.cancel();
    }
    streamSubscription =
        keboardEventChannel.receiveBroadcastStream().listen((dynamic height) {
      callback(height as double);                                                                                             
    });
  }

  void dispose() {
    if (streamSubscription != null) {
      streamSubscription!.cancel();
    }
  }
}

3. 组件监听键盘高度变化

import 'package:flutter/material.dart';

import '../channel/keyboard_height.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: TestWidget(),
    );
  }
}

class TestWidget extends StatefulWidget {
  const TestWidget({Key? key}) : super(key: key);

  @override
  State<TestWidget> createState() => _TestWidgetState();
}

class _TestWidgetState extends State<TestWidget> {
  final KeyBoardHeight _keyBoardHeight = KeyBoardHeight();

  @override
  void initState() {
    super.initState();
    _keyBoardHeight.onKeyboardHeightChanged((double height) {});
  }

  @override
  Widget build(BuildContext context) {
    return const SizedBox();
  }
}