Skip to content

Commit

Permalink
Color wheel (#247)
Browse files Browse the repository at this point in the history
* Add support for top and bottom sheet

* Color side sheet

* Paint color wheel

* Cache wheel

* Shade square painter

* Refactor hue wheel

* Should repaint

* Proper square size

* Position hue indicator

* Position shade indicator

* Shade pointer handler

* Remove unused code

* Update indicator on drag

* Indicator shouldnt consume pointer

* Hue pointer handler

* Keep state in hsv

* Persist color

* Apply color on tools

* Color comparer

* Full width side sheet

* Correct side sheet calculations

* Palette

* Border

* Landscape layout

* Fill cells

* Remove on longpress

* Generate start palette

* Refill empty cell on longpress

* Persist palette

* Fix picker when changing orientation

* Restore palette scrollposition

* Handle comparer tap

* iPhone safe padding

* Fix safe padding in other sheets

* Show system bar on iOS

* Remove commented code

* Repaint boundary around gradients

* Version and notes
  • Loading branch information
ruskakimov authored Nov 8, 2021
1 parent d0b6596 commit 9278fb9
Show file tree
Hide file tree
Showing 12 changed files with 702 additions and 208 deletions.
6 changes: 3 additions & 3 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ PODS:
- GoogleUtilities/Reachability (~> 7.4)
- GoogleUtilities/UserDefaults (~> 7.4)
- Flutter (1.0.0)
- flutter_ffmpeg/full-gpl (0.4.0):
- flutter_ffmpeg/full-gpl (0.4.2):
- Flutter
- mobile-ffmpeg-full-gpl (= 4.4)
- gallery_saver (0.0.1):
Expand Down Expand Up @@ -273,8 +273,8 @@ SPEC CHECKSUMS:
FirebaseCrashlytics: 69cddb6bfa7656c5346e603bc85b029392252ee6
FirebaseInstallations: 7f31798a8198c354eadcb87176d2090b62edc187
FirebaseMessaging: 1a33b4af3c8042ed6ddacb6c031894af2064bfab
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
flutter_ffmpeg: bc8496ea20331e486cd29a03a3c6ff10d32db565
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_ffmpeg: 3ec3912c74649851e6c3b1885511eef78142ac4f
gallery_saver: 9fc173c9f4fcc48af53b2a9ebea1b643255be542
GoogleAppMeasurement: c6bbc9753d046b5456dd4f940057fbad2c28419e
GoogleDataTransport: 04c3e9a480bbcaa2ec3f5d27f1cdeb6a92f20c8d
Expand Down
18 changes: 18 additions & 0 deletions lib/common/data/extensions/color_methods.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'dart:ui';

import 'package:flutter/material.dart';

extension ColorMethods on Color {
int toRGBA() {
return [
Expand All @@ -19,3 +21,19 @@ extension ColorMethods on Color {
].reduce((color, channel) => (color << 8) | channel);
}
}

extension HSVMethods on HSVColor {
String toStr() => '$alpha,$hue,$saturation,$value';
}

extension HSVParsing on String {
HSVColor parseHSVColor() {
final parts = this.split(',').map((part) => double.parse(part)).toList();
return HSVColor.fromAHSV(
parts[0],
parts[1],
parts[2],
parts[3],
);
}
}
69 changes: 58 additions & 11 deletions lib/common/ui/open_side_sheet.dart
Original file line number Diff line number Diff line change
@@ -1,44 +1,91 @@
import 'dart:math';
import 'package:flutter/material.dart';

enum Side {
left,
top,
right,
bottom,
}

const sideAlignments = [
Alignment.centerLeft,
Alignment.topCenter,
Alignment.centerRight,
Alignment.bottomCenter,
];

const hiddenOffset = [
Offset(-1, 0),
Offset(0, -1),
Offset(1, 0),
Offset(0, 1),
];

openSideSheet({
required BuildContext context,
required Widget Function(BuildContext) builder,
bool rightSide = true,
Side portraitSide = Side.right,
Side landscapeSide = Side.right,
double maxExtent = 320,
Duration transitionDuration = const Duration(milliseconds: 250),
}) {
showGeneralDialog(
barrierLabel: "Sheet Barrier",
barrierDismissible: true,
barrierColor: Colors.black.withOpacity(0.5),
context: context,
pageBuilder: (context, animation1, animation2) {
final sheetWidth = min(320.0, MediaQuery.of(context).size.width - 56);
final side = MediaQuery.of(context).orientation == Orientation.portrait
? portraitSide
: landscapeSide;

final horizontalSide = side.index % 2 == 0;
final screenEstate = horizontalSide
? MediaQuery.of(context).size.width
: MediaQuery.of(context).size.height;

final safePadding = MediaQuery.of(context).padding;
final safeSidePadding = rightSide ? safePadding.right : safePadding.left;
final sideSafePadding = [
safePadding.left,
safePadding.top,
safePadding.right,
safePadding.bottom
][side.index];

final contentEstate = horizontalSide
? screenEstate - safePadding.horizontal
: screenEstate - safePadding.vertical;

final sheetExtent = min(maxExtent, contentEstate - 56) + sideSafePadding;

return Align(
alignment: (rightSide ? Alignment.centerRight : Alignment.centerLeft),
alignment: sideAlignments[side.index],
child: SizedBox(
height: double.infinity,
width: sheetWidth + safeSidePadding,
height: horizontalSide ? double.infinity : sheetExtent,
width: !horizontalSide ? double.infinity : sheetExtent,
child: Material(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(
left: !rightSide,
right: rightSide,
bottom: false,
left: side == Side.left,
top: true,
right: side == Side.right,
bottom: side == Side.bottom,
child: builder(context),
),
),
),
);
},
transitionDuration: Duration(milliseconds: 250),
transitionDuration: transitionDuration,
transitionBuilder: (context, animation1, animation2, child) {
final side = MediaQuery.of(context).orientation == Orientation.portrait
? portraitSide
: landscapeSide;

return SlideTransition(
position: Tween(
begin: Offset((rightSide ? 1 : -1), 0),
begin: hiddenOffset[side.index],
end: Offset(0, 0),
).chain(CurveTween(curve: Curves.ease)).animate(animation1),
child: child,
Expand Down
53 changes: 53 additions & 0 deletions lib/common/ui/orientation_listener.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';

class OrientationListener extends StatefulWidget {
OrientationListener({
Key? key,
required this.onOrientationChanged,
required this.child,
}) : super(key: key);

final ValueChanged<Orientation> onOrientationChanged;
final Widget child;

@override
_OrientationListenerState createState() => _OrientationListenerState();
}

class _OrientationListenerState extends State<OrientationListener>
with WidgetsBindingObserver {
late Orientation _orientation;

Orientation get currentOrientation {
final size = WidgetsBinding.instance!.window.physicalSize;
return size.width > size.height
? Orientation.landscape
: Orientation.portrait;
}

@override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
_orientation = currentOrientation;
}

@override
void dispose() {
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}

@override
void didChangeMetrics() {
if (currentOrientation != _orientation) {
_orientation = currentOrientation;
widget.onOrientationChanged(_orientation);
}
}

@override
Widget build(BuildContext context) {
return widget.child;
}
}
124 changes: 118 additions & 6 deletions lib/drawing/data/toolbox/toolbox_model.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,46 @@
import 'package:flutter/material.dart';
import 'package:mooltik/common/data/extensions/color_methods.dart';
import 'package:mooltik/drawing/data/toolbox/tools/bucket.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'tools/tools.dart';

class ToolboxModel extends ChangeNotifier {
ToolboxModel(SharedPreferences sharedPreferences)
: _bucket = Bucket(sharedPreferences),
: _preferences = sharedPreferences,
_bucket = Bucket(sharedPreferences),
_paintBrush = PaintBrush(sharedPreferences),
_eraser = Eraser(sharedPreferences),
_lasso = Lasso(sharedPreferences) {
_lasso = Lasso(sharedPreferences),
_hsvColor = _restoreHSVColor(sharedPreferences),
_palette = _restorePalette(sharedPreferences) {
_selectedTool = _paintBrush;
_applyColorOnTools();
}

final SharedPreferences _preferences;

Color get color => _hsvColor.toColor();
HSVColor get hsvColor => _hsvColor;
HSVColor _hsvColor;

List<HSVColor?> get palette => _palette;
final List<HSVColor?> _palette;
double paletteScollOffset = 0; // For restoring scroll offset on reopening.

void setPaletteCell(int cellIndex, HSVColor? color) {
_palette[cellIndex] = color;
notifyListeners();
_savePalette();
}

List<Tool> get tools => [
bucket,
paintBrush,
eraser,
lasso,
];

Bucket get bucket => _bucket;
final Bucket _bucket;

Expand All @@ -33,10 +61,94 @@ class ToolboxModel extends ChangeNotifier {
notifyListeners();
}

void changeToolColor(Color color) {
if (selectedTool is ToolWithColor) {
(selectedTool as ToolWithColor).color = color;
notifyListeners();
void changeColor(HSVColor hsvColor) {
_hsvColor = hsvColor;
_applyColorOnTools();
notifyListeners();
_preferences.setString(_hsvColorKey, _hsvColor.toStr());
}

void _applyColorOnTools() {
for (final tool in tools) {
if (tool is ToolWithColor) tool.applyColor(color);
}
}

static HSVColor _restoreHSVColor(SharedPreferences sharedPreferences) {
if (sharedPreferences.containsKey(_hsvColorKey)) {
final raw = sharedPreferences.getString(_hsvColorKey);
if (raw != null) return raw.parseHSVColor();
}
return _startHsvColor;
}

static List<HSVColor?> _restorePalette(SharedPreferences sharedPreferences) {
if (sharedPreferences.containsKey(_hsvPaletteKey)) {
final raws = sharedPreferences.getStringList(_hsvPaletteKey);
if (raws != null && raws.length == _paletteCellCount) {
return raws
.map((str) => str != '' ? str.parseHSVColor() : null)
.toList();
}
}
return _generateStartPalette();
}

void _savePalette() {
_preferences.setStringList(
_hsvPaletteKey,
_palette.map((color) => color != null ? color.toStr() : '').toList(),
);
}

static List<HSVColor?> _generateStartPalette() {
final List<HSVColor?> colors = List.filled(_paletteCellCount, null);
int i = _paletteCellCount - 1;
for (var color in _materialColors.reversed) {
for (var shade in _shades.reversed) {
colors[i] = HSVColor.fromColor(color[shade]!);
i--;
}
}
return colors;
}

static const HSVColor _startHsvColor = HSVColor.fromAHSV(1, 0, 0, 0);
static const int _paletteCellCount = 90;

static const String _hsvColorKey = 'hsv_color';
static const String _hsvPaletteKey = 'hsv_palette';
}

const _customGreyMaterial = MaterialColor(
0xFF9E9E9E,
<int, Color>{
100: Colors.white,
200: Color(0xFFEEEEEE),
300: Color(0xFFE0E0E0),
400: Color(0xFFBDBDBD),
500: Color(0xFF9E9E9E),
600: Color(0xFF757575),
700: Color(0xFF616161),
800: Color(0xFF424242),
900: Colors.black,
},
);

const _materialColors = <MaterialColor>[
_customGreyMaterial,
Colors.blueGrey,
Colors.brown,
Colors.orange,
Colors.yellow,
Colors.lime,
Colors.green,
Colors.teal,
Colors.blue,
Colors.indigo,
Colors.purple,
Colors.pink,
Colors.red,
];

const _shades = [100, 300, 400, 500, 700, 900];
19 changes: 3 additions & 16 deletions lib/drawing/data/toolbox/tools/tool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,12 @@ abstract class Tool {
}

abstract class ToolWithColor extends Tool {
ToolWithColor(SharedPreferences sharedPreferences)
: super(sharedPreferences) {
// Restore selected color.
if (sharedPreferences.containsKey(_colorKey)) {
_color = Color(sharedPreferences.getInt(_colorKey)!);
}
}
ToolWithColor(SharedPreferences sharedPreferences) : super(sharedPreferences);

/// Tool color.
Color get color => _color;
Color _color = Colors.black;
set color(Color color) {

void applyColor(Color color) {
_color = color;
sharedPreferences.setInt(_colorKey, _color.value);
}

// ========================
// Shared preferences keys:
// ========================

String get _colorKey => name + '_color';
}
Loading

0 comments on commit 9278fb9

Please sign in to comment.