Flutter中解決輸入框(TextField)被鍵盤遮擋問題

??最近在工作中遇到了文本框被輸入法遮擋的問題,在網上找了一些方法,一言難盡,現在很多人寫技術博客,要不就隨便轉一轉,或者隨便一寫,也不講解清楚,或者傳圖等。個人覺得不好,既然要寫技術博客,就要把他寫好,可能自己會麻煩點,費點事。但是,如果要寫博客,我覺得要盡量讓人理解,不要就放個鏈接,或者放段代碼等等,一副只可意會不可言傳的表情~~
??這是我寫博客的初衷,給自己留下知識,也給別人帶來知識。盡管你的一篇技術博客內容可能很簡單。最近在一本《靠譜》的書中讀到一節“讓對方聽得懂”,也是這么個意思,把讀你博客的讀者都當成小白,用簡練的文字向讀者講述你的知識。

扯遠了~
《Flutter的撥云見日》系列文章如下:
1、Flutter中指定字體(全局或者局部,自有字庫或第三方)
2、Flutter發布Package(Pub.dev或私有Pub倉庫)
3、Flutter中解決輸入框(TextField)被鍵盤遮擋問題

一、Flutter自帶文本框自適應輸入法buff

??首先一個頁面如果在buildView中被包裹在Scaffold組件中,那么很幸運Scaffold是自帶自適應輸入法彈出的,它有一個屬性resizeToAvoidBottomInset,用來控制Scaffold組件是否需要自適應輸入法彈出,重新計算view的高度,它是默認打開的。

Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: _buildContentView(context)  //被ListView或者SingleChildScrollView等滑動控件包裹的TextField
    );

效果如下圖(如果文本框被輸入法遮擋,Scaffold會默認重新計算整個View的高度,其實也就是減去輸入法的高度,讓文本框滑動不被遮擋):


normal.gif

并且隨著輸入的字數增加,文本框是可以自適應向上滑動的。自帶的就是香~??????

下面我們看下resizeToAvoidBottomInset設置為false,也就是不自適應輸入法的情況,如圖:

Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: _buildContentView(context) //被ListView或者SingleChildScrollView等滑動控件包裹的TextField
);

  _buildContentView(BuildContext context) {
    return Container(
      padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 20.0, ),
      child: Stack(
        children: [
///用SingleChildScrollView或者ListView都可以
//          SingleChildScrollView(
//            child: Column(
//                  children: [
//                    Text(
//                      '''
//                      ''',
//                      style: TextStyle(fontSize: 20.0, color: Colors.black),
//                    ),
//                   TextField(
//                    ),
//                  ],
//                )
             ListView(
                  children: [
                    Text(
                      '''
                      ''',
                      style: TextStyle(fontSize: 20.0, color: Colors.black),
                    ),
                   TextField(
                    ),
                  ],
             )
        ],
      ),
    );
  }
no_resize.gif

二、“變態”需求,文本框全顯示

??在Part 1中,其實我們可以看到flutter Scaffold已經為大家考慮了文本框被輸入法遮擋的問題,文本框也可以根據輸入的問題自適應向上滑動,可以木有辦法,PO要求文本框全部顯示粗來,怎么辦? ??????~

??木有辦法,只有把民工必備技能使出,只有把度娘、古哥請出來。還真別說,還真是亂七八糟的,沒一個講清楚,講透的。不是沒圖沒真相,就是貼了一段不知出處的代碼。算了算了,實踐出真知。

2.1 首先,看圖說話,下圖確實做到了,輸入法彈出是,文本框全顯示,??????(有兩把爛刷子~ ??)
resize_display_all.gif
2.2 嗯,前方高能,上一段不知出處的代碼????
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';

///
/// Helper class that ensures a Widget is visible when it has the focus
/// For example, for a TextFormField when the keyboard is displayed
///
/// 使用方法:
///
/// In the class that implements the Form,
///   Instantiate a FocusNode
///   FocusNode _focusNode = new FocusNode();
///
/// In the build(BuildContext context), wrap the TextFormField as follows:
///
///   new EnsureVisibleWhenFocused(
///     focusNode: _focusNode,
///     child: new TextFormField(
///       ...
///       focusNode: _focusNode,
///     ),
///   ),
///
/// Initial source code written by Collin Jackson.
/// Extended (see highlighting) to cover the case when the keyboard is dismissed and the
/// user clicks the TextFormField/TextField which still has the focus.
///
class EnsureVisibleWhenFocused extends StatefulWidget {
  const EnsureVisibleWhenFocused({
    Key key,
    @required this.child,
    @required this.focusNode,
    this.curve: Curves.ease,
    this.duration: const Duration(milliseconds: 100),
  }) : super(key: key);

  /// The node we will monitor to determine if the child is focused
  ///傳入FocusNode,用于監聽TextField獲取焦點事件
  final FocusNode focusNode;

  /// The child widget that we are wrapping
  final Widget child;

  /// The curve we will use to scroll ourselves into view.
  ///
  /// Defaults to Curves.ease.
  final Curve curve;

  /// The duration we will use to scroll ourselves into view
  ///
  /// Defaults to 100 milliseconds.
  final Duration duration;

  @override
  _EnsureVisibleWhenFocusedState createState() => new _EnsureVisibleWhenFocusedState();
}

///
/// We implement the WidgetsBindingObserver to be notified of any change to the window metrics
///實現WidgetsBindingObserver接口,監聽屏幕矩陣變化事件
class _EnsureVisibleWhenFocusedState extends State<EnsureVisibleWhenFocused> with WidgetsBindingObserver  {

  @override
  void initState(){
    super.initState();
    widget.focusNode.addListener(_ensureVisible);  ///監聽焦點事件
    WidgetsBinding.instance.addObserver(this);      ///監聽屏幕矩陣是否發生變化
  }

  @override
  void dispose(){
    WidgetsBinding.instance.removeObserver(this);
    widget.focusNode.removeListener(_ensureVisible);
    super.dispose();
  }

  ///
  /// This routine is invoked when the window metrics have changed.
  /// This happens when the keyboard is open or dismissed, among others.
  /// It is the opportunity to check if the field has the focus
  /// and to ensure it is fully visible in the viewport when
  /// the keyboard is displayed
  ///屏幕矩陣發生變化時系統調用,如鍵盤彈出或是收回
  @override
  void didChangeMetrics(){
    super.didChangeMetrics();
    if (widget.focusNode.hasFocus){ ///有焦點時,進入滑動顯示處理Function
      _ensureVisible();
    }
  }

  ///
  /// This routine waits for the keyboard to come into view.
  /// In order to prevent some issues if the Widget is dismissed in the
  /// middle of the loop, we need to check the "mounted" property
  ///
  /// This method was suggested by Peter Yuen (see discussion).
  ///等待鍵盤顯示在屏幕上
  Future<Null> _keyboardToggled() async {
    if (mounted){
      EdgeInsets edgeInsets = MediaQuery.of(context).viewInsets;
      while (mounted && MediaQuery.of(context).viewInsets == edgeInsets) {
        await new Future.delayed(const Duration(milliseconds: 10));
      }
    }

    return;
  }

  Future<Null> _ensureVisible() async {
    // Wait for the keyboard to come into view
    await Future.any([new Future.delayed(const Duration(milliseconds: 300)), _keyboardToggled()]);

    // No need to go any further if the node has not the focus
    if (!widget.focusNode.hasFocus){
      return;
    }
    // Find the object which has the focus
    //找到Current RenderObjectWidget,獲得當前獲得焦點的widget,這里既TextField
    final RenderObject object = context.findRenderObject();
    final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);

    // If we are not working in a Scrollable, skip this routine
    if (viewport == null) {
      return;
    }

    // Get the Scrollable state (in order to retrieve its offset)
    //獲取滑動狀態,目的是為了獲取滑動的offset
    ScrollableState scrollableState = Scrollable.of(context);
    assert(scrollableState != null);

    // Get its offset
    ScrollPosition position = scrollableState.position;
    double alignment;

    ///這里需要解釋下
    ///1、position.pixels是指滑動widget,滑動的offset(一般指距離頂部的偏移量(滑出屏幕多少距離))
    ///2、viewport.getOffsetToReveal(object, 0.0).offset 這個方法,可以看下源碼
    ///      他有一個alignment參數,0.0 代表顯示在頂部,0.5代表顯示在中間,1.0代表顯示在底部
    ///       offset是指view顯示在三個位置時距離頂部的偏移量
    ///       他們兩者相比較就可以知道當前滑動widget是需要向上還是向下滑動,來完全顯示TextField

    ///判斷TextField處于頂部時是否全部顯示,需不需下滑來完整顯示
    if (position.pixels > viewport.getOffsetToReveal(object, 0.0).offset) { 
      // Move down to the top of the viewport
      alignment = 0.0;
    ///判斷TextField處于低部時是否全部顯示,需不需上滑來完整顯示
    } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0).offset){
      // Move up to the bottom of the viewport
      alignment = 1.0;
    } else {
      // No scrolling is necessary to reveal the child
      return;
    }

    //這是ScrollPosition的內部方法,將給定的view 滾動到給定的位置,
   //alignment的意義和上面描述的一致, 三種位置頂部,底部,中間
    position.ensureVisible(
      object,
      alignment: alignment,
      duration: widget.duration,
      curve: widget.curve,
    );
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

這里的不可描述代碼挺長的,其實仔細看并不復雜,有很多基本我已經進行了中文解釋。大家多看代碼
1、傳入FocusNode,這里是為了監聽TextField獲取焦點情況
2、實現WidgetsBindingObserver接口,是為了監聽屏幕矩陣變化(輸入法彈出或收回)
3、在didChangeMetrics()方法中接受屏幕矩陣變化,進入滑動邏輯處理方法_ensureVisible()
4、在_ensureVisible()方法中首先會進行300毫秒的循環等待,等待輸入法顯示在屏幕中
5、然后獲取當前獲取焦點的RenderObject

    // Find the object which has the focus
    final RenderObject object = context.findRenderObject();
    final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);

6、獲取滑動widget的ScrollPosition,實則是為了獲取滑動的偏移量(也就是滑出屏幕距離)

    // Get the Scrollable state (in order to retrieve its offset)
    ScrollableState scrollableState = Scrollable.of(context);
    assert(scrollableState != null);

    // Get its offset
    ScrollPosition position = scrollableState.position;

7、先解釋viewport.getOffsetToReveal(object, 0.0).offset方法,可以看下源碼,他有一個alignment參數,0.0 代表顯示在頂部,0.5代表顯示在中間,1.0代表顯示在底部。offset是指view顯示在三個位置時距離頂部的偏移量

    ///判斷TextField處于頂部時是否全部顯示,需不需下滑來完整顯示
    if (position.pixels > viewport.getOffsetToReveal(object, 0.0).offset) { 
      // Move down to the top of the viewport
      alignment = 0.0;
    ///判斷TextField處于低部時是否全部顯示,需不需上滑來完整顯示
    } else if (position.pixels < viewport.getOffsetToReveal(object, 1.0).offset){
      // Move up to the bottom of the viewport
      alignment = 1.0;
    } else {
      // No scrolling is necessary to reveal the child
      return;
    }

7.1 position.pixels > viewport.getOffsetToReveal(object, 0.0).offset 代表這種情況


image.png
image.png

7.2 position.pixels < viewport.getOffsetToReveal(object, 1.0).offset 代表這種情況


image.png

8、根據7中比較得出的alignment也就是需要顯示的位置,調用ScrollPosition內部方法滑動至指定位置

    //這是ScrollPosition的內部方法,將給定的view 滾動到給定的位置,
   //alignment的意義和上面描述的一致, 三種位置頂部,底部,中間
    position.ensureVisible(
      object,
      alignment: alignment,
      duration: widget.duration,
      curve: widget.curve,
    );

文字多有難懂,意亂,該用圖時就用圖,上圖
第一個TextField的位置是alignment = 0.0, 底下那個TextField的位置是alignment = 1.0


滑動位置根據alignment.gif
2.3 使用方法

代碼中也是講到了,其實他就是一個包裝類,將TextField用EnsureVisibleWhenFocused類包裹就可以,并講FocusNode傳入,因為它需要監聽焦點

EnsureVisibleWhenFocused(
  focusNode: _contentFocusNode,
    child: TextField(

    ),
),

三、全顯需求解決,還剩下一個問題

??因為有時候我們使用ListView或者ScrollView,然后這些滑動View中有文本框,我們在頁面底部需要有一個Submit或者Next按鈕。 這需求并不變態,常規操作。

如圖:


底部固定懸浮按鈕.png

實現底部固定懸浮按鈕,想必大家都知道,類似于android中的FrameLayout,在Flutter中我們可以使用Stack和Positioned兩個widget實現。 這不難。

嗯~ 實現個這個有什么難度,小case!!!

可是當你再一點文本框輸入時,你傻臉了,什么鬼?
底部固定懸浮按鈕跟著輸入框一起上來了, ̄□ ̄||.png

什么原因?我是誰?我在哪?因為前面說過Scaffold默認會打開重新計算View高度的設置,而布局是Stack的,Next按鈕使用Positioned布局在離底部38.0px的地方,自然就出現在輸入法上面了~~~
但是又不能把Scaffold設置關掉,因為關了文本框又不會自適應滑動了~

Stack(
   children: [
      ListView(
         children: [],
      ),

      Positioned(
           bottom: 38.0,
           left: 0,
           right: 0,
           child: MaterialButton(
           ),
      ),
    ],
),

大寫尷尬~

后面思考了下,還是有辦法滴~
主要是有兩種方式:

3.1 第一種估計都能想到,監聽鍵盤彈出事件,來隱藏或者顯示Next按鈕

既然想到就開始action吧~,
1、這里我用了第三方庫來獲取鍵盤彈出事件--->flutter_keyboard_visibility: 3.2.2
2、自己動手類似于Part 2中一樣繼承StatefullWidget,自己搗鼓一個監聽鍵盤的包裝類

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:meta/meta.dart';

class EnsureButtonVisibleWhenFocused extends StatefulWidget {
  const EnsureButtonVisibleWhenFocused({
    Key key,
    @required this.child,
  }) : super(key: key);

  /// The child widget that we are wrapping
  final Widget child;

  @override
  _EnsureVisibleWhenFocusedState createState() => new _EnsureVisibleWhenFocusedState();
}

class _EnsureVisibleWhenFocusedState extends State<EnsureButtonVisibleWhenFocused> {
  bool isKeyboardVisible = false;

  @override
  void initState(){
    super.initState();
    KeyboardVisibility.onChange.listen((isKeyboardVisible) {
      if(this.mounted) {
        setState(() {
          this.isKeyboardVisible = isKeyboardVisible;
        });
      }
    });
  }

  @override
  void dispose(){
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return isKeyboardVisible ? SizedBox() : widget.child;
  }
}

3、將監聽鍵盤包裝類EnsureButtonVisibleWhenFocused包裹Next Button

EnsureButtonVisibleWhenFocused(
   child: Positioned(
     bottom: 38.0,
     left: 0,
     right: 0,
     child: MaterialButton(
     ),
   ),
)

4、成品展示,鐺鐺鐺鐺~


resize_display_all_with_next_display.gif
3.2 是不是感覺方法一過于繁瑣?魔改從無止境~

1、在Page頁面的build方法中加入

@override
Widget build(BuildContext context) {
    double bottom = MediaQuery.of(context).viewInsets.bottom;   ///這里bottom為0說明鍵盤沒有彈出,>0則是鍵盤彈出
}

2、在Positioned中加入如下鬼魅邏輯O(∩_∩)O哈哈~

Positioned(
   bottom: bottom > 0 ? 100.0 * -1 : 38.0, ///鍵盤彈出時,給Positioned的bottom設置負值,
                                                      ///那么它肯定被遺落在看不見的邊邊jiaojiao
                                                      ///當鍵盤收回時,給其設置正常的bottom就粗現了
   left: 0,
   right: 0,
    child: MaterialButton(
   ),
)

3、效果和3.1出奇的一致,不貼圖了,去試試吧~

四、收隊

一上午加一中午,寫博客實屬不易,很簡單的東西,要全部寫出來,寫清楚,講明白,還是很耗費時間的,我既然寫,就要把它寫清楚,講明白,這是我的初衷。希望是偶確實寫清楚,講明白的。如有不明白,歡迎留言,偶們一起探討,為您解憂。也有助于偶更好的寫清楚,講明白~

今天就到這吧~ 休息休息會兒

申明:禁用于商業用途,如若轉載,請附帶原文鏈接。http://www.lxweimin.com/p/5bf431c5d03d蟹蟹~

PS: 寫文不易,覺得沒有浪費你時間,請給個點贊~ ??

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。