You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

175 lines
6.0 KiB

import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../../../core/theme/app_palette.dart';
/// 转写 / 分析进行中的等待态。
///
/// 取代原来的 `assets/lottie/meeting_wait.json`(一张「JOIN」对话气泡插画)——
/// 那张图和本页在做的事(把音频变成文稿)没有任何关系,风格也与全站的
/// 222° 冷蓝灰不搭。现在改成**自绘**:上面一排声波表示音频正在被读,
/// 下面几条带流光的骨架行预告即将生成的文稿形状,两者共用同一个
/// [AnimationController]。
///
/// 自绘而不是再换一个 Lottie,是因为:① 不用为一个等待态再背一份 json 资源;
/// ② 颜色全部走 [AppPalette],明暗主题自动跟随,插画做不到;
/// ③ 四个 tab(概览/发言/全文/思维导图)共用同一个等待态,自绘好调。
class TranscribingPlaceholder extends StatefulWidget {
const TranscribingPlaceholder(this.title, {super.key});
/// 状态文案,已含省略号(`transcribing` / `analyzing`),这里不要再补点。
final String title;
@override
State<TranscribingPlaceholder> createState() =>
_TranscribingPlaceholderState();
}
class _TranscribingPlaceholderState extends State<TranscribingPlaceholder>
with SingleTickerProviderStateMixin {
late final AnimationController _c;
/// 骨架行的宽度占比,模拟一段自然段落的长短参差。
static const List<double> _lineWidths = <double>[1.0, 0.94, 0.82, 0.62];
/// 声波柱数。奇数,让中间那根成为视觉锚点。
static const int _barCount = 7;
@override
void initState() {
super.initState();
_c = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1600),
)..repeat();
}
@override
void dispose() {
_c.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final bool isDark = Theme.of(context).brightness == Brightness.dark;
return Center(
child: Padding(
// 往上抬一点:等待态居中偏上比死居中更像「内容马上要从这里长出来」
padding: EdgeInsets.only(bottom: 64.h, left: 24.w, right: 24.w),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_waveform(isDark),
SizedBox(height: 36.h),
_skeleton(isDark),
SizedBox(height: 32.h),
Text(
widget.title,
style: TextStyle(
fontSize: 15.sp,
fontWeight: FontWeight.w500,
color: AppPalette.textSecondary(isDark),
),
),
],
),
),
);
}
/// 声波:每根柱子给一个固定相位差,整排看起来像一道行进的波而不是随机跳动。
Widget _waveform(bool isDark) {
return SizedBox(
height: 40.h,
child: AnimatedBuilder(
animation: _c,
builder: (_, __) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: List<Widget>.generate(_barCount, (int i) {
final double phase = i / _barCount;
// 0..1 的正弦,取绝对值让波谷也留一点高度,柱子不会缩到看不见
final double v =
(math.sin((_c.value + phase) * 2 * math.pi) + 1) / 2;
return Padding(
padding: EdgeInsets.symmetric(horizontal: 3.w),
child: Container(
width: 4.w,
height: 10.h + 26.h * v,
decoration: BoxDecoration(
color: AppPalette.accent.withValues(alpha: 0.35 + 0.5 * v),
borderRadius: BorderRadius.circular(2.r),
),
),
);
}),
);
},
),
);
}
/// 骨架行 + 流光。流光用 [ShaderMask] 叠在实心色块上,
/// `srcATop` 保证亮带只出现在色块内部、不会溢到行与行之间的空隙。
Widget _skeleton(bool isDark) {
final Color base = AppPalette.border(isDark);
final Color highlight = isDark
? AppPalette.brand500.withValues(alpha: 0.85)
: Colors.white.withValues(alpha: 0.9);
return AnimatedBuilder(
animation: _c,
builder: (_, __) {
return ShaderMask(
blendMode: BlendMode.srcATop,
shaderCallback: (Rect rect) => LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: <Color>[
highlight.withValues(alpha: 0),
highlight,
highlight.withValues(alpha: 0),
],
stops: const <double>[0.30, 0.50, 0.70],
transform: _SlideGradient(_c.value * 2 - 1),
).createShader(rect),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
for (int i = 0; i < _lineWidths.length; i++) ...<Widget>[
if (i > 0) SizedBox(height: 14.h),
FractionallySizedBox(
widthFactor: _lineWidths[i],
child: Container(
height: 12.h,
decoration: BoxDecoration(
color: base,
borderRadius: BorderRadius.circular(6.r),
),
),
),
],
],
),
);
},
);
}
}
/// 把渐变横向平移 [dx](以自身宽度为单位)。
///
/// Flutter 只内置了 [GradientRotation],要让亮带扫过去只能自己写一个。
class _SlideGradient extends GradientTransform {
const _SlideGradient(this.dx);
final double dx;
@override
Matrix4 transform(Rect bounds, {TextDirection? textDirection}) =>
Matrix4.translationValues(bounds.width * dx, 0, 0);
}