173 lines
5.2 KiB
Dart
173 lines
5.2 KiB
Dart
// Author: fruit.com.cn
|
|
// 职责:推荐流与播放页共用的简介样式、固定切换入口与高度过渡。
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../../../core/theme/app_colors.dart';
|
|
import '../../../l10n/app_localizations.dart';
|
|
|
|
const double _synopsisFont = 15;
|
|
const double _synopsisLineHeight = 1.4;
|
|
const int _collapsedSynopsisLines = 2;
|
|
const double _synopsisToggleWidthFraction = 0.55;
|
|
const double _synopsisToggleGap = 6;
|
|
const _synopsisMotion = Duration(milliseconds: 240);
|
|
const _synopsisStyle = TextStyle(
|
|
fontSize: _synopsisFont,
|
|
fontWeight: FontWeight.w400,
|
|
height: _synopsisLineHeight,
|
|
);
|
|
const _synopsisToggleStyle = TextStyle(
|
|
color: AppColors.textPrimary,
|
|
fontSize: _synopsisFont,
|
|
fontWeight: FontWeight.w400,
|
|
height: _synopsisLineHeight,
|
|
);
|
|
|
|
/// 两行折叠的简介,展开与收起入口在右下共用同一占位。
|
|
class ExpandableSynopsis extends StatefulWidget {
|
|
/// [keyPrefix] 区分页内标识,[expandedFooter] 仅在展开时展示。
|
|
const ExpandableSynopsis({
|
|
super.key,
|
|
required this.synopsis,
|
|
this.keyPrefix = 'home-feed',
|
|
this.expandedFooter,
|
|
this.textColor = AppColors.homeFeedVideoCopy,
|
|
});
|
|
|
|
/// 按当前语言获取的简介正文。
|
|
final String synopsis;
|
|
|
|
/// 当前页面的组件标识前缀。
|
|
final String keyPrefix;
|
|
|
|
/// 展开后附加的声明等内容。
|
|
final Widget? expandedFooter;
|
|
|
|
/// 简介正文颜色;默认使用视频信息区统一的半透明浅灰白。
|
|
final Color textColor;
|
|
|
|
@override
|
|
State<ExpandableSynopsis> createState() => _ExpandableSynopsisState();
|
|
}
|
|
|
|
class _ExpandableSynopsisState extends State<ExpandableSynopsis> {
|
|
bool _expanded = false;
|
|
|
|
@override
|
|
void didUpdateWidget(covariant ExpandableSynopsis oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.synopsis != widget.synopsis) _expanded = false;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.synopsis.trim().isEmpty) return const SizedBox.shrink();
|
|
final l10n = AppLocalizations.of(context)!;
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
if (!_needsToggle(constraints.maxWidth)) return _body();
|
|
return Row(
|
|
key: _key('layout'),
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: _toggle,
|
|
child: _animatedBody(),
|
|
),
|
|
),
|
|
const SizedBox(width: _synopsisToggleGap),
|
|
ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxWidth: constraints.maxWidth * _synopsisToggleWidthFraction,
|
|
),
|
|
child: IntrinsicWidth(child: _toggleButton(l10n)),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
bool _needsToggle(double maxWidth) {
|
|
final painter = _painter(widget.synopsis, _bodyStyle);
|
|
painter.layout(maxWidth: maxWidth);
|
|
final overflows = painter.didExceedMaxLines;
|
|
painter.dispose();
|
|
return overflows;
|
|
}
|
|
|
|
TextPainter _painter(String text, TextStyle style) => TextPainter(
|
|
text: TextSpan(
|
|
text: text,
|
|
style: DefaultTextStyle.of(context).style.merge(style),
|
|
),
|
|
maxLines: _collapsedSynopsisLines,
|
|
textDirection: Directionality.of(context),
|
|
textScaler: MediaQuery.textScalerOf(context),
|
|
locale: Localizations.localeOf(context),
|
|
textHeightBehavior: DefaultTextStyle.of(context).textHeightBehavior,
|
|
);
|
|
|
|
Widget _animatedBody() {
|
|
if (MediaQuery.disableAnimationsOf(context)) return _body();
|
|
return AnimatedSize(
|
|
key: _key('size'),
|
|
duration: _synopsisMotion,
|
|
curve: Curves.easeInOutCubic,
|
|
alignment: Alignment.topLeft,
|
|
child: _body(),
|
|
);
|
|
}
|
|
|
|
Key _key(String suffix) => Key('${widget.keyPrefix}-synopsis-$suffix');
|
|
|
|
TextStyle get _bodyStyle => _synopsisStyle.copyWith(color: widget.textColor);
|
|
|
|
Widget _body() {
|
|
final style = _bodyStyle;
|
|
final text = Text.rich(
|
|
TextSpan(text: widget.synopsis, style: style),
|
|
key: _expanded ? _key('expanded') : Key('${widget.keyPrefix}-synopsis'),
|
|
style: style,
|
|
maxLines: _expanded ? null : _collapsedSynopsisLines,
|
|
overflow: _expanded ? TextOverflow.clip : TextOverflow.ellipsis,
|
|
);
|
|
if (!_expanded || widget.expandedFooter == null) return text;
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [text, widget.expandedFooter!],
|
|
);
|
|
}
|
|
|
|
Widget _toggleButton(AppLocalizations l10n) => Semantics(
|
|
button: true,
|
|
expanded: _expanded,
|
|
child: GestureDetector(
|
|
key: _key('more'),
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: _toggle,
|
|
child: IndexedStack(
|
|
index: _expanded ? 1 : 0,
|
|
alignment: Alignment.bottomRight,
|
|
children: [
|
|
_toggleLabel(l10n.expandSynopsis),
|
|
_toggleLabel(l10n.showLess),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
Widget _toggleLabel(String label) => Text(
|
|
label,
|
|
maxLines: _collapsedSynopsisLines,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: TextAlign.right,
|
|
style: _synopsisToggleStyle,
|
|
);
|
|
|
|
void _toggle() => setState(() => _expanded = !_expanded);
|
|
}
|