Catch-all commit for outstanding pre-existing local changes. Mixes several themes that would normally be split: - Rename: UxPlugin → XPlugin across iOS, macOS, Android registrants. - New top-level packages under lib/src/: anim/ (animated values, panes, sheets, dock, measured), core/ (Emitter, ReactiveBuilder scaffolding, presenter/widget/value/dispose primitives), navi/ (Screen/ScreenStack/Router/hero/transitions), reactive/. - Edits across existing plugins (clipboard, crash, file, gallery, keyboard, scanner, sensor, url) to align with the new core. - Test updates and CHANGELOG/README touches accompanying the above.
162 lines
5.8 KiB
Dart
162 lines
5.8 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:ux/ux.dart';
|
|
|
|
void main() => runApp(MaterialApp(
|
|
theme: ThemeData(useMaterial3: true),
|
|
home: ChatScreen(),
|
|
));
|
|
|
|
/// Demonstrates XKeyboard in a chat UI:
|
|
/// - Frame-accurate keyboard height tracking (no Flutter viewInsets lag)
|
|
/// - Interactive dismiss (swipe the keyboard down like iMessage)
|
|
/// - Scroll freeze while the user is panning the keyboard
|
|
class ChatScreen extends StatefulWidget {
|
|
const ChatScreen({super.key});
|
|
|
|
@override
|
|
State<ChatScreen> createState() => _ChatScreenState();
|
|
}
|
|
|
|
class _ChatScreenState extends State<ChatScreen> {
|
|
final _keyboard = XKeyboard.instance;
|
|
final _textController = TextEditingController();
|
|
final _messages = List.generate(
|
|
30,
|
|
(i) => (text: 'Message ${30 - i}', isMe: i % 3 == 0),
|
|
);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// trackingInset: height of the input bar, so the pan-to-dismiss gesture
|
|
// activates when the finger enters the keyboard zone below the input bar.
|
|
_keyboard.enableInteractiveDismiss(trackingInset: 56);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_keyboard.disableInteractiveDismiss();
|
|
_textController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _send() {
|
|
final text = _textController.text.trim();
|
|
if (text.isEmpty) return;
|
|
setState(() => _messages.insert(0, (text: text, isMe: true)));
|
|
_textController.clear();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Disable Flutter's built-in resize — we handle it ourselves.
|
|
return Scaffold(
|
|
resizeToAvoidBottomInset: false,
|
|
appBar: AppBar(title: Text('XKeyboard Chat')),
|
|
// ListenableBuilder rebuilds only when XKeyboard notifies (height changes).
|
|
body: ListenableBuilder(
|
|
listenable: _keyboard,
|
|
builder: (context, _) {
|
|
final keyboardHeight = _keyboard.height;
|
|
final safeBottom = MediaQuery.viewPaddingOf(context).bottom;
|
|
final bottom = math.max(keyboardHeight, safeBottom);
|
|
|
|
return Column(
|
|
children: [
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onTap: () => FocusScope.of(context).unfocus(),
|
|
child: ListView.builder(
|
|
// Freeze scrolling while the user is panning the keyboard down.
|
|
// This prevents the list from bouncing during interactive dismiss.
|
|
reverse: true,
|
|
physics: _keyboard.isTracking
|
|
? NeverScrollableScrollPhysics()
|
|
: null,
|
|
padding: EdgeInsets.only(top: 16, bottom: 8),
|
|
itemCount: _messages.length,
|
|
itemBuilder: (context, i) {
|
|
final msg = _messages[i];
|
|
final isMe = msg.isMe;
|
|
return Padding(
|
|
padding:
|
|
EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
child: Align(
|
|
alignment: isMe
|
|
? Alignment.centerRight
|
|
: Alignment.centerLeft,
|
|
child: Container(
|
|
constraints: BoxConstraints(
|
|
maxWidth: MediaQuery.sizeOf(context).width * 0.75,
|
|
),
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: 14, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: isMe
|
|
? Theme.of(context).colorScheme.primaryContainer
|
|
: Theme.of(context)
|
|
.colorScheme
|
|
.surfaceContainerHighest,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Text(msg.text),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
// Input bar — sits directly above the keyboard.
|
|
Container(
|
|
padding: EdgeInsets.only(
|
|
left: 12,
|
|
right: 4,
|
|
top: 8,
|
|
bottom: 8 + bottom,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).colorScheme.surface,
|
|
border: Border(
|
|
top: BorderSide(
|
|
color: Theme.of(context).colorScheme.outlineVariant,
|
|
),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _textController,
|
|
textInputAction: TextInputAction.send,
|
|
onSubmitted: (_) => _send(),
|
|
decoration: InputDecoration(
|
|
hintText: 'Type a message...',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(24),
|
|
),
|
|
contentPadding: EdgeInsets.symmetric(
|
|
horizontal: 16, vertical: 10),
|
|
isDense: true,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 4),
|
|
IconButton(
|
|
icon: Icon(Icons.send),
|
|
color: Theme.of(context).colorScheme.primary,
|
|
onPressed: _send,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|