Flutter控件TextField使用踩坑记

selection自动跳转

问题描述:

Column(
        children: [
          RaisedButton(
            onPressed: () {
              _controller.text = "newText";
            },
            child: Text("click me"),
          ),
          TextField(
            controller: _controller,
            autofocus: true,
          ),
        ],
      )
复制代码

当点击按钮通过TextEditingController去修改TextField内容时TextField的游标会自动移动到最前端。

问题解决:

Column(
        children: [
          RaisedButton(
            onPressed: () {
              _controller.text = "newText";
              
              //每次修改内容的时候需要再手动修改selection
              _controller.selection = TextSelection.fromPosition(
                  TextPosition(offset: _controller.text.length));
            },
            child: Text("click me"),
          ),
          TextField(
            controller: _controller,
            autofocus: true,
          ),
        ],
      )
复制代码

键盘覆盖输入框

问题描述:

笔者所在的项目采用的是混合开发模式(原生+Flutter),在Android端配置了一个FlutterActivity用于承载Flutter页面,但是在开发时发现在有TextField的界面键盘弹出时总是会覆盖输入框,于是笔者开启了漫长的踩坑之旅:

  • 首先是去找相关issue,无果
  • google后发现有开源大神写了一个辅助类EnsureVisible.dart, 于是发挥CV技能,无果。但是发现作者写了一行内容:

DEPRECATED. (Now integrated Into Flutter!!!). Ensure Visible for Flutter. Makes sure TextField or other widgets are scrolled into view when they receive input focus. Just pass the focusNode provided to your TextField inside the builder.

意思是:不用再折腾了,Flutter已经支持了该特性....

  • 因为笔者是Android开发,自然想到了Activity的adjustResize特性,于是尝试了一下,结果发现效果很棒,于是问题成功解决。

在默认创建FlutterApplication时系统默认创建的Activity配置如下:

<activity
    android:name=".MainActivity"
    android:launchMode="singleTop"
    android:theme="@style/LaunchTheme"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
    android:hardwareAccelerated="true"
    android:windowSoftInputMode="adjustResize">
    
    <meta-data
        android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
        android:value="true" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    intent-filter>
activity>
复制代码

可以看到是配置了windowSoftInputMode的,所以采用官方配置不会遇到我的问题。

之后我把上面提到的EnsureVisible类仔细研究了一下,看看它究竟解决了什么问题以及怎么解决的:

  • 首先是解决了什么问题:众所周知,adjustResize实现的效果其实是压缩了键盘上方的布局高度,但是布局的改变可能导致输入框部分或全部被挤到屏幕之外,这个时候它就会调用Scrollable的ensureVisible方法把输入框滚动到可见区
  • 然后是如何做到的:EnsureVisible用到了WidgetsBinding这个类,当页面布局发生改变的时候通过WidgetsBindingObserver的回调获取新的页面状态,看代码:
class _EnsureVisibleState extends State<EnsureVisible> with WidgetsBindingObserver {
  final FocusNode _focusNode = new FocusNode();
  bool _alreadyScrolling = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }
  
  @override
  void didChangeMetrics() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      //布局改变时回调
      if (_focusNode.hasFocus && !_alreadyScrolling) {
        final alignment = resolveAlignment();
        if (alignment != null) {
          _alreadyScrolling = true;
          Scrollable.ensureVisible(context,
            alignment: alignment,
            duration: widget.duration,
            curve: widget.curve,
          ).whenComplete(() => _alreadyScrolling = false);
        }
      }
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
  
 }
复制代码

在didChangeMetrics方法中获取需要滚动的参数resolveAlignment:

double resolveAlignment() {
    if (widget.alignment == null) {
      final RenderObject object = context.findRenderObject();
      final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
      if (viewport == null) {
        // If we have no viewport we don't attempt to scroll.
        return null;
      }
      ScrollableState scrollableState = Scrollable.of(context);
      if (scrollableState == null) {
        // If we can't find a ancestor Scrollable we don't attempt to scroll.
        return null;
      }
      ScrollPosition position = scrollableState.position;
      if (position.pixels > viewport.getOffsetToReveal(object, 0.0).offset) {
        // Move down to the top of the viewport
        return 0.0;
      }
      else if (position.pixels < viewport.getOffsetToReveal(object, 1.0).offset) {
        // Move up to the bottom of the viewport
        return 1.0;
      }
      else {
        // No scrolling is necessary to reveal the child
        return null;
      }
    }
    else {
      // Use supplied Alignment parameter.
      return 0.5 + (0.5 * widget.alignment.y);
    }
}
复制代码

当然,作者还引入了FocusNode,在键盘获取焦点的时候手动调用didChangeMetrics方法,但是我觉得并没有必要再多处理一次。

扩展:根据上面的思路笔者写了一个监听键盘弹出隐藏事件的Widget:

import 'package:flutter/material.dart';

typedef KeyboardShowCallback = void Function(bool isKeyboardShowing);

class KeyboardDetector extends StatefulWidget {

  KeyboardShowCallback keyboardShowCallback;

  Widget content;

  KeyboardDetector({this.keyboardShowCallback, @required this.content});

  @override
  _KeyboardDetectorState createState() => _KeyboardDetectorState();
}

class _KeyboardDetectorState extends State<KeyboardDetector>
    with WidgetsBindingObserver {
  @override
  void initState() {
    WidgetsBinding.instance.addObserver(this);
    super.initState();
  }

  @override
  void didChangeMetrics() {
    super.didChangeMetrics();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      print(MediaQuery.of(context).viewInsets.bottom);
      setState(() {
        widget.keyboardShowCallback
            ?.call(MediaQuery.of(context).viewInsets.bottom > 0);
      });
    });
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

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

复制代码

使用不再赘述~

你可能感兴趣的:(Flutter控件TextField使用踩坑记)