Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Created January 18, 2023 20:53
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save slightfoot/bf9c7ad68f2ac0718ce3a2ff167ec2e3 to your computer and use it in GitHub Desktop.
Save slightfoot/bf9c7ad68f2ac0718ce3a2ff167ec2e3 to your computer and use it in GitHub Desktop.
Resizey Profile Avatar Thing for #HumpDayQandA - 18/01/2023 - by Simon Lightfoot
// MIT License
//
// Copyright (c) 2023 Simon Lightfoot
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class ProfileImageData {
const ProfileImageData(this.assetPath, this.transform);
final String assetPath;
final Matrix4 transform;
}
void main() {
runApp(
AvatarApp(
profileImageData: ProfileImageData(
'assets/profile.png',
Matrix4.identity(),
),
),
);
}
@immutable
class AvatarApp extends StatefulWidget {
const AvatarApp({
super.key,
required this.profileImageData,
});
static AvatarAppState of(BuildContext context) {
return context.findAncestorStateOfType<AvatarAppState>()!;
}
final ProfileImageData profileImageData;
@override
State<AvatarApp> createState() => AvatarAppState();
}
class AvatarAppState extends State<AvatarApp> {
late final ValueNotifier<ProfileImageData> profileImageData;
@override
void initState() {
super.initState();
profileImageData = ValueNotifier<ProfileImageData>(widget.profileImageData);
}
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: ProfileScreen(),
);
}
}
@immutable
class ProfileScreen extends StatelessWidget {
const ProfileScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: const [
const SizedBox(height: 32.0),
Center(
child: SizedBox.square(
dimension: 96.0,
child: ProfileAvatarButton(),
),
),
const SizedBox(height: 8.0),
Text(
'Simon',
style: TextStyle(
fontSize: 28.0,
),
),
const SizedBox(height: 32.0),
],
),
);
}
}
@immutable
class ProfileAvatarButton extends StatelessWidget {
const ProfileAvatarButton({super.key});
@override
Widget build(BuildContext context) {
final avatarData = AvatarApp.of(context).profileImageData;
return Stack(
children: [
ClipOval(
child: ValueListenableBuilder(
valueListenable: avatarData,
builder: (BuildContext context, ProfileImageData value, Widget? child) {
return Transform(
transform: value.transform,
child: Image.asset(value.assetPath),
);
},
),
),
Material(
type: MaterialType.transparency,
shape: const CircleBorder(
side: BorderSide(color: Colors.orange, width: 6.0),
),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
Navigator.of(context).push(ProfileAvatarPickerScreen.route());
},
child: const SizedBox.expand(),
),
),
],
);
}
}
@immutable
class ProfileAvatarPickerScreen extends StatefulWidget {
const ProfileAvatarPickerScreen({super.key});
static Route<void> route() {
return PageRouteBuilder(
pageBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return const ProfileAvatarPickerScreen();
},
transitionsBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
);
}
@override
State<ProfileAvatarPickerScreen> createState() => _ProfileAvatarPickerScreenState();
}
class _ProfileAvatarPickerScreenState extends State<ProfileAvatarPickerScreen> {
late String _assetPath;
late Matrix4 _transform;
late StateSetter _applyTransform;
@override
void initState() {
super.initState();
final avatarData = AvatarApp.of(context).profileImageData.value;
_assetPath = avatarData.assetPath;
_transform = avatarData.transform;
}
void _onPanUpdate(DragUpdateDetails details) {
_applyTransform(() {
final d = details.delta;
// print('pan: ${details.localPosition}');
_transform *= Matrix4.translationValues(d.dx, d.dy, 0.0);
});
}
void _onPointerSignal(PointerSignalEvent event) {
if (event is PointerScrollEvent) {
// print('scroll: ${event.localPosition}');
final zoomIn = (event.scrollDelta.dy < 0);
final zoomFactor = zoomIn ? 1.05 : 0.95;
final pos = event.localPosition;
_applyTransform(() {
_transform *= (Matrix4.translationValues(pos.dx, pos.dy, 0.0) *
Matrix4.diagonal3Values(zoomFactor, zoomFactor, 1.0) *
Matrix4.translationValues(-pos.dx, -pos.dy, 0.0));
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(96.0),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Stack(
fit: StackFit.expand,
children: [
ClipOval(
child: FittedBox(
child: SizedBox.square(
dimension: 96.0,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanUpdate: _onPanUpdate,
child: Listener(
behavior: HitTestBehavior.opaque,
onPointerSignal: _onPointerSignal,
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
_applyTransform = setState;
return Transform(
transform: _transform,
child: Image.asset(
_assetPath,
fit: BoxFit.contain,
),
);
},
),
),
),
),
),
),
IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.orange, width: 6.0),
),
),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: ElevatedButton(
onPressed: () {
AvatarApp.of(context).profileImageData.value =
ProfileImageData(_assetPath, _transform);
Navigator.of(context).pop();
},
child: const Text('Save Changes'),
),
),
],
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment