更新
This commit is contained in:
@@ -20,23 +20,20 @@ class ClickableContainer extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(splashFactory: InkRipple.splashFactory),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
onTap: onTap,
|
||||
splashColor: highlightColor.withOpacity(0.2),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
),
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
onTap: onTap,
|
||||
splashColor: highlightColor.withOpacity(0.5),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,10 +21,151 @@ class TopSlideNotification extends StatefulWidget {
|
||||
this.duration = const Duration(seconds: 2),
|
||||
});
|
||||
|
||||
static OverlayEntry? _currentEntry; // 单例 OverlayEntry
|
||||
static bool _isShowing = false;
|
||||
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
String text = '操作成功!',
|
||||
double fontSize = 16,
|
||||
Color? textColor,
|
||||
double slideOffset = 300.0,
|
||||
Duration duration = const Duration(seconds: 2),
|
||||
}) {
|
||||
// 如果已有弹窗,先移除
|
||||
_removeCurrentEntry();
|
||||
|
||||
final overlay = Overlay.of(context);
|
||||
final entry = OverlayEntry(
|
||||
builder: (_) => TopSlideNotification(
|
||||
text: text,
|
||||
fontSize: fontSize,
|
||||
textColor: textColor,
|
||||
slideOffset: slideOffset,
|
||||
duration: duration,
|
||||
),
|
||||
);
|
||||
|
||||
_currentEntry = entry;
|
||||
_isShowing = true;
|
||||
overlay.insert(entry);
|
||||
|
||||
// 自动移除
|
||||
Future.delayed(duration + const Duration(milliseconds: 500), () {
|
||||
_removeCurrentEntry();
|
||||
});
|
||||
}
|
||||
|
||||
static void _removeCurrentEntry() {
|
||||
if (_currentEntry != null && _isShowing) {
|
||||
_currentEntry!.remove();
|
||||
_currentEntry = null;
|
||||
_isShowing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
State<TopSlideNotification> createState() => _TopSlideNotificationState();
|
||||
}
|
||||
|
||||
/// 工具方法:调用时直接加进 Overlay 上
|
||||
class _TopSlideNotificationState extends State<TopSlideNotification>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _animation;
|
||||
bool _isAnimating = false; // 标志位,控制是否正在动画中
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// 初始化 AnimationController
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
// 动画初始化完成后调用
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
_startAnimation(); // 调用动画启动方法
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
// 获取屏幕高度,用于计算动画的偏移值
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final offsetValue = widget.slideOffset! / screenHeight;
|
||||
|
||||
// 设置动画
|
||||
_animation =
|
||||
Tween<Offset>(begin: const Offset(0, -1), end: Offset(0, offsetValue))
|
||||
.animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
reverseCurve: Curves.easeIn,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// 确保动画控制器在组件销毁时被释放
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color get _textColor {
|
||||
return widget.textColor ?? Get.find<ThemeController>().currentColor.sc2;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SlideTransition(
|
||||
position: _animation,
|
||||
child: Material(
|
||||
color: stringToColor("#000000").withOpacity(0.8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
||||
child: Container(
|
||||
child: Text(
|
||||
widget.text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSize,
|
||||
color: _textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 执行动画
|
||||
Future<void> _startAnimation() async {
|
||||
if (_isAnimating) return; // 如果正在动画中,则不执行新的动画
|
||||
_isAnimating = true; // 标记动画开始
|
||||
|
||||
try {
|
||||
await _controller.forward();
|
||||
await Future.delayed(widget.duration);
|
||||
|
||||
// 只有在组件仍然挂载时才执行 reverse 动作
|
||||
if (mounted) {
|
||||
await _controller.reverse();
|
||||
}
|
||||
} finally {
|
||||
_isAnimating = false; // 动画完成后,标记动画结束
|
||||
}
|
||||
}
|
||||
|
||||
// 工具方法:调用时直接加进 Overlay 上
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
String text = '操作成功!',
|
||||
@@ -50,81 +191,3 @@ class TopSlideNotification extends StatefulWidget {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _TopSlideNotificationState extends State<TopSlideNotification>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Offset> _animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) async {
|
||||
await _controller.forward();
|
||||
await Future.delayed(widget.duration);
|
||||
await _controller.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final offsetValue = widget.slideOffset! / screenHeight;
|
||||
|
||||
_animation = Tween<Offset>(
|
||||
begin: const Offset(0, -1),
|
||||
end: Offset(0, offsetValue),
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeOut,
|
||||
reverseCurve: Curves.easeIn,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Color get _textColor {
|
||||
return widget.textColor ?? Get.find<ThemeController>().currentColor.sc2;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: SlideTransition(
|
||||
position: _animation,
|
||||
child: Material(
|
||||
color: stringToColor("#000000").withOpacity(0.8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0),
|
||||
child: Container(
|
||||
// color: Colors.red,
|
||||
child: Text(
|
||||
widget.text,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: widget.fontSize,
|
||||
color: _textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,79 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
// class WebViewWidget extends StatefulWidget {
|
||||
// final String url;
|
||||
// const WebViewWidget({Key? key, required this.url}) : super(key: key);
|
||||
class MyWebView extends StatefulWidget {
|
||||
final String url;
|
||||
final Function()? onLoad;
|
||||
final Function(MyWebView view, String msg)? onMessage;
|
||||
|
||||
// @override
|
||||
// _WebViewWidgetState createState() => _WebViewWidgetState();
|
||||
// }
|
||||
const MyWebView({
|
||||
Key? key,
|
||||
required this.url,
|
||||
this.onLoad,
|
||||
this.onMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
// class _WebViewWidgetState extends State<WebViewWidget> {
|
||||
// late WebViewController _webViewController;
|
||||
@override
|
||||
State<MyWebView> createState() => _MyWebViewState();
|
||||
}
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// // 初始化 WebView 控件
|
||||
// WebView.platform = SurfaceAndroidWebView();
|
||||
// }
|
||||
class _MyWebViewState extends State<MyWebView> {
|
||||
late final WebViewController _controller;
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(
|
||||
// title: Text('WebView'),
|
||||
// ),
|
||||
// body: WebView(
|
||||
// initialUrl: widget.url, // 设置要打开的网页地址
|
||||
// javascriptMode: JavascriptMode.unrestricted, // 启用 JavaScript
|
||||
// onWebViewCreated: (WebViewController webViewController) {
|
||||
// _webViewController = webViewController;
|
||||
// },
|
||||
// onPageStarted: (String url) {
|
||||
// print("页面开始加载:$url");
|
||||
// },
|
||||
// onPageFinished: (String url) {
|
||||
// print("页面加载完成:$url");
|
||||
// },
|
||||
// navigationDelegate: (NavigationRequest request) {
|
||||
// if (request.url.startsWith('https://www.google.com/')) {
|
||||
// print('拦截了URL请求: ${request.url}');
|
||||
// return NavigationDecision.prevent; // 拦截特定的请求
|
||||
// }
|
||||
// return NavigationDecision.navigate;
|
||||
// },
|
||||
// gestureNavigationEnabled: true, // 启用手势返回
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageFinished: (url) {
|
||||
widget.onLoad?.call();
|
||||
},
|
||||
onWebResourceError: (error) {
|
||||
print("WebView 加载错误: ${error.description}");
|
||||
},
|
||||
onNavigationRequest: (NavigationRequest request) {
|
||||
final url = request.url;
|
||||
if (url.startsWith('http') || url.startsWith('https')) {
|
||||
return NavigationDecision.navigate;
|
||||
}
|
||||
if (url.startsWith('weixin://')) {
|
||||
_launchWeChatUrl(url);
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
print('拦截未知协议: $url');
|
||||
return NavigationDecision.prevent;
|
||||
},
|
||||
),
|
||||
)
|
||||
..addJavaScriptChannel(
|
||||
'FlutterChannel',
|
||||
onMessageReceived: (msg) {
|
||||
widget.onMessage?.call(widget, msg.message);
|
||||
},
|
||||
)
|
||||
..loadRequest(Uri.parse(widget.url));
|
||||
}
|
||||
|
||||
void _launchWeChatUrl(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
print('⚠️ 无法跳转微信: $url');
|
||||
}
|
||||
}
|
||||
|
||||
// 提供方法给外部调用 JS
|
||||
void sendData(String data) {
|
||||
_controller.runJavaScript("window.postMessage('$data')");
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return WebViewWidget(controller: _controller);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user