Flutter-自定义尺子

简介: Flutter-自定义尺子

案例分析

1.效果功能分析

  • 滑动选择刻度尺
  • 支持中间选择刻度值
  • 支持设置最大最小值
  • 支持设置默认值
  • 支持设置大刻度的子刻度数
  • 支持设置步长
  • 支持设置刻度尺、数字的颜色及大小
  • 支持滑动选中回调
  • 支持刻度尺回弹效果

2.功能拆解

  • 自定义Widget(继承StatefulWidget)。
  • 使用ListView实现水平滑动效果(3个子Widget,左右为空白,中间为刻度尺)。
  • 绘制刻度尺Widget(刻度线、刻度值)。
  • 监听滑动获取中间值并回调。
  • 手指抬起滑动停止粘性回弹。

3.功能参数

  • 默认值
  • 最小值
  • 最大值
  • 步长
  • 刻度尺的宽高
  • 大刻度子子刻度数
  • 单刻度宽度
  • 刻度线颜色及宽度
  • 刻度尺数值颜色及宽度
  • 中间刻度线颜色
  • 选择回调

4.功能代码实现

4.1 首先自定义Widget继承自StatefulWidget,
class RulerView extends StatefulWidget {
}
4.2 功能参数根据(3.功能参数)进行定义
class RulerView extends StatefulWidget {
  //默认值
  final int value;
  //最小值
  final int minValue;
  //最大值
  final int maxValue;
  //步数 一个刻度的值
  final int step;
  //尺子的宽度
  final int width;
  //尺子的高度
  final int height;
  //每个大刻度的子刻度数
  final int subScaleCountPerScale;
  //每一刻度的宽度
  final int subScaleWidth;
  //左右空白间距宽度
  double paddingItemWidth;
  //刻度尺选择回调
  final void Function(int) onSelectedChanged;
  //刻度颜色
  final Color scaleColor;
  //指示器颜色
  final Color indicatorColor;
  //刻度文字颜色
  final Color scaleTextColor;
  //刻度文字的大小
  final double scaleTextWidth;
  //刻度线的大小
  final double scaleWidth;
  //计算总刻度数
  int totalSubScaleCount;

  RulerView({
    Key key,
    this.value = 10,
    this.minValue = 0,
    this.maxValue = 100,
    this.step = 1,
    this.width = 200,
    this.height = 60,
    this.subScaleCountPerScale = 10,
    this.subScaleWidth = 8,
    this.scaleColor = Colors.black,
    this.scaleWidth = 2,
    this.scaleTextColor = Colors.black,
    this.scaleTextWidth = 15,
    this.indicatorColor = Colors.red,
    @required this.onSelectedChanged,
  }) : super(key: key) {
  }
  ...
  ...
}
4.3 需要对参数配置及默认值边界检查
 //检查最大数-最小数必须是步数的倍数
    if ((maxValue - minValue) % step != 0) {
      throw Exception("(maxValue - minValue)必须是 step 的整数倍");
    }
    //默认值 不能低于最小值 或者大于最大值
    if (value < minValue || value > maxValue) {
      throw Exception(
          "value 必须在minValue和maxValue范围内(minValue<=value<=maxValue)");
    }
    //总刻度数
    totalSubScaleCount = (maxValue - minValue) ~/ step;

    //检查总刻度数必须是大刻度子刻度数的倍数
    if (totalSubScaleCount % subScaleCountPerScale != 0) {
      throw Exception(
          "(maxValue - minValue)~/step 必须是 subScaleCountPerScale 的整数倍");
    }
    //空白item的宽度
    paddingItemWidth = width / 2;
4.4 真正的时刻到了

上面4.1,4.2,4.3其实都是准备工作,到现在我们再来看看这个尺子的效果:

其实在文章开头已经写了具体的实现思路,现在我们来具体分析下:

思路1 尺子的外貌

首先只看静止的尺子,有同学就会说,唉,不就是绘制一条横线,很多条竖线,还有一些数字呗,对,你说的对!没错,尺子的外貌就是考察我们自定义View的绘制,Flutter中的自定义View其实和Android基本差不多,具体下面会代码细说。


思路2 尺子的滑动

滑动呢?尺子是需要根据手指滑动而滑动,一说到滑动,就想到了事件分发,滑动冲突等等,上头。这里给同学想到一个简便方法,说起简便方法就想到我们上学时做题目,一个题目一个答案,解题过程会有很多种,往往有些学霸们会使用简便方法去解答,那么何为简便方法?


简便方法?

所谓简便方法就是利用之前已经验证的公式来快速解决这个问题。呦西,明白了,不就是投机取巧吗?我会我会。那么言归正传,找到已经支持滑动的控件不就是对应已经验证的公式吗,我太聪明了,说干就干!已经支持滑动的Widget,第一想到的就是ListView, 一个水平滑动的ListView,嘿嘿嘿,我想到了,看下图:

我们可以把尺子布局作为ListView的一个Item去看,然后尺子左右距离通过空白Item占位就可以了,那么空白占位是多少呢?根据效果图可知,尺子往右滑动到最后,尺子的最左边是停留在屏幕宽度中心的,所以说空白占位Item的宽度就是屏幕的宽度,哦了,撸代码

ListView.builder(
              physics: ClampingScrollPhysics(),
              padding: EdgeInsets.all(0),
              controller: _scrollController,
              scrollDirection: Axis.horizontal,
              itemCount: 3,
              itemBuilder: (BuildContext context, int index) {
                //2边的空白占位控件
                if (index == 0 || index == 2) {
                  return Container(
                    width: widget.paddingItemWidth,
                    height: 0,
                  );
                } else {
                  //刻度尺
                  return Container(
                    child: RealRulerView(
                      subGridCount: widget.totalSubScaleCount,
                      subScaleWidth: widget.subScaleWidth,
                      step: widget.step,
                      minValue: widget.minValue,
                      height: widget.height,
                      scaleColor: widget.scaleColor,
                      scaleWidth: widget.scaleWidth,
                      scaleTextWidth: widget.scaleTextWidth,
                      scaleTextColor: widget.scaleTextColor,
                      subScaleCountPerScale: widget.subScaleCountPerScale,
                    ),
                  );
                }
              },
            ),

到这里滑动的功能就解决了,下面就是要实现这个尺子的绘制,绘制尺子和Android基本一样,主要考察Flutter的CustomPaint,CustomPainter,canvas等相关的API使用,我这里直接给完整代码,主要注释已经在代码当中。

///真实刻度尺View
class RealRulerView extends StatelessWidget {
  const RealRulerView({
    Key key,
    this.subGridCount,
    this.subScaleWidth,
    this.minValue,
    this.height,
    this.step,
    this.scaleColor,
    this.scaleWidth,
    this.scaleTextColor,
    this.scaleTextWidth,
    this.subScaleCountPerScale,
  }) : super(key: key);

  //刻度总数
  final int subGridCount;

  //每个刻度的宽度
  final int subScaleWidth;

  //刻度尺的高度
  final int height;

  //刻度尺最小值
  final int minValue;

  //每个大刻度的小刻度数
  final int subScaleCountPerScale;

  //步长 一刻度的值
  final int step;

  //刻度尺颜色
  final Color scaleColor;

  //刻度尺宽度
  final double scaleTextWidth;

  //刻度线宽度
  final double scaleWidth;

  //数字颜色
  final Color scaleTextColor;

  @override
  Widget build(BuildContext context) {
    double rulerWidth = (subScaleWidth * subGridCount).toDouble();
    double rulerHeight = this.height.toDouble();
    return CustomPaint(
      size: Size(rulerWidth, rulerHeight),
      painter: RulerViewPainter(
        this.subScaleWidth,
        this.step,
        this.minValue,
        this.scaleColor,
        this.scaleWidth,
        this.scaleTextColor,
        this.scaleTextWidth,
        this.subScaleCountPerScale,
      ),
    );
  }
}

class RulerViewPainter extends CustomPainter {
  final int subScaleWidth;

  final int step;

  final int minValue;

  final Color scaleColor;

  final Color scaleTextColor;

  final double scaleTextWidth;

  final int subScaleCountPerScale;

  final double scaleWidth;

  Paint linePaint;

  TextPainter textPainter;

  RulerViewPainter(
    this.subScaleWidth,
    this.step,
    this.minValue,
    this.scaleColor,
    this.scaleWidth,
    this.scaleTextColor,
    this.scaleTextWidth,
    this.subScaleCountPerScale,
  ) {
    //刻度尺
    linePaint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.stroke
      ..strokeWidth = scaleWidth
      ..color = scaleColor;

    //数字
    textPainter = TextPainter(
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
  }

  @override
  void paint(Canvas canvas, Size size) {
    //绘制线
    drawLine(canvas, size);
    //绘制数字
    drawNum(canvas, size);
  }

  ///绘制线
  void drawLine(Canvas canvas, Size size) {
    //绘制横线
    canvas.drawLine(
      Offset(0, 0 + scaleWidth / 2),
      Offset(size.width, 0 + scaleWidth / 2),
      linePaint,
    );
    //第几个小格子
    int index = 0;
    //绘制竖线
    for (double x = 0; x <= size.width; x += subScaleWidth) {
      if (index % subScaleCountPerScale == 0) {
        canvas.drawLine(
            Offset(x, 0), Offset(x, size.height * 3 / 8), linePaint);
      } else {
        canvas.drawLine(Offset(x, 0), Offset(x, size.height / 4), linePaint);
      }
      index++;
    }
  }

  ///绘制数字
  void drawNum(Canvas canvas, Size size) {
    canvas.save();
    //坐标移动(0,0)点
    canvas.translate(0, 0);
    //每个大格子的宽度
    double offsetX = (subScaleWidth * subScaleCountPerScale).toDouble();
    int index = 0;
    //绘制数字
    for (double x = 0; x <= size.width; x += offsetX) {
      textPainter.text = TextSpan(
        text: "${minValue + index * step * subScaleCountPerScale}",
        style: TextStyle(color: scaleTextColor, fontSize: scaleTextWidth),
      );
      textPainter.layout();
      textPainter.paint(
        canvas,
        new Offset(
          -textPainter.width / 2,
          size.height - textPainter.height,
        ),
      );
      index++;
      canvas.translate(offsetX, 0);
    }
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

到这里,尺子的效果和滑动已经完毕了,但是治标不治本啊,我虽然滑动了尺子,滑动多少啊,我不知道啊?对哦,我要知道我滑动多少了啊,怎么办呢?

图中尺子滑动的距离和ListView滑动的距离是一直的,所以是相等的,所以只要我们监听ListView滑动,然后通过滑动的距离和刻度的起点值、每个刻度值,就可以计算出滑动了多少刻度值。

滑动刻度值=起始值+滑动距离/单个刻度距离*单个刻度值

监听ListView滑动多少,这里可以有个小知识点:

NotificationListener:

if (notification is ScrollStartNotification) {
  print('滚动开始');
}
if (notification is ScrollUpdateNotification) {
  print('滚动中');
}
if (notification is ScrollEndNotification) {
  print('停止滚动');
  if (_scrollController.position.extentAfter == 0) {
    print('滚动到底部');
  }
  if (_scrollController.position.extentBefore == 0) {
    print('滚动到头部');
  }
}

具体计算代码

 bool _onNotification(Notification notification) {
    //ScrollNotification是基类 (ScrollStartNotification/ScrollUpdateNotification/ScrollEndNotification)
    if (notification is ScrollNotification) {
      //距离widget中间最近的刻度值
      int centerValue = widget.minValue +
          //notification.metrics.pixels水平滚动的偏移量
          //先计算出滚动偏移量是滚动了多少个刻度,然后取整,在乘以每个刻度的刻度值就是当前选中的值
          (notification.metrics.pixels / widget.subScaleWidth).round() *
              widget.step;

      // 选中值回调
      if (widget.onSelectedChanged != null) {
        widget.onSelectedChanged(centerValue);
      }
      ...
    }
    return true; //停止通知
  }

当我们停止滑动,弹起手指,需要尺子回弹到最近刻度值上,因为每个刻度之前是有距离的,当我们滑动时是可能滑动到刻度之间位置,这时候抬手,我们是知道当前滑动距离是多少的,但是多出来或少出来一段距离,简单的说就是:

滑动的距离%单个刻度值≠0

所以其实我们只要取整就可以了,也就是四舍五入。好了基本的实现就到这里了,下面贴出完整代码。

完整代码
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

///自定义尺子
class RulerView extends StatefulWidget {
  //默认值
  final int value;

  //最小值
  final int minValue;

  //最大值
  final int maxValue;

  //步数 一个刻度的值
  final int step;

  //尺子的宽度
  final int width;

  //尺子的高度
  final int height;

  //每个大刻度的子刻度数
  final int subScaleCountPerScale;

  //每一刻度的宽度
  final int subScaleWidth;

  //左右空白间距宽度
  double paddingItemWidth;

  //刻度尺选择回调
  final void Function(int) onSelectedChanged;

  //刻度颜色
  final Color scaleColor;

  //指示器颜色
  final Color indicatorColor;

  //刻度文字颜色
  final Color scaleTextColor;

  //刻度文字的大小
  final double scaleTextWidth;

  //刻度线的大小
  final double scaleWidth;

  //计算总刻度数
  int totalSubScaleCount;

  RulerView({
    Key key,
    this.value = 10,
    this.minValue = 0,
    this.maxValue = 100,
    this.step = 1,
    this.width = 200,
    this.height = 60,
    this.subScaleCountPerScale = 10,
    this.subScaleWidth = 8,
    this.scaleColor = Colors.black,
    this.scaleWidth = 2,
    this.scaleTextColor = Colors.black,
    this.scaleTextWidth = 15,
    this.indicatorColor = Colors.red,
    @required this.onSelectedChanged,
  }) : super(key: key) {
    //检查最大数-最小数必须是步数的倍数
    if ((maxValue - minValue) % step != 0) {
      throw Exception("(maxValue - minValue)必须是 step 的整数倍");
    }
    //默认值 不能低于最小值 或者大于最大值
    if (value < minValue || value > maxValue) {
      throw Exception(
          "value 必须在minValue和maxValue范围内(minValue<=value<=maxValue)");
    }
    //总刻度数
    totalSubScaleCount = (maxValue - minValue) ~/ step;

    //检查总刻度数必须是大刻度子刻度数的倍数
    if (totalSubScaleCount % subScaleCountPerScale != 0) {
      throw Exception(
          "(maxValue - minValue)~/step 必须是 subScaleCountPerScale 的整数倍");
    }
    //空白item的宽度
    paddingItemWidth = width / 2;
  }

  @override
  State<StatefulWidget> createState() {
    return RulerState();
  }
}

class RulerState extends State<RulerView> {
  ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController(
      //初始位置
      initialScrollOffset:
          // ((默认值-最小值)/步长 )=第几个刻度,再乘以每个刻度的宽度就是初始位置
          (widget.value - widget.minValue) / widget.step * widget.subScaleWidth,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: widget.width.toDouble(),
      height: widget.height.toDouble(),
      child: Stack(
        alignment: Alignment.topCenter,
        children: <Widget>[
          NotificationListener(
            onNotification: _onNotification,
            child: ListView.builder(
              physics: ClampingScrollPhysics(),
              padding: EdgeInsets.all(0),
              controller: _scrollController,
              scrollDirection: Axis.horizontal,
              itemCount: 3,
              itemBuilder: (BuildContext context, int index) {
                //2边的空白控件
                if (index == 0 || index == 2) {
                  return Container(
                    width: widget.paddingItemWidth,
                    height: 0,
                  );
                } else {
                  //刻度尺
                  return Container(
                    child: RealRulerView(
                      subGridCount: widget.totalSubScaleCount,
                      subScaleWidth: widget.subScaleWidth,
                      step: widget.step,
                      minValue: widget.minValue,
                      height: widget.height,
                      scaleColor: widget.scaleColor,
                      scaleWidth: widget.scaleWidth,
                      scaleTextWidth: widget.scaleTextWidth,
                      scaleTextColor: widget.scaleTextColor,
                      subScaleCountPerScale: widget.subScaleCountPerScale,
                    ),
                  );
                }
              },
            ),
          ),
          //指示器
          Container(
            width: 2,
            height: widget.height / 2,
            color: widget.indicatorColor,
          ),
        ],
      ),
    );
  }

  ///监听刻度尺滚动通知
  bool _onNotification(Notification notification) {
    //ScrollNotification是基类 (ScrollStartNotification/ScrollUpdateNotification/ScrollEndNotification)
    if (notification is ScrollNotification) {
      print("-------metrics.pixels-------${notification.metrics.pixels}");
      //距离widget中间最近的刻度值
      int centerValue = widget.minValue +
          //notification.metrics.pixels水平滚动的偏移量
          //先计算出滚动偏移量是滚动了多少个刻度,然后取整,在乘以每个刻度的刻度值就是当前选中的值
          (notification.metrics.pixels / widget.subScaleWidth).round() *
              widget.step;

      // 选中值回调
      if (widget.onSelectedChanged != null) {
        widget.onSelectedChanged(centerValue);
      }
      //如果是否滚动停止,停止则滚动到centerValue
      if (_scrollingStopped(notification, _scrollController)) {
        select(centerValue);
      }
    }
    return true; //停止通知
  }

  ///判断是否滚动停止
  bool _scrollingStopped(
    Notification notification,
    ScrollController scrollController,
  ) {
    return
        //停止滚动
        notification is UserScrollNotification
            //没有滚动正在进行
            &&
            notification.direction == ScrollDirection.idle &&
            scrollController.position.activity is! HoldScrollActivity;
  }

  ///选中值
  void select(int centerValue) {
    //根据(中间值-最小值)/步长=第几个刻度,然后第几个刻度乘以每个刻度的宽度就是移动的宽度
    double x =
        (centerValue - widget.minValue) / widget.step * widget.subScaleWidth;
    _scrollController.animateTo(x,
        duration: Duration(milliseconds: 200), curve: Curves.decelerate);
  }
}

///真实刻度尺View
class RealRulerView extends StatelessWidget {
  const RealRulerView({
    Key key,
    this.subGridCount,
    this.subScaleWidth,
    this.minValue,
    this.height,
    this.step,
    this.scaleColor,
    this.scaleWidth,
    this.scaleTextColor,
    this.scaleTextWidth,
    this.subScaleCountPerScale,
  }) : super(key: key);

  //刻度总数
  final int subGridCount;

  //每个刻度的宽度
  final int subScaleWidth;

  //刻度尺的高度
  final int height;

  //刻度尺最小值
  final int minValue;

  //每个大刻度的小刻度数
  final int subScaleCountPerScale;

  //步长 一刻度的值
  final int step;

  //刻度尺颜色
  final Color scaleColor;

  //刻度尺宽度
  final double scaleTextWidth;

  //刻度线宽度
  final double scaleWidth;

  //数字颜色
  final Color scaleTextColor;

  @override
  Widget build(BuildContext context) {
    double rulerWidth = (subScaleWidth * subGridCount).toDouble();
    double rulerHeight = this.height.toDouble();
    return CustomPaint(
      size: Size(rulerWidth, rulerHeight),
      painter: RulerViewPainter(
        this.subScaleWidth,
        this.step,
        this.minValue,
        this.scaleColor,
        this.scaleWidth,
        this.scaleTextColor,
        this.scaleTextWidth,
        this.subScaleCountPerScale,
      ),
    );
  }
}

class RulerViewPainter extends CustomPainter {
  final int subScaleWidth;

  final int step;

  final int minValue;

  final Color scaleColor;

  final Color scaleTextColor;

  final double scaleTextWidth;

  final int subScaleCountPerScale;

  final double scaleWidth;

  Paint linePaint;

  TextPainter textPainter;

  RulerViewPainter(
    this.subScaleWidth,
    this.step,
    this.minValue,
    this.scaleColor,
    this.scaleWidth,
    this.scaleTextColor,
    this.scaleTextWidth,
    this.subScaleCountPerScale,
  ) {
    //刻度尺
    linePaint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.stroke
      ..strokeWidth = scaleWidth
      ..color = scaleColor;

    //数字
    textPainter = TextPainter(
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
  }

  @override
  void paint(Canvas canvas, Size size) {
    //绘制线
    drawLine(canvas, size);
    //绘制数字
    drawNum(canvas, size);
  }

  ///绘制线
  void drawLine(Canvas canvas, Size size) {
    //绘制横线
    canvas.drawLine(
      Offset(0, 0 + scaleWidth / 2),
      Offset(size.width, 0 + scaleWidth / 2),
      linePaint,
    );
    //第几个小格子
    int index = 0;
    //绘制竖线
    for (double x = 0; x <= size.width; x += subScaleWidth) {
      if (index % subScaleCountPerScale == 0) {
        canvas.drawLine(
            Offset(x, 0), Offset(x, size.height * 3 / 8), linePaint);
      } else {
        canvas.drawLine(Offset(x, 0), Offset(x, size.height / 4), linePaint);
      }
      index++;
    }
  }

  ///绘制数字
  void drawNum(Canvas canvas, Size size) {
    canvas.save();
    //坐标移动(0,0)点
    canvas.translate(0, 0);
    //每个大格子的宽度
    double offsetX = (subScaleWidth * subScaleCountPerScale).toDouble();
    int index = 0;
    //绘制数字
    for (double x = 0; x <= size.width; x += offsetX) {
      textPainter.text = TextSpan(
        text: "${minValue + index * step * subScaleCountPerScale}",
        style: TextStyle(color: scaleTextColor, fontSize: scaleTextWidth),
      );
      textPainter.layout();
      textPainter.paint(
        canvas,
        new Offset(
          -textPainter.width / 2,
          size.height - textPainter.height,
        ),
      );
      index++;
      canvas.translate(offsetX, 0);
    }
    canvas.restore();
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

相关文章
|
1月前
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
111 1
|
1月前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
131 1
|
1月前
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
80 1
|
1月前
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
86 1
|
1月前
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
84 1
|
3月前
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
114 3
|
4月前
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
109 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
4月前
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
64 0
|
4月前
|
开发者 容器 Java
Azure云之旅:JSF应用的神秘部署指南,揭开云原生的新篇章!
【8月更文挑战第31天】本文探讨了如何在Azure上部署JavaServer Faces (JSF) 应用,充分发挥其界面构建能力和云平台优势,实现高效安全的Web应用。Azure提供的多种服务如App Service、Kubernetes Service (AKS) 和DevOps简化了部署流程,并支持应用全生命周期管理。文章详细介绍了使用Azure Spring Cloud和App Service部署JSF应用的具体步骤,帮助开发者更好地利用Azure的强大功能。无论是在微服务架构下还是传统环境中,Azure都能为JSF应用提供全面支持,助力开发者拓展技术视野与实践机会。
21 0
|
4月前
|
开发框架 API 开发者
Flutter表单控件深度解析:从基本构建到高级自定义,全方位打造既美观又实用的移动端数据输入体验,让应用交互更上一层楼
【8月更文挑战第31天】在构建美观且功能强大的移动应用时,表单是不可或缺的部分。Flutter 作为热门的跨平台开发框架,提供了丰富的表单控件和 API,使开发者能轻松创建高质量表单。本文通过问题解答形式,深入解读 Flutter 表单控件,并通过具体示例代码展示如何构建优秀的移动应用表单。涵盖创建基本表单、处理表单提交、自定义控件样式、焦点管理和异步验证等内容,适合各水平开发者学习和参考。
111 0