Flutter控件TextField使用踩坑记

selection自动跳转

问题描述:

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

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

问题解决:

Column(
        children: <Widget>[
          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">
    <!-- This keeps the window background of the activity showing
         until Flutter renders its first frame. It can be removed if
         there is no splash screen (such as the default splash screen
         defined in @style/LaunchTheme). -->
    <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;
  }
}

复制代码

使用不再赘述~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值