flutter刷新页面_用Flutter实现58App的首页

背景

Flutter作为全新跨平台应用框架,在页面渲染和MD开发上优势明显,可谓是业界一枝独秀。正好最近有这样的一个机会学习Flutter开发,我们便尝试用它开发一个MD风格的较复杂页面,来比较跟原生应用开发的优势。也是想通过对新框架的学习探索,找到适合自身应用的框架。

页面展示

首页是整个应用里边交互最为复杂的一个页面了,它集合了各种滑动方式,包括:纵向滑动、横向滑动、嵌套滑动;同时,也集合了各种动效,包括:下拉刷新、上拉加载、头图视差、二级吸顶、回到顶部、横向Banner和纵向News轮播等。

d51a2b575031754c71853dde3d044703.png

开发历程

  • 搭建了开发环境,新建flutter module并学习dart语法

  • 调研用Flutter实现CoordinatorLayout的方案

  • 实现了首页主框架的demo搭建,目前同样遇到了滑动冲突的问题,在调研解决方案

  • 解决了滑动冲突的问题,并集成了下拉刷新能力

  • 完成了各区块和feed流的静态UI内容,目前剩余feed流加载更多和负二楼动效

  • 实现首页feed流的加载更多功能

技术难点

两级吸顶

在Flutter中实现吸顶功能比较容易,使用SliverPersistentHeader控件或者间接使用该控件都可以满足吸顶的功能;更重要的是,它支持滑动过程中任意组件的吸顶,即多级吸顶功能。

既然多级吸顶都支持,那么两级吸顶就很轻松了,首页头部和feed流tab的两级吸顶是这样实现的:第一级,使用SliverAppBar(它内部就是一个SliverPersistentHeader控件),不仅可以吸顶,还带有折叠属性,折叠属性能更好的满足头部滑动时的动效处理;第二级,使用SliverPersistentHeader并自定义它的delegate,通过pinned属性灵活选择当前模块吸顶与否,这样可以实现任意组件的吸顶功能。

SliverAppBar(
pinned: true,
...,
bottom: PreferredSize(
child: Header(...),
preferredSize: Size(screenWidth, 15),
),
),
SliverPersistentHeader(
pinned: false,
delegate: _SliverColumnDelegate(
Column(...),
)
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverTabBarDelegate(
TabBar(...)
),
),

pinned的原理很简单,将它设置为true内容到达顶部后不会再跟随外层的ScrollView继续滚动;反之,内容则会滚动出容器外。

而native端实现这个二级吸顶却很费力,通常你可能需要事先隐藏一个跟吸顶内容一样的驻顶view在那里,然后在页面滚动时计算吸顶内容是否已经划至顶部,维护驻顶view的可见属性达到吸顶效果。

上面粗犷的两级吸顶完成了,但想要充分满足首页的折叠效果和准确的二级吸顶需求,还得深挖一下AppBar内部的折叠计算方法。

SliverAppBar折叠计算

SliverAppBar通常作为页面头部使用,是会随内容一起滑动的一个组件;它的构造方法中有四个Widget类型的参数。分析Widget类型的参数,是因为我们需要一个容器来满足自定义首页头部——它既能实现吸顶,又可以接入自定义组件。

  • leading // 左侧按钮

  • title // 标题

  • flexibleSpace // 可以展开区域

  • bottom // 底部内容区域

回顾一下首页的折叠展示效果,首先排除了leading,因为它的位置大小只是一个按钮的位置,显示比较局限;然后title受leading占位影响宽度有限制也无法满足需要;之后,就剩下两个参数可选了,从命名上看,感觉flexibleSpace更符合折叠效果的实现思路,然后一直在尝试使用其实现头部折叠的需求,但开发过程中发现折叠后的高度是无法达到预期的,最大高度也满足不了设计图给的高度。本来想直接排除法使用起bottom的,但是想到一遇到问题就绕过还是有点SUI。那么想知道为什么flexibleSpace会有高度限制,必然得看一下SliverAppBar的实现源码了。

class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMixin {
...
@override
Widget build(BuildContext context) {
assert(!widget.primary || debugCheckHasMediaQuery(context));
final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? widget.bottom.preferredSize.height + topPadding : null;

return MediaQuery.removePadding(
context: context,
removeBottom: true,
child: SliverPersistentHeader(
floating: widget.floating,
pinned: widget.pinned,
delegate: _SliverAppBarDelegate(
...
collapsedHeight: collapsedHeight,
topPadding: topPadding,
),
),
);
}
}

final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? widget.bottom.preferredSize.height + topPadding : null;

变量collapsedHeight代表了折叠后头部的高度,从它的计算表达式可看出:当widget.bottom == null的时候,collapsedHeight的值为null。换言之,如果不使用bottom,那么折叠高度是没有的。如果没有折叠后的高度会发生什么?这个需要进一步验证。

const double kToolbarHeight = 56.0;

class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
...
final double _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
@override
double get minExtent => collapsedHeight ?? (topPadding + kToolbarHeight + _bottomHeight);
}

从上面的源码看,如果collapsedHeight == null,那么折叠后的头部高度就是topPadding + kToolbarHeight了。topPadding是系统状态栏的高度,kToolbarHeight是个自定义常量。不难看出,bottom为空时折叠头部的高度就会是一个固定高度。那么反过来,想要自定义高度,就必须得使用bottom,折叠后的头部高度完全取决于bottom的高度(一般,系统状态栏的高度是确定的)。

你看,不是我们用排除法用了最后一个参数bottom,而是我们分析后知道不用它真得不行。

实现两级吸顶并明确了头部参数设置后,其实整个页面框架就基本拟定了。接下来,我们细化一下,看看头部控件具体怎么实现。

自定义头部

首页头部组件包括以下内容:

  1. 搜索栏和城市名吸顶

  2. 头图视差

基于之前首页native的开发经验,这些效果的实现其实可以由一个变量驱动完成,即首页头部的纵向滑动偏移值。这个偏移值参照它的初始位置,分为上偏移和下偏移。上偏移驱动处理搜索栏和城市名的动效,下偏移则驱动处理头图视差的动效。

通过自定义Header组件来处理搜索栏和城市名吸顶的动画,其中主要是借助外部传入的上偏移值驱动整个动画的完成。

import 'package:flutter/material.dart';
import 'package:wuba_flutter_lib/home/search_bar.dart';

const double SEARCH_MARGIN_LEFT = 15.0; // 搜索栏left居左位置

typedef OnOffsetChangeListener = Function(double percent);

class Header extends StatefulWidget {

Header({
Key key,
this.offset: 0.0,// 外部驱动的偏移属性
this.cityName,
this.onOffsetChangeListener,
}) : super(key: key);

final double offset;
final String cityName;
final OnOffsetChangeListener onOffsetChangeListener;

double searchLeft = SEARCH_MARGIN_LEFT;
double searchLeftEnd = SEARCH_MARGIN_LEFT;

@override
State<StatefulWidget> createState() => HeaderState();
}

class HeaderState extends State<Header> with TickerProviderStateMixin {

AnimationController searchBgColorAnimController;
Animation<Color> searchBgColor;

// 偏移值驱动动画属性
drive(offset) {
// 过渡比例
double percent = offset / 80.0 > 1.0 ? 1.0 : offset / 80.0;
// 偏移比例回调
if (widget.onOffsetChangeListener != null) {
widget.onOffsetChangeListener(percent);
}
// 搜索栏居左吸顶后的位置
widget.searchLeftEnd = SEARCH_MARGIN_LEFT + (widget.cityName ?? '').length * 22 + CITY_MARGIN_RIGHT;
// 搜索栏居左位置
widget.searchLeft = (SEARCH_MARGIN_LEFT + (widget.searchLeftEnd - SEARCH_MARGIN_LEFT) * percent);
// 背景颜色控制
searchBgColorAnimController.value = percent;
}

@override
void didUpdateWidget(Header oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.offset != oldWidget.offset) {
drive(widget.offset);
}
}

@override
void initState() {
super.initState();
searchBgColorAnimController = new AnimationController(vsync: this);
searchBgColor = ColorTween(
begin: Color(0xffffffff),
end: Color(0xffDADDE1),
).animate(
CurvedAnimation(
parent: searchBgColorAnimController,
curve: Interval(0.0, 1.0, curve: Curves.linear),
),
);
}

@override
Widget build(BuildContext context) {
return Stack(
overflow: Overflow.visible,
children: <Widget>[
// 搜索栏
SearchBar(
left: widget.searchLeft,
bgColor: searchBgColor.value,
...
),
...
],
);
}

}

头图视差 则使用了Container的矩阵变换属性,主要是对y轴进行位移,这个位移加以视差系数便能产生跟Header组件的视差效果。

// 矩阵
Matrix4 matrix = Matrix4.translationValues(0.0, _offset/*驱动y轴偏移*/, 0.0);

// 容器
Container(
transform: matrix,// 矩阵变换
width: screenWidth,
height: screenWidth,
child: Image.asset("assets/images/home_bg.jpg", fit: BoxFit.fill)
),

组件化思考

Flutter中分无状态组件StatelessWidget和有状态组件StatefulWidget,React中分无状态组件Stateless Component和高级组件Stateful Component,它们在组件化方面的设计思路是一样的。

组件化 越来越趋向于按状态划分设计,因为这样更贴合实际场景并满足需要。比如首页的区块列表场景中,有一些区块一旦设置后不会再发生状态改变,可理解为无状态的;而另有一些区块初始化后还需要做状态变更,它有了状态,可视为有状态的。无状态的区块和有状态的区块进行组件封装,便成了无状态组件和有状态组件。

首页区块中,无状态的组件主要包括:

  • BigGroup,大类页

  • SmallGroup,小类页

  • LocalNews,同城头条

  • LocalTribe,同城部落

  • BannerAd,广告Banner

有状态的组件目前只有一个:

  • Notification,通知提醒;没有下发通知链接或者请求后台后发现没有通知内容时需要隐藏

如此,按照首页区块的场景,我们便基于无状态组件设计封装了首页无状态组件类HomeStatelessWidget,而基于有状态组件实现了首页有状态组件HomeStatefulWidget

HomeStatelessWidget类封装,内部设有一个容器,然后需要指定它的大小,仅此而已。

abstract class HomeStatelessWidget<T> extends StatelessWidget implements PreferredSizeWidget {

HomeStatelessWidget(this.context, this.key, this.value);

final BuildContext context;
final String key;
final T value;

Widget get child;
double get height;

@override
Size get preferredSize {// 指定容器大小
return Size(MediaQuery.of(context).size.width, height);
}

@override
Widget build(BuildContext context) {
return Container(// 容器
width: preferredSize.width,
height: preferredSize.height,
color: Colors.white,
alignment: Alignment.centerLeft,
child: child,
);
}
}

HomeStatefulWidget类封装,和HomeStatelessWidget类近似,只是多了一个状态类HomeStatefulWidgetState,它用于管理组件的各种状态。

abstract class HomeStatefulWidget<T> extends StatefulWidget implements PreferredSizeWidget {

HomeSizeStatefulWidget(this.context, this.key, this.value);

final BuildContext context;
final String key;
final T value;

Widget get child;
double get height;

void initState(State state) {}

@override
Size get preferredSize {
return Size(MediaQuery.of(context).size.width, height);
}

@override
State<StatefulWidget> createState() => HomeStatefulWidgetState();
}

// 状态类
class HomeStatefulWidgetState extends State<HomeStatefulWidget> {
@override
void initState() {
super.initState();
widget.initState(this);
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.preferredSize.width,
height: widget.preferredSize.height,
color: Colors.white,
alignment: Alignment.centerLeft,
child: widget.child,
);
}
}

无状态组件实现起来很容易,只需要给它一次性赋值就可以了,这里不做过多解释。接下来,看看有状态的组件是如何实现的!

通知提醒组件因为需要改变可见性状态,所以要实现首页有状态的组件类HomeStatefulWidget才能满足状态的管理,如下是通知提醒组件的代码实现。

这一点跟native相比,优势还是很明显的。因为native端在view的设计上没有“状态”这个概念,它对状态的概念完全是模糊的。

class Notification extends HomeStatefulWidget<String> {

// 状态字段,当通知内容为空时控制当前组件是否可见
bool isContentEmpty = true;

Notification(BuildContext context, String key, String value) : super(context, key, value);

@override
void initState(State<StatefulWidget> state) {
super.initState(state);
// 如果url不为空,则请求通知接口数据
if (!isUrlEmpty()) {
HomeDataManager.getNotification(value).then((object) {
// 获取到通知数据,改变组件的可见性状态
state.setState(() {
isContentEmpty = object == null;
});
});
}
}

@override
Widget get child => isUrlEmpty() || isContentEmpty ? Container() : Center(child: Text(value));

/// 如果url为空,或是通知接口返回的内容为空,则隐藏自己;
/// 否则,显示自己。
@override
double get height => isUrlEmpty() || isContentEmpty ? 0 : 40;

// 判断传入的url是否为空
bool isUrlEmpty() => (value == null || '' == value);

}

滑动冲突

Android中,只要两个“轮子”有嵌套关系,那么势必存在滑动冲突的问题。要解决嵌套滑动冲突,就只能允许一个轮子驱动,而另一个轮子被带动;而不是两个轮子同时驱动

5bea9ca05d2d488802c3862dec662a2e.gif

首页中存在两级冲突问题,也就是说有两层嵌套关系。一,下拉刷新和首页主体;二,首页主体和feed流内容。这相当于有三个轮子存在相互嵌套的关联,如何解决三个轮子的滑动冲突问题,这里有三种思路:

  1. 由一个轮子驱动,另外两个轮子同步被带动;

  2. 由一个轮子驱动,另一个轮子被带动,还有一个轮子卸载;

  3. 由一个轮子先驱动,到达某个位置后转换为另一个轮子驱动,然后剩下的两个轮子跟1和2情况。

三种思路其实都是将三个轮子的嵌套关系进行了降维处理,本质上都在解决两个轮子的冲突问题;总之,核心思想是不能出现两个轮子同时驱动。

NestedScrollViewRefreshIndicator(// 下拉刷新
child: ExtendedNestedScrollView(// 首页主体
keepOnlyOneInnerNestedScrollPositionActive: true,
headerSliverBuilder: (c, f) {
return <Widget>[
SliverAppBar(), // 头部搜索
SliverPersistentHeader(),// 区块列表
SliverPersistentHeader(),// feed流TabBar
];
},
body: TabBarView(// feed流内容
children: [
Container(
child: ListView(),// 推荐
),
Container(
child: ListView(),// 家乡
),
Container(
child: ListView(),// 部落 
),
],
),
),
)

首页主体控件使用了NestedScrollView的扩展类ExtendedNestedScrollView,前者允许嵌套滚动,但是对子视图的高度有要求——确定的高度。做过feed流的开发都知道,它的高度并不好计算,因为模板类型不同对应各自的高度不等,加以本身又可以无限加载扩展,高度一直在变化计算起来难度很大。基于NestedScrollView的扩展类ExtendedNestedScrollView解决了这个痛点,在不依赖子视图高度的情况下同样能够满足嵌套滚动。

解决滑动冲突问题,离不开它的这个重要属性keepOnlyOneInnerNestedScrollPositionActive,直译是仅保活一个内部嵌套的滚动位置,意译便是仅允许一个内部嵌套的视图滚动,即仅允许一个轮子驱动。

if (widget.keepOnlyOneInnerNestedScrollPositionActive) {
///get notifications and compute active one in _innerController.nestedPositions
body = NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (((notification is ScrollEndNotification) ||
(notification is UserScrollNotification &&
notification.direction == ScrollDirection.idle)) &&
notification.metrics is PageMetrics &&
notification.metrics.axis == Axis.horizontal) {
_coordinator._innerController
._computeActivatedNestedPosition(notification);
}
return false;
},
child: body);
}

这里的条件判断计算,其实已经能看出来了,它是实现了上面思路3的做法。此时,首页的两级嵌套滚动冲突解决方案其实已经浮出水面了,只剩下最后一个轮子的处理了,具体是使用情况1还是情况2呢?

ScrollController _scrollController = ScrollController();

_scrollListener() {
setState(() {
_offset = _scrollController.offset;
});
}

@override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
}

NestedScrollViewRefreshIndicator(// 下拉刷新
// _offset > 0.0 表示头部上移动,这时候禁止notification事件处理
notificationPredicate: (notification) => _offset == 0.0,
child: ExtendedNestedScrollView(// 首页主体
controller: _scrollController,
keepOnlyOneInnerNestedScrollPositionActive: true,
...
),
)

这个问题其实不是一个单选,具体在应用场景中,最终两者都有用到。下滑到达顶部,此时需要触发下拉刷新操作,随即下拉刷新模块被带动,那么就实现了思路1的做法;而其他位置的滑动,则不会触发这个操作,所以可以理解为将其暂时卸载,那么就有了思路2的做法。整体首页的实现,其实是综合应用了这三种思路。

下拉刷新

  • 下拉高度限制

  • 负二楼 // TODO

默认的下拉刷新组件在下拉时可以一直往下,没有对滑动距离做限制,而首页要求下拉至头图完整出现后不再滚动。这个特定的需求RefreshIndicator并不能满足,需要改动一下这个组件才可以。

class NestedScrollViewRefreshIndicator extends StatefulWidget {
final OnOffsetCallback onOffset;// 下拉偏移量回调
final double offsetLimit;// 下拉偏移的限制值
const NestedScrollViewRefreshIndicator({
this.onOffset,
this.offsetLimit = 0.0,
...
});
}

class NestedScrollViewRefreshIndicatorState
extends State<NestedScrollViewRefreshIndicator>
with TickerProviderStateMixin<NestedScrollViewRefreshIndicator> {

AnimationController _positionController;
AnimationController _scaleController;

Animation<RelativeRect> _positionRect;
Animation<RelativeRect> _positionRectDown;
Animation<RelativeRect> _positionRectUp;

Animatable _headerPositionTweenDown;
Animatable _headerPositionTweenUp;

double _headerOffset = 0.0;// 头部偏移值

@override
void initState() {
super.initState();
_headerPositionTweenDown = RelativeRectTween(
begin: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
end: RelativeRect.fromLTRB(0.0, widget.offsetLimit, 0.0, 0.0)
);

_positionController = AnimationController(vsync: this);
_scaleController = AnimationController(vsync: this);

_positionRectDown = _positionController.drive(_headerPositionTweenDown);
_positionRect = _mode != _RefreshIndicatorMode.done ? _positionRectDown : _positionRectUp;

if (widget.onOffset != null) {
_positionController.addListener(() {
_headerOffset = _positionController.value;
widget.onOffset(_headerOffset);
double value = widget.offsetLimit * _headerOffset;
_headerPositionTweenUp = RelativeRectTween(
begin: RelativeRect.fromLTRB(0.0, value, 0.0, 0.0),
end: RelativeRect.fromLTRB(0.0, 0, 0.0, 0.0)
);
_positionRectUp = _scaleController.drive(_headerPositionTweenUp);
});
_scaleController.addListener(() {
double value = (1.0 - _scaleController.value) * _headerOffset;
widget.onOffset(value);
});
}
}

setPositionRect(newMode) {
setState(() {
_positionRect = newMode != _RefreshIndicatorMode.done ? _positionRectDown : _positionRectUp;
});
}

// _RefreshIndicatorMode.drag
bool _handleScrollNotification(ScrollNotification notification) {
...
setPositionRect(_RefreshIndicatorMode.drag);
return false;
}

// _RefreshIndicatorMode.canceled || _RefreshIndicatorMode.done
Future<void> _dismiss(_RefreshIndicatorMode newMode) async {
...
setPositionRect(newMode);
switch (newMode) {
case _RefreshIndicatorMode.done:
await _scaleController.animateTo(1.0, duration: _kIndicatorScaleDuration);
break;
case _RefreshIndicatorMode.canceled:
await _positionController.animateTo(0.0, duration: _kIndicatorScaleDuration);
break;
default:
assert(false);
}
}

// _RefreshIndicatorMode.refresh
void _show() {
...
_positionController.animateTo(1.0 / _kDragSizeFactorLimit, duration: _kIndicatorSnapDuration).then<void>((void value) {
setPositionRect(_RefreshIndicatorMode.refresh);
});
}

@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
// child, // 改动前
PositionedTransition(// 改动后
rect: _positionRect,
child: child,
),
...
],
);
}
}

以上便是摘出的改动了下拉刷新控件的代码逻辑,主要是通过位置动画限定了首页主体向下滚动的最大位移。同时,通过动画偏移的计算,向外输出了头部偏移的值,以便于外部通过监听这个偏移值做更多的动效处理;比如:搜索框、天气、城市、头部、扫码、背景图等头部元素的动画处理。

负二楼的效果实现其实并不复杂,能理解如何通过动画原理改动下拉刷新控件从而实现个性化的动效,那么实现负二楼的效果就是个举一反三的事情。

加载更多

加载更多的原理其实跟native的思路是一样的——判断列表滚动到最末位置触发特定事件。之前native的做法就是判断RecyclerView滑动到最后一项时向feed流最末位置插入一个特定的动画模板,等加载结束后再把这个模板去掉,然后把请求到的内容添加到视图列表中去,这样列表组件就拥有了一个加载更多的能力。

ExtendedNestedScrollView的改动:

double nestOffset(double value, _NestedScrollPosition target) {
// 滑动到小于50的时候触发加载更多事件
if (target.extentAfter < 50) {
_onLoadMore();
}
}

总结

这样,一个由Flutter开发的首页就已经基本落地了。整个开发过程总结下来,有这样几点可以分享:

  1. 用Flutter和Android开发首页,都依赖了MD组件,它们对此支持得都比较完善;由于Dart语言的特性,Flutter在使用这些组件时更容易扩展、灵活性更强。

  2. Flutter状态化的组件管理机制,显得比Android更切合场景,在区块列表的设计上得心应手,这点也是众多前端框架的亮点。

  3. Flutter的动画设计api很丰富,能充分满足各种UI动效,让页面开发更轻松且不复杂。

  4. Flutter表达性更强,又加以跨平台的解决方案,减少了代码量并大大提升了开发效率,为应用开发起到了开源节流的作用。

  5. Flutter作为新秀,在Java老大哥已经烂熟于MVP等模式设计后,Flutter在此方面还需要积累;也可能Flutter本身并不需要这样的积累,它等待的是比Java中更好的开发模式。

参考文档

Flutter扩展NestedScrollView

118b998c7af91856d60e710ab71d674d.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/244478.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python调用报表制作工具_使用Python轻松制作漂亮的表格

Python太有用而且很方便 图表可以用matplotlib轻松制作&#xff0c;数值计算只要有numpy就行。 最近&#xff0c;Python被广泛用于机器学习系统的研究&#xff0c;甚至还能制作游戏。 我突然想知道&#xff1a;“是否可以用Python来制作图表而不是表格&#xff1f;” 这个时候&…

小米网关控制空调伴侣_小爱同学怎么控制灯?

说说我们神奇小爱同学吧&#xff0c;小爱同学是小米旗下的一款智能AI音箱&#xff0c;会根据您的指令来操作电器设备&#xff0c;比如说开关灯&#xff0c;那么小爱同学怎么控制灯&#xff1f;如果家里的是传统的灯泡&#xff0c;不是智能灯连接还能控制吗&#xff1f;今天蜜罐…

bochs上网镜像怎么上网_【干货科普】上网慢!经常掉线!怎么办?

文章来源&#xff1a;鲜枣课堂(ID&#xff1a;xzclasscom)作为也算是懂点技术的半专业人士&#xff0c;我们放假回去&#xff0c;遇到亲友&#xff0c;很可能被问到这样的问题——“我的手机(电脑)上网经常掉线&#xff0c;是为什么&#xff1f;”“我的手机(电脑)上网总是很慢…

sql 中位数_【PL/SQL 自定义函数】 常用场景

看完这章后你会学习到以下内容&#xff1a;1.练习场景2.面试场景3.工作应用场景总览思维导图&#xff1a;面试部分&#xff1a;1.创建函数,从emp表中查询指定员工编号的职工的工资CREATE OR REPLACE FUNCTION CHECK_SAL(F_EMPNO IN EMP.EMPNO%TYPE) RETURN NUMBER ISV_SAL VARC…

swift date 计算差_[Swift 设计模式] 适配器

更多内容&#xff0c;欢迎关注公众号&#xff1a;Swift花园喜欢文章&#xff1f;不如来个 ➕三连&#xff1f;关注专栏&#xff0c;关注我 将一个不兼容的对象转换成目标接口或者类&#xff0c;这是适配器模式的作用。下面这件东西是适配器模式在现实世界中最贴切的表达。 USB-…

委外订单_听听晚报-英特尔扩大芯片委外订单、苹果秋季或举行两场发布会

英特尔扩大芯片委外订单据外媒报道称&#xff0c;美国半导体厂商英特尔已与中国台湾芯片制造厂商台积电达成协议&#xff0c;明年开始采用后者7nm的优化版本6nm制程量产处理器或显卡&#xff0c;预估投片量将达到18万片。该消息发出后&#xff0c;资本市场反应剧烈&#xff0c;…

打开另外一个页面_如何在PDF页面中插入图片?

如何给PDF添加图片&#xff1f;有些时候为了丰富PDF的文档内容&#xff0c;需要添加一些图片&#xff0c;相比Word或PPT文档可以直接插入图片&#xff0c;而PDF的操作很多人可能并不熟悉&#xff0c;下面一起来看看如何在PDF文档中插入图片。关于PDF文档插入图片分为两种情况&a…

spring boot mybatis 整合_MyBatis学习:MyBatis和Spring整合

1. 整合的工程结构首先我们来看下整合之后的工程结构是什么样的。2. 配置文件在于spring整合之前&#xff0c;mybatis都是自己管理数据源的&#xff0c;然后sqlSessionFactory是我们自己去注入的&#xff0c;现在整合了&#xff0c;这些都要交给spring来管理了&#xff0c;来看…

python中缩进_python编程中的缩进是什么意思

Python最具特色的是用缩进来标明成块的代码。我下面以if选择结构来举例。if后面跟随条件&#xff0c;如果条件成立&#xff0c;则执行归属于if的一个代码块。 下面对比C语言来看一下if ( i > 0 ) { x 1; y 2; } 如果i > 0的话&#xff0c;我们将进行括号中所包括的两个…

返回后的数据处理_【掘金使用技巧2】掘金返回数据中时间的处理方法

掘金输出的时间数据处理方法掘金在为使用者提供数据时&#xff0c;有一类数据处理起来有些麻烦&#xff0c;这类数据就是时间数据。它们长这样&#xff1a;或者这样&#xff1a;查看一下它们的类型&#xff0c;发现有datetime,datetime64,Timestamp等等。这么多各种各样的类型&…

springboot jwt token前后端分离_为什么要 前后端分离 ?

作 者&#xff1a;互扯程序来 源&#xff1a;互扯程序广而告之&#xff1a;由于此订阅号换了个皮肤&#xff0c;系统自动取消了读者的公众号置顶。导致用户接受文章不及时。您可以打开订阅号&#xff0c;选择置顶(星标)公众号&#xff0c;重磅干货&#xff0c;第一时间送达&…

分计算iv值_一文读懂评分卡的IV、KS、AUC、GINI指标

前言&#xff1a;当一张评分卡构建完成时&#xff0c;筛选出一组特征生成了分数&#xff0c;我们会想要知道这个分数是否靠谱&#xff0c;即是否可以依赖这个分数将好坏客户区分开来&#xff0c;这个时候就需要评判评分卡有效性的指标。测量评分卡好坏区分能力的指标有许多&…

linux 查找文件夹_用python打造一个基于socket的文件(夹)传输系统

这段时间在学习python&#xff0c;接触到了网络编程中的socket这块&#xff0c;加上自己在用的Linux服务器都是原生支持python的&#xff0c;于是乎有了个做文件传输功能程序的想法。毕竟python语言中&#xff0c;有下载功能的框架一抓一大把&#xff0c;但是主机与主机间快速搭…

mysql gtid 备份恢复_MySQL基于gtid特性与xtrabackup的数据恢复

一、gtid特性介绍&#xff1a;GTID(global transaction identifier)是MySQL 5.6的新特性&#xff0c;可以唯一的标识一个事务&#xff0c;由UUIDTID组成&#xff1a;UUID是MySQL实例的唯一标识TID是该实例上已提交的事务的数量在主从复制中&#xff0c;GTID代替了classic的复制…

编码gbk的不可映射字符_Python基础:编码表和字符的故事

在计算机内部&#xff0c;都是每8位组成的一个个字节&#xff0c;比如我们使用"abc".encode()把abc转化成二进制byte类型&#xff0c;注意byte是不可变类型&#xff1a; 编码过程>>> abc.encode() # 把str字符变为bytes字节类型&#xff1b;字符是一个个连接…

mysql 中文字段名_MySQL全文索引怎么做?| 教程分享

- 点击上方“爱数据学习社”关注我们吧&#xff01; -文末领取【商业分析资料】为什么要用全文索引我们在用一个东西前&#xff0c;得知道为什么要用它&#xff0c;使用全文索引无非有以下原因&#xff1a;like查询太慢、json字段查询太慢(车太慢了)没时间引入ElasticSearch、S…

leftjoin多个on条件_MYSQL|为什么LEFT JOIN会这么慢?

之前谈了怎样后台导出SAP序时账&#xff0c;因为导出的序时账数据量较大(3家主体公司&#xff0c;2017-2020年的数据)&#xff0c;用了数据库MYSQL中的LEFT JOIN 来处理连接多表汇总数据&#xff0c;查询太慢啦&#xff0c;后来沦落到用手工分年来汇总数据&#xff0c;然后再导…

python 画布包括不了全部组件?_试验程序:画布版九键琴

近期有读者询问如何制作出滑动琴键连续发出声音的程序&#xff0c;他尝试用一排按钮充当琴键&#xff0c;但每次滑动只能触发一个按钮的点击事件&#xff0c;因此也只能发出一个声音。我提示他用画布替代按钮&#xff0c;他希望给予更具体的提示&#xff0c;于是我索性自己做了…

c 子类对象 访问父类对象受保护成员_06-JavaSe面向对象

一.static1.它是一种修饰符2.使用位置&#xff1a;它用来修饰成员变量和成员方法static修饰成员变量&#xff0c;叫类变量&#xff1b;static修饰成员方法&#xff0c;叫类方法&#xff1b;类成员类变量类方法没有使用static修饰成员变量&#xff0c;叫实例变量&#xff1b;没有…

c++ stack 遍历_C/C++内存分配!

一、预备知识—程序的内存分配一个由c/C编译的程序占用的内存分为以下几个部分1、栈区&#xff08;stack&#xff09;— 由编译器自动分配释放 &#xff0c;存放函数的参数值&#xff0c;局部变量的值等。其操作方式类似于数据结构中的栈。2、堆区&#xff08;heap&#xff09;…