├── test └── spannable_grid_test.dart ├── example ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcworkspace │ │ └── contents.xcworkspacedata │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ └── .gitignore ├── android │ ├── gradle.properties │ ├── .gitignore │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── spannablegrid │ │ │ │ │ │ └── example │ │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── build.gradle ├── .metadata ├── README.md ├── .gitignore ├── test │ └── widget_test.dart ├── pubspec.yaml ├── pubspec.lock └── lib │ └── main.dart ├── assets └── spannablegrid-001.gif ├── lib ├── spannable_grid.dart └── src │ ├── spannable_grid_empty_cell_view.dart │ ├── spannable_grid_cell_data.dart │ ├── spannable_grid_delegate.dart │ ├── spannable_grid_cell_view.dart │ ├── spannable_grid_options.dart │ └── spannable_grid.dart ├── .metadata ├── pubspec.yaml ├── LICENSE ├── CHANGELOG.md ├── .gitignore ├── pubspec.lock └── README.md /test/spannable_grid_test.dart: -------------------------------------------------------------------------------- 1 | 2 | void main() { 3 | } 4 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /assets/spannablegrid-001.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/assets/spannablegrid-001.gif -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/echedev/spannablegrid-flutter/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /lib/spannable_grid.dart: -------------------------------------------------------------------------------- 1 | /// [SpannableGrid] Flutter widget that allows it's cells to span columns and 2 | /// rows and supports moving cells withing the grid. 3 | library spannable_grid; 4 | 5 | export 'src/spannable_grid.dart'; 6 | export 'src/spannable_grid_cell_data.dart'; 7 | export 'src/spannable_grid_options.dart'; 8 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 27321ebbad34b0a3fafe99fac037102196d655ff 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: spannable_grid 2 | description: Custom grid widget that allows it's cells to span columns and rows and supports editing. 3 | version: 0.3.0 4 | homepage: https://github.com/ech89899/spannablegrid-flutter 5 | repository: https://github.com/ech89899/spannablegrid-flutter 6 | 7 | environment: 8 | sdk: '>=2.12.0 <3.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | flutter: 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/spannablegrid/example/com/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package spannablegrid.example.com.example 2 | 3 | import androidx.annotation.NonNull; 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 10 | GeneratedPluginRegistrant.registerWith(flutterEngine); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | SpannableGrid Demo 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Exceptions to above rules. 37 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 38 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Evgeny Cherkasov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/spannable_grid_empty_cell_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:spannable_grid/spannable_grid.dart'; 4 | 5 | import 'spannable_grid_cell_data.dart'; 6 | 7 | class SpannableGridEmptyCellView extends StatelessWidget { 8 | const SpannableGridEmptyCellView({ 9 | Key? key, 10 | required this.data, 11 | required this.style, 12 | required this.onWillAccept, 13 | required this.onAccept, 14 | this.content, 15 | this.isEditing = false, 16 | }) : super(key: key); 17 | 18 | final SpannableGridCellData data; 19 | 20 | final SpannableGridStyle style; 21 | 22 | final Function(SpannableGridCellData data) onWillAccept; 23 | 24 | final Function(SpannableGridCellData data) onAccept; 25 | 26 | final Widget? content; 27 | 28 | final bool isEditing; 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | final emptyCellView = content ?? 33 | Container( 34 | color: style.backgroundColor, 35 | ); 36 | return isEditing 37 | ? DragTarget( 38 | builder: (context, List candidateData, 39 | rejectedData) { 40 | return emptyCellView; 41 | }, 42 | onWillAccept: (data) => onWillAccept(data!), 43 | onAccept: (data) => onAccept(data), 44 | ) 45 | : emptyCellView; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.3.0] - *2022-01-18* 2 | 3 | **BREAKING!** 4 | * API of the SpannableGrid widget is changed (in part of styling and editing options) 5 | * Added support of immediate editing 6 | * Added optional restrictions of moving cells 7 | * Added more grid sizing options 8 | 9 | ## [0.2.1] - *2021-07-11* 10 | 11 | * Code clean up 12 | * Doc fixes 13 | 14 | ## [0.2.0] - *2021-04-18* 15 | 16 | * Migration to null safety. 17 | 18 | ## [0.1.4] - *2021-01-23* 19 | 20 | * Fix a bug where the widget didn't refresh with updated cells. 21 | * Fix a crash while dragging the cell. 22 | 23 | ## [0.1.3] - *2020-11-02* 24 | 25 | * Added `rowHeight` parameter, that allows to set height of grid rows explicitly. 26 | 27 | ## [0.1.2] - *2020-09-12* 28 | 29 | * Added `showGrid` parameter, that allows grid structure to be visible permanently, no only in the editing mode 30 | * Added `emptyCellView` parameter, that can be used to set custom view for empty cells 31 | 32 | ## [0.1.0] - *2020-03-01* 33 | 34 | * Added `editingOnLongPress` parameter, that controls if the editing mode is allowed 35 | 36 | ## [0.0.4] - *2020-01-24* 37 | 38 | * Added `editingGridColor` and `editingCellDecoration` parameters to customize the widget appearance in editing mode 39 | 40 | ## [0.0.3] - *2020-01-20* 41 | 42 | * Update README.md 43 | 44 | ## [0.0.1] - *2020-01-20* 45 | 46 | * **SpannableGrid** is a custom grid view that allows its cells to span 47 | columns and rows. The grid has a fixed size defined by number of columns 48 | and rows. The widget supports edit mode, in which user can move cells in 49 | the grid. 50 | -------------------------------------------------------------------------------- /lib/src/spannable_grid_cell_data.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | /// A metadata that defines an item (cell) of [SpannableGrid]. 4 | /// 5 | /// The item [id] is required and must be unique within the grid widget. 6 | /// Item is positioned to [column] and [row] withing the grid and span 7 | /// [columnSpan] and [rowSpan] cells. By default, the grid item occupies 8 | /// a single cell. 9 | /// The content of the cell is determined by the [child] widget. 10 | /// 11 | /// ```dart 12 | /// List cells = List(); 13 | /// cells.add(SpannableGridCellData( 14 | /// column: 1, 15 | /// row: 1, 16 | /// columnSpan: 2, 17 | /// rowSpan: 2, 18 | /// id: "Test Cell 1", 19 | /// child: Container( 20 | /// color: Colors.lime, 21 | /// child: Center( 22 | /// child: Text("Tile 2x2", 23 | /// style: Theme.of(context).textTheme.title, 24 | /// ), 25 | /// ), 26 | /// ), 27 | /// )); 28 | /// cells.add(SpannableGridCellData( 29 | /// column: 4, 30 | /// row: 1, 31 | /// id: "Test Cell 2", 32 | /// child: Container( 33 | /// color: Colors.lime, 34 | /// child: Center( 35 | /// child: Text("Tile 1x1", 36 | /// style: Theme.of(context).textTheme.title, 37 | /// ), 38 | /// ), 39 | /// ), 40 | /// )); 41 | /// ``` 42 | class SpannableGridCellData { 43 | SpannableGridCellData( 44 | {required this.id, 45 | this.child, 46 | required this.column, 47 | required this.row, 48 | this.columnSpan = 1, 49 | this.rowSpan = 1}); 50 | 51 | Object id; 52 | Widget? child; 53 | int column; 54 | int row; 55 | int columnSpan; 56 | int rowSpan; 57 | } 58 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/src/spannable_grid_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:spannable_grid/spannable_grid.dart'; 3 | 4 | import 'spannable_grid_cell_data.dart'; 5 | 6 | class SpannableGridDelegate extends MultiChildLayoutDelegate { 7 | SpannableGridDelegate({ 8 | required this.cells, 9 | required this.columns, 10 | required this.rows, 11 | required this.onCellSizeCalculated, 12 | // this.rowHeight, 13 | required this.spacing, 14 | this.gridSize = SpannableGridSize.parentWidth, 15 | }); 16 | 17 | final Map cells; 18 | 19 | final int columns; 20 | 21 | // final double? rowHeight; 22 | 23 | final int rows; 24 | 25 | final double spacing; 26 | 27 | final SpannableGridSize gridSize; 28 | 29 | final Function(Size size) onCellSizeCalculated; 30 | 31 | @override 32 | void performLayout(Size size) { 33 | final double cellHeight = size.height / rows; 34 | final double cellWidth = size.width / columns; 35 | onCellSizeCalculated(Size(cellWidth, cellHeight)); 36 | 37 | for (SpannableGridCellData cell in cells.values) { 38 | final childHeight = cell.rowSpan * cellHeight - spacing * 2; 39 | final childWidth = cell.columnSpan * cellWidth - spacing * 2; 40 | layoutChild( 41 | cell.id, 42 | BoxConstraints( 43 | minWidth: childWidth, 44 | maxWidth: childWidth, 45 | minHeight: childHeight, 46 | maxHeight: childHeight, 47 | )); 48 | positionChild( 49 | cell.id, 50 | Offset((cell.column - 1) * cellWidth + spacing, 51 | (cell.row - 1) * cellHeight + spacing)); 52 | } 53 | } 54 | 55 | @override 56 | bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => true; 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | build/ 32 | 33 | # Android related 34 | **/android/**/gradle-wrapper.jar 35 | **/android/.gradle 36 | **/android/captures/ 37 | **/android/gradlew 38 | **/android/gradlew.bat 39 | **/android/local.properties 40 | **/android/**/GeneratedPluginRegistrant.java 41 | 42 | # iOS/XCode related 43 | **/ios/**/*.mode1v3 44 | **/ios/**/*.mode2v3 45 | **/ios/**/*.moved-aside 46 | **/ios/**/*.pbxuser 47 | **/ios/**/*.perspectivev3 48 | **/ios/**/*sync/ 49 | **/ios/**/.sconsign.dblite 50 | **/ios/**/.tags* 51 | **/ios/**/.vagrant/ 52 | **/ios/**/DerivedData/ 53 | **/ios/**/Icon? 54 | **/ios/**/Pods/ 55 | **/ios/**/.symlinks/ 56 | **/ios/**/profile 57 | **/ios/**/xcuserdata 58 | **/ios/.generated/ 59 | **/ios/Flutter/App.framework 60 | **/ios/Flutter/Flutter.framework 61 | **/ios/Flutter/Flutter.podspec 62 | **/ios/Flutter/Generated.xcconfig 63 | **/ios/Flutter/app.flx 64 | **/ios/Flutter/app.zip 65 | **/ios/Flutter/flutter_assets/ 66 | **/ios/Flutter/flutter_export_environment.sh 67 | **/ios/ServiceDefinitions.json 68 | **/ios/Runner/GeneratedPluginRegistrant.* 69 | 70 | # Exceptions to above rules. 71 | !**/ios/**/default.mode1v3 72 | !**/ios/**/default.mode2v3 73 | !**/ios/**/default.pbxuser 74 | !**/ios/**/default.perspectivev3 75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 76 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "spannablegrid.example.com.example" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | } 55 | } 56 | } 57 | 58 | flutter { 59 | source '../..' 60 | } 61 | 62 | dependencies { 63 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 64 | testImplementation 'junit:junit:4.12' 65 | androidTestImplementation 'androidx.test:runner:1.1.1' 66 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 67 | } 68 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: SpannableGrid Demo 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 1.0.0+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | spannable_grid: 24 | path: ../ 25 | # The following adds the Cupertino Icons font to your application. 26 | # Use with the CupertinoIcons class for iOS style icons. 27 | cupertino_icons: ^0.1.2 28 | 29 | dev_dependencies: 30 | flutter_test: 31 | sdk: flutter 32 | 33 | 34 | # For information on the generic Dart part of this file, see the 35 | # following page: https://dart.dev/tools/pub/pubspec 36 | 37 | # The following section is specific to Flutter. 38 | flutter: 39 | 40 | # The following line ensures that the Material Icons font is 41 | # included with your application, so that you can use the icons in 42 | # the material Icons class. 43 | uses-material-design: true 44 | 45 | # To add assets to your application, add an assets section, like this: 46 | # assets: 47 | # - images/a_dot_burr.jpeg 48 | # - images/a_dot_ham.jpeg 49 | 50 | # An image asset can refer to one or more resolution-specific "variants", see 51 | # https://flutter.dev/assets-and-images/#resolution-aware. 52 | 53 | # For details regarding adding assets from package dependencies, see 54 | # https://flutter.dev/assets-and-images/#from-packages 55 | 56 | # To add custom fonts to your application, add a fonts section here, 57 | # in this "flutter" section. Each entry in this list should have a 58 | # "family" key with the font family name, and a "fonts" key with a 59 | # list giving the asset and other descriptors for the font. For 60 | # example: 61 | # fonts: 62 | # - family: Schyler 63 | # fonts: 64 | # - asset: fonts/Schyler-Regular.ttf 65 | # - asset: fonts/Schyler-Italic.ttf 66 | # style: italic 67 | # - family: Trajan Pro 68 | # fonts: 69 | # - asset: fonts/TrajanPro.ttf 70 | # - asset: fonts/TrajanPro_Bold.ttf 71 | # weight: 700 72 | # 73 | # For details regarding fonts from package dependencies, 74 | # see https://flutter.dev/custom-fonts/#from-packages 75 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/src/spannable_grid_cell_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | import 'spannable_grid_cell_data.dart'; 5 | import 'spannable_grid_options.dart'; 6 | 7 | class SpannableGridCellView extends StatelessWidget { 8 | const SpannableGridCellView({ 9 | Key? key, 10 | required this.data, 11 | required this.editingStrategy, 12 | required this.style, 13 | required this.isEditing, 14 | required this.isSelected, 15 | required this.canMove, 16 | required this.onDragStarted, 17 | required this.onEnterEditing, 18 | required this.onExitEditing, 19 | required this.size, 20 | }) : super(key: key); 21 | 22 | final SpannableGridCellData data; 23 | 24 | final SpannableGridEditingStrategy editingStrategy; 25 | 26 | final SpannableGridStyle style; 27 | 28 | final bool isEditing; 29 | 30 | final bool isSelected; 31 | 32 | final bool canMove; 33 | 34 | final Function(Offset localPosition) onDragStarted; 35 | 36 | final VoidCallback onEnterEditing; 37 | 38 | final VoidCallback onExitEditing; 39 | 40 | final Size size; 41 | 42 | @override 43 | Widget build(BuildContext context) { 44 | var result = data.child!; 45 | if (isEditing) { 46 | if (style.contentOpacity < 1.0) { 47 | result = Opacity( 48 | opacity: style.contentOpacity, 49 | child: result, 50 | ); 51 | } 52 | if (isSelected) { 53 | result = Stack( 54 | children: [ 55 | result, 56 | Container( 57 | decoration: style.selectedCellDecoration ?? 58 | BoxDecoration( 59 | border: Border.all( 60 | color: Theme.of(context).primaryColor, 61 | width: 4.0, 62 | ), 63 | ), 64 | ), 65 | ], 66 | ); 67 | if (editingStrategy.exitOnTap) { 68 | result = GestureDetector( 69 | onTap: onExitEditing, 70 | onTapDown: (details) => onDragStarted(details.localPosition), 71 | child: result, 72 | ); 73 | } 74 | result = Draggable( 75 | child: result, 76 | maxSimultaneousDrags: 1, 77 | feedback: SizedBox( 78 | width: size.width, 79 | height: size.height, 80 | child: result, 81 | ), 82 | childWhenDragging: const SizedBox.shrink(), 83 | data: data, 84 | onDraggableCanceled: (velocity, offset) { 85 | if (editingStrategy.immediate) { 86 | onExitEditing(); 87 | } 88 | }, 89 | onDragCompleted: () { 90 | if (editingStrategy.immediate) { 91 | onExitEditing(); 92 | } 93 | }, 94 | ); 95 | } 96 | } else { 97 | if (editingStrategy.allowed) { 98 | if (editingStrategy.enterOnLongTap) { 99 | result = GestureDetector( 100 | onLongPress: onEnterEditing, 101 | child: result, 102 | ); 103 | } else if (editingStrategy.immediate && canMove) { 104 | result = GestureDetector( 105 | onPanDown: (details) { 106 | onDragStarted(details.localPosition); 107 | onEnterEditing(); 108 | }, 109 | child: result, 110 | ); 111 | result = Draggable( 112 | child: result, 113 | maxSimultaneousDrags: 1, 114 | feedback: SizedBox( 115 | width: size.width, 116 | height: size.height, 117 | child: result, 118 | ), 119 | childWhenDragging: const SizedBox.shrink(), 120 | data: data, 121 | ); 122 | } 123 | } 124 | } 125 | return result; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.8.1" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.3.1" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | fake_async: 47 | dependency: transitive 48 | description: 49 | name: fake_async 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.2.0" 53 | flutter: 54 | dependency: "direct main" 55 | description: flutter 56 | source: sdk 57 | version: "0.0.0" 58 | flutter_test: 59 | dependency: "direct dev" 60 | description: flutter 61 | source: sdk 62 | version: "0.0.0" 63 | matcher: 64 | dependency: transitive 65 | description: 66 | name: matcher 67 | url: "https://pub.dartlang.org" 68 | source: hosted 69 | version: "0.12.10" 70 | meta: 71 | dependency: transitive 72 | description: 73 | name: meta 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "1.7.0" 77 | path: 78 | dependency: transitive 79 | description: 80 | name: path 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.8.0" 84 | sky_engine: 85 | dependency: transitive 86 | description: flutter 87 | source: sdk 88 | version: "0.0.99" 89 | source_span: 90 | dependency: transitive 91 | description: 92 | name: source_span 93 | url: "https://pub.dartlang.org" 94 | source: hosted 95 | version: "1.8.1" 96 | stack_trace: 97 | dependency: transitive 98 | description: 99 | name: stack_trace 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "1.10.0" 103 | stream_channel: 104 | dependency: transitive 105 | description: 106 | name: stream_channel 107 | url: "https://pub.dartlang.org" 108 | source: hosted 109 | version: "2.1.0" 110 | string_scanner: 111 | dependency: transitive 112 | description: 113 | name: string_scanner 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.1.0" 117 | term_glyph: 118 | dependency: transitive 119 | description: 120 | name: term_glyph 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "1.2.0" 124 | test_api: 125 | dependency: transitive 126 | description: 127 | name: test_api 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "0.4.2" 131 | typed_data: 132 | dependency: transitive 133 | description: 134 | name: typed_data 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.3.0" 138 | vector_math: 139 | dependency: transitive 140 | description: 141 | name: vector_math 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "2.1.0" 145 | sdks: 146 | dart: ">=2.12.0 <3.0.0" 147 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.8.1" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.1.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.3.1" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.15.0" 46 | cupertino_icons: 47 | dependency: "direct main" 48 | description: 49 | name: cupertino_icons 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "0.1.3" 53 | fake_async: 54 | dependency: transitive 55 | description: 56 | name: fake_async 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.2.0" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_test: 66 | dependency: "direct dev" 67 | description: flutter 68 | source: sdk 69 | version: "0.0.0" 70 | matcher: 71 | dependency: transitive 72 | description: 73 | name: matcher 74 | url: "https://pub.dartlang.org" 75 | source: hosted 76 | version: "0.12.10" 77 | meta: 78 | dependency: transitive 79 | description: 80 | name: meta 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "1.7.0" 84 | path: 85 | dependency: transitive 86 | description: 87 | name: path 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "1.8.0" 91 | sky_engine: 92 | dependency: transitive 93 | description: flutter 94 | source: sdk 95 | version: "0.0.99" 96 | source_span: 97 | dependency: transitive 98 | description: 99 | name: source_span 100 | url: "https://pub.dartlang.org" 101 | source: hosted 102 | version: "1.8.1" 103 | spannable_grid: 104 | dependency: "direct main" 105 | description: 106 | path: ".." 107 | relative: true 108 | source: path 109 | version: "0.2.1" 110 | stack_trace: 111 | dependency: transitive 112 | description: 113 | name: stack_trace 114 | url: "https://pub.dartlang.org" 115 | source: hosted 116 | version: "1.10.0" 117 | stream_channel: 118 | dependency: transitive 119 | description: 120 | name: stream_channel 121 | url: "https://pub.dartlang.org" 122 | source: hosted 123 | version: "2.1.0" 124 | string_scanner: 125 | dependency: transitive 126 | description: 127 | name: string_scanner 128 | url: "https://pub.dartlang.org" 129 | source: hosted 130 | version: "1.1.0" 131 | term_glyph: 132 | dependency: transitive 133 | description: 134 | name: term_glyph 135 | url: "https://pub.dartlang.org" 136 | source: hosted 137 | version: "1.2.0" 138 | test_api: 139 | dependency: transitive 140 | description: 141 | name: test_api 142 | url: "https://pub.dartlang.org" 143 | source: hosted 144 | version: "0.4.2" 145 | typed_data: 146 | dependency: transitive 147 | description: 148 | name: typed_data 149 | url: "https://pub.dartlang.org" 150 | source: hosted 151 | version: "1.3.0" 152 | vector_math: 153 | dependency: transitive 154 | description: 155 | name: vector_math 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "2.1.0" 159 | sdks: 160 | dart: ">=2.12.0 <3.0.0" 161 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:spannable_grid/spannable_grid.dart'; 3 | 4 | void main() => runApp(MyApp()); 5 | 6 | class MyApp extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return MaterialApp( 10 | title: 'SpannableGrid Demo', 11 | theme: ThemeData( 12 | primarySwatch: Colors.blueGrey, 13 | accentColor: Colors.amber, 14 | ), 15 | home: MyHomePage(title: 'SpannableGrid Demo'), 16 | ); 17 | } 18 | } 19 | 20 | class MyHomePage extends StatefulWidget { 21 | MyHomePage({Key key, this.title}) : super(key: key); 22 | 23 | final String title; 24 | 25 | @override 26 | _MyHomePageState createState() => _MyHomePageState(); 27 | } 28 | 29 | class _MyHomePageState extends State { 30 | bool _singleCell = false; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return Scaffold( 35 | appBar: AppBar( 36 | title: Text(widget.title), 37 | ), 38 | body: Center( 39 | child: Column( 40 | mainAxisAlignment: MainAxisAlignment.center, 41 | children: [ 42 | SpannableGrid( 43 | columns: 4, 44 | rows: 4, 45 | cells: _getCells(), 46 | onCellChanged: (cell) { print('Cell ${cell.id} changed'); }, 47 | ), 48 | Padding( 49 | padding: const EdgeInsets.all(8.0), 50 | child: TextButton( 51 | child: Text('Change'), 52 | onPressed: () { 53 | setState(() { 54 | _singleCell = !_singleCell; 55 | }); 56 | }, 57 | ), 58 | ), 59 | ], 60 | ), 61 | ), 62 | ); 63 | } 64 | 65 | List _getCells() { 66 | var result = []; 67 | if (_singleCell) { 68 | result.add(SpannableGridCellData( 69 | column: 1, 70 | row: 1, 71 | columnSpan: 4, 72 | rowSpan: 4, 73 | id: "Test Cell 1", 74 | child: Container( 75 | color: Colors.lime, 76 | child: Center( 77 | child: Text("Tile 4x4", 78 | style: Theme 79 | .of(context) 80 | .textTheme 81 | .headline6, 82 | ), 83 | ), 84 | ), 85 | )); 86 | } 87 | else { 88 | result.add(SpannableGridCellData( 89 | column: 1, 90 | row: 1, 91 | columnSpan: 2, 92 | rowSpan: 2, 93 | id: "Test Cell 1", 94 | child: Container( 95 | color: Colors.lime, 96 | child: Center( 97 | child: Text("Tile 2x2", 98 | style: Theme 99 | .of(context) 100 | .textTheme 101 | .headline6, 102 | ), 103 | ), 104 | ), 105 | )); 106 | result.add(SpannableGridCellData( 107 | column: 4, 108 | row: 1, 109 | columnSpan: 1, 110 | rowSpan: 1, 111 | id: "Test Cell 2", 112 | child: Container( 113 | color: Colors.lime, 114 | child: Center( 115 | child: Text("Tile 1x1", 116 | style: Theme 117 | .of(context) 118 | .textTheme 119 | .headline6, 120 | ), 121 | ), 122 | ), 123 | )); 124 | result.add(SpannableGridCellData( 125 | column: 1, 126 | row: 4, 127 | columnSpan: 3, 128 | rowSpan: 1, 129 | id: "Test Cell 3", 130 | child: Container( 131 | color: Colors.lightBlueAccent, 132 | child: Center( 133 | child: Text("Tile 3x1", 134 | style: Theme 135 | .of(context) 136 | .textTheme 137 | .headline6, 138 | ), 139 | ), 140 | ), 141 | )); 142 | result.add(SpannableGridCellData( 143 | column: 4, 144 | row: 3, 145 | columnSpan: 1, 146 | rowSpan: 2, 147 | id: "Test Cell 4", 148 | child: Container( 149 | color: Colors.lightBlueAccent, 150 | child: Center( 151 | child: Text("Tile 1x2", 152 | style: Theme 153 | .of(context) 154 | .textTheme 155 | .headline6, 156 | ), 157 | ), 158 | ), 159 | )); 160 | } 161 | return result; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spannable Grid 2 | pub version MIT License 3 | 4 | The **SpannableGrid** is a Flutter widget that allows its cells to span columns 5 | and rows and supports moving cells inside the grid. 6 | 7 | ![SpannableGrid Demo](./assets/spannablegrid-001.gif) 8 | 9 | ## Features 10 | 11 | - The widget is sized itself to fit its parent width and/or height 12 | - The number of columns and rows is fixed 13 | - Cells can span columns and rows 14 | - Supports editing mode, in which cells can be moved inside the grid to available places 15 | - Various editing strategies 16 | - Styling the grid and its cells 17 | 18 | ## Usage 19 | 20 | In the `dependencies:` section of your `pubspec.yaml`, add the following line: 21 | 22 | ```yaml 23 | dependencies: 24 | spannable_grid: ^0.3.0 25 | ``` 26 | 27 | Import the package 28 | 29 | ```dart 30 | import 'package:spannable_grid/spannable_grid.dart'; 31 | ``` 32 | 33 | #### Create grid items 34 | 35 | The `SpannableGrid` widget requires the list of `SpannableGridCellData` objects that define cells appearance. 36 | 37 | ```dart 38 | final cells = []; 39 | cells.add(SpannableGridCellData( 40 | column: 1, 41 | row: 1, 42 | columnSpan: 2, 43 | rowSpan: 2, 44 | id: "Test Cell 1", 45 | child: Container( 46 | color: Colors.lime, 47 | child: Center( 48 | child: Text("Tile 2x2", 49 | style: Theme.of(context).textTheme.title, 50 | ), 51 | ), 52 | ), 53 | )); 54 | cells.add(SpannableGridCellData( 55 | column: 4, 56 | row: 1, 57 | columnSpan: 1, 58 | rowSpan: 1, 59 | id: "Test Cell 2", 60 | child: Container( 61 | color: Colors.lime, 62 | child: Center( 63 | child: Text("Tile 1x1", 64 | style: Theme.of(context).textTheme.title, 65 | ), 66 | ), 67 | ), 68 | )); 69 | ``` 70 | 71 | #### Add SpannableGrid widget 72 | 73 | ```dart 74 | SpannableGrid( 75 | columns: 4, 76 | rows: 4, 77 | cells: cells, 78 | onCellChanged: (cell) { print('Cell ${cell.id} changed'); }, 79 | ), 80 | ``` 81 | 82 | #### Editing mode 83 | 84 | In the editing mode user can move cells to another empty place withing the grid. 85 | 86 | Use `editingStrategy` parameter to define the behaviour in the editing mode. 87 | It has following options: 88 | 89 | | Name | Description | 90 | |---|---| 91 | | `allowed` | User can move the cells | 92 | | `enterOnLongTap` | User should use a long tap on the cell to enter the editing mode | 93 | | `exitOnTap` | When finished moving the cell, user should tap the cell to exit the editing mode | 94 | | `immediate` | User can move cells immediately, just by starting dragging them | 95 | | `moveOnlyToNearby` | The cell can be moved only to nearby empty cells | 96 | 97 | User can enter editing mode by long press on the cell. 98 | 99 | In the editing mode the editing cell is highlighted, other cells are faded and the grid structure becomes visible. User can move editing cell to another available place inside the grid. 100 | 101 | The updated cell is returned in `onCellChanged` callback. 102 | 103 | #### Styling 104 | 105 | Use the `style` parameter to style the appearance of the grid and its cells. 106 | 107 | The following options are supported: 108 | 109 | | Name | Description | 110 | |---|---| 111 | | `backgroundColor` | It is used to display empty cells, when the `emptyCellView` parameter is not specified. | 112 | | `contentOpacity` | It is used in the editing mode to make the content cells transparent, so the underlying grid structure becomes visible. | 113 | | `selectedCellDecoration` | An additional decoration that is applied to the selected cell to highlight it in the editing mode. | 114 | | `spacing` | A space between gird cells. | 115 | 116 | When the `showGrid` parameter is `true` the grid's structure is always visible. Otherwise it appears only in the editing mode. 117 | 118 | #### Full example 119 | 120 | You can find demo app in the [example](https://github.com/ech89899/spannablegrid-flutter/tree/master/example) project. 121 | 122 | ## More 123 | 124 | #### Changelog 125 | 126 | Please check the [Changelog](CHANGELOG.md) page for the latest version and changes. 127 | 128 | #### License 129 | 130 | Author: Evgeny Cherkasov. 131 | 132 | This package is published under [MIT License](LICENSE). 133 | 134 | #### Contributions 135 | 136 | Feel free to contribute to this project. 137 | 138 | [Flutter](https://flutter.dev/docs) -------------------------------------------------------------------------------- /lib/src/spannable_grid_options.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | /// Determines behaviour in the editing mode. 5 | /// 6 | /// In the editing mode of the [SpannableGrid] user can move cell to available 7 | /// empty places. 8 | /// This class contains options that allows to customize behaviour in the editing mode. 9 | /// 10 | /// The default strategy allows editing cells, user can enter editing mode by long tap 11 | /// on the cell, can move the selected cell to any empty place, and should tap 12 | /// on the cell to exit editing mode. 13 | /// 14 | class SpannableGridEditingStrategy { 15 | /// Create a default editing strategy instance. 16 | /// 17 | const SpannableGridEditingStrategy({ 18 | this.allowed = true, 19 | this.enterOnLongTap = true, 20 | this.exitOnTap = true, 21 | this.immediate = false, 22 | this.moveOnlyToNearby = false, 23 | }); 24 | 25 | /// Create a strategy instance that disable editing. 26 | /// 27 | factory SpannableGridEditingStrategy.disabled() => 28 | const SpannableGridEditingStrategy( 29 | allowed: false, 30 | enterOnLongTap: false, 31 | exitOnTap: false, 32 | immediate: false, 33 | ); 34 | 35 | /// Create a strategy instance that allow immediate moving cells. 36 | /// 37 | factory SpannableGridEditingStrategy.immediate() => 38 | const SpannableGridEditingStrategy( 39 | allowed: true, 40 | enterOnLongTap: false, 41 | exitOnTap: false, 42 | immediate: true, 43 | ); 44 | 45 | /// Determines if the editing of cells is allowed. 46 | /// 47 | final bool allowed; 48 | 49 | /// Enter editing mode by long tap on the cell. 50 | /// 51 | final bool enterOnLongTap; 52 | 53 | /// Exit editing mode by tap on selected cell. 54 | /// 55 | final bool exitOnTap; 56 | 57 | /// Allows immediate editing. 58 | /// 59 | /// The editing mode is turned on/off automatically when user start/end dragging the cell, 60 | /// 61 | final bool immediate; 62 | 63 | /// Allow to move the cell only to nearby empty place. 64 | /// 65 | final bool moveOnlyToNearby; 66 | 67 | /// Create a copy of the editing strategy with some parameters changed. 68 | /// 69 | SpannableGridEditingStrategy copyWith({ 70 | bool? allowed, 71 | bool? enterOnLongTap, 72 | bool? exitOnTap, 73 | bool? immediate, 74 | bool? moveOnlyToNearby, 75 | }) { 76 | return SpannableGridEditingStrategy( 77 | allowed: allowed ?? this.allowed, 78 | enterOnLongTap: enterOnLongTap ?? this.enterOnLongTap, 79 | exitOnTap: exitOnTap ?? this.exitOnTap, 80 | immediate: immediate ?? this.immediate, 81 | moveOnlyToNearby: moveOnlyToNearby ?? this.moveOnlyToNearby, 82 | ); 83 | } 84 | } 85 | 86 | /// A set of options to style the [SpannableGrid]. 87 | /// 88 | /// 89 | class SpannableGridStyle { 90 | const SpannableGridStyle({ 91 | this.backgroundColor = Colors.black12, 92 | this.contentOpacity = 0.5, 93 | this.selectedCellDecoration, 94 | this.spacing = 2.0, 95 | }); 96 | 97 | /// A color of empty cells. 98 | /// 99 | /// It is used to display empty cells, when the [emptyCellView] is not specified. 100 | /// 101 | /// Defaults to [Colors.black12]. 102 | /// 103 | final Color backgroundColor; 104 | 105 | /// Used in the editing mode to make the grid structure visible. 106 | /// 107 | /// This opacity value is applied to cells while the grid is in editing mode, 108 | /// so the underlying grid structure can be visible. 109 | /// 110 | final double contentOpacity; 111 | 112 | /// An additional decoration of the selected cell. 113 | /// 114 | /// This decoration is used to highlight the selected cell in the editing mode. 115 | /// 116 | final Decoration? selectedCellDecoration; 117 | 118 | /// Space between cells. 119 | /// 120 | final double spacing; 121 | } 122 | 123 | /// How the [SpannableGrid] fits its parent. 124 | /// 125 | enum SpannableGridSize { 126 | /// The grid is sized to fit both parent's width and height. 127 | /// 128 | /// In this case the cell size will has the same aspect ratio as the grid's parent. 129 | /// 130 | parent, 131 | 132 | /// The grid is sized to fit its parent's width. 133 | /// 134 | /// Cells are square in this case, and the grid's height is calculated to place all 135 | /// the rows. 136 | /// Consider to wrap the [SpannableGrid] in to some scrollable widget to avoid 137 | /// overflow errors. 138 | /// 139 | /// This sizing option is used by default. 140 | /// 141 | parentWidth, 142 | 143 | /// The grid is sized to fit its parent's height. 144 | /// 145 | /// Cells are square in this case, and the grid's width is calculated to place all 146 | /// the columns. 147 | /// Consider to wrap the [SpannableGrid] in to some scrollable widget to avoid 148 | /// overflow errors. 149 | /// 150 | parentHeight, 151 | } 152 | -------------------------------------------------------------------------------- /lib/src/spannable_grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | import 'spannable_grid_cell_data.dart'; 5 | import 'spannable_grid_cell_view.dart'; 6 | import 'spannable_grid_delegate.dart'; 7 | import 'spannable_grid_empty_cell_view.dart'; 8 | import 'spannable_grid_options.dart'; 9 | 10 | /// A grid widget that allows its items to span columns and rows and supports 11 | /// editing. 12 | /// 13 | /// Widget layouts its children (defined in [cells]) in a grid of fixed [columns] 14 | /// and [rows]. 15 | /// The gaps between grid cells is defined by optional [spacing] parameter. 16 | /// The [SpannableGrid] is sized to fit its parent widget width. 17 | /// 18 | /// The widget supports editing mode in which user can move selected cell to 19 | /// available places withing the grid. User enter the editing mode by long 20 | /// press on the cell. In the editing mode the editing cell is highlighted 21 | /// while other cells are faded. All grid structure becomes visible. User exit 22 | /// the editing mode by click on editing cell. Updated [SpannableGridCellData] 23 | /// object is returned in the [onCellChanged] callback. 24 | /// 25 | /// ```dart 26 | /// SpannableGrid( 27 | /// columns: 4, 28 | /// rows: 4, 29 | /// cells: cells, 30 | /// spacing: 2.0, 31 | /// onCellChanged: (cell) { print('Cell ${cell.id} changed'); }, 32 | /// ), 33 | /// ``` 34 | /// 35 | /// See also: 36 | /// - [SpannableGridCellData] 37 | /// - [SpannableGridEditingStrategy] 38 | /// - [SpannableGridStyle] 39 | /// - [SpannableGridSize] 40 | /// 41 | class SpannableGrid extends StatefulWidget { 42 | SpannableGrid({ 43 | Key? key, 44 | required this.cells, 45 | required this.columns, 46 | required this.rows, 47 | this.editingStrategy = const SpannableGridEditingStrategy(), 48 | this.style = const SpannableGridStyle(), 49 | this.emptyCellView, 50 | this.gridSize = SpannableGridSize.parentWidth, 51 | this.onCellChanged, 52 | this.showGrid = false, 53 | }) : super(key: key); 54 | 55 | /// Items data 56 | /// 57 | /// A list of [SpannableGridCellData] objects, containing item's id, position, 58 | /// size and content widget 59 | final List cells; 60 | 61 | /// Number of columns 62 | final int columns; 63 | 64 | /// Number of rows 65 | final int rows; 66 | 67 | /// How an editing mode should work. 68 | /// 69 | /// Defines if the editing mode is supported and what actions are recognized to 70 | /// enter and exit the editing mode. 71 | /// 72 | /// Default strategy is to allow the editing mode, enter it by long tap and exit by tap. 73 | /// 74 | final SpannableGridEditingStrategy editingStrategy; 75 | 76 | /// Appearance of the grid. 77 | /// 78 | /// Contain parameters to style cells and grid layout in both view and editing modes. 79 | /// 80 | final SpannableGridStyle style; 81 | 82 | /// A widget to display in empty cells. 83 | /// 84 | /// Also it is used as a background for all cells in the editing mode. 85 | /// If it is not set, the [emptyCellColor] is used to display empty cell. 86 | /// 87 | final Widget? emptyCellView; 88 | 89 | /// How a grid size is determined. 90 | /// 91 | /// When it is [SpannableGridSize.parent], grid is sized to fully fit parent's constrains. 92 | /// This means that grid cell's aspect ratio will be the same as the grid's one. 93 | /// If it is [SpannableGridSize.parentWidth] or [SpannableGridSize.parentHeight], 94 | /// then grid's height or width respectively will be equal the opposite side. 95 | /// Consequently, in this case grid cell's aspect ratio is 1 (grid cells are square). 96 | /// 97 | /// Defaults to [SpannableGridSize.parentWidth]. 98 | /// 99 | final SpannableGridSize gridSize; 100 | 101 | /// A callback, that called when a cell position is changed by the user 102 | final Function(SpannableGridCellData?)? onCellChanged; 103 | 104 | /// When set to 'true', the grid structure is always visible. 105 | /// 106 | /// In this case the [emptyCellView] or [emptyCellColor] is used to display empty cells. 107 | /// 108 | /// Defaults to 'false'. 109 | /// 110 | final bool showGrid; 111 | 112 | @override 113 | _SpannableGridState createState() => _SpannableGridState(); 114 | } 115 | 116 | class _SpannableGridState extends State { 117 | final _availableCells = >[]; 118 | 119 | final _cells = {}; 120 | 121 | final _children = []; 122 | 123 | Size? _cellSize; 124 | 125 | bool _isEditing = false; 126 | 127 | SpannableGridCellData? _editingCell; 128 | 129 | // When dragging started, contains a relative position of the pointer in the 130 | // dragging widget 131 | Offset? _dragLocalPosition; 132 | 133 | @override 134 | void didChangeDependencies() { 135 | super.didChangeDependencies(); 136 | _updateCellsAndChildren(); 137 | } 138 | 139 | @override 140 | void didUpdateWidget(SpannableGrid oldWidget) { 141 | super.didUpdateWidget(oldWidget); 142 | _updateCellsAndChildren(); 143 | } 144 | 145 | @override 146 | Widget build(BuildContext context) { 147 | return _constrainGrid( 148 | child: CustomMultiChildLayout( 149 | delegate: SpannableGridDelegate( 150 | cells: _cells, 151 | columns: widget.columns, 152 | rows: widget.rows, 153 | spacing: widget.style.spacing, 154 | gridSize: widget.gridSize, 155 | onCellSizeCalculated: (size) { 156 | _cellSize = size; 157 | }), 158 | children: _children, 159 | ), 160 | ); 161 | } 162 | 163 | Widget _constrainGrid({required Widget child}) { 164 | switch (widget.gridSize) { 165 | case SpannableGridSize.parent: 166 | return child; 167 | case SpannableGridSize.parentWidth: 168 | case SpannableGridSize.parentHeight: 169 | return AspectRatio( 170 | aspectRatio: widget.columns / widget.rows, 171 | child: child, 172 | ); 173 | } 174 | } 175 | 176 | void _onEnterEditing(SpannableGridCellData cell) { 177 | setState(() { 178 | _isEditing = true; 179 | _editingCell = _cells[cell.id]; 180 | _updateCellsAndChildren(); 181 | }); 182 | } 183 | 184 | void _onExitEditing() { 185 | setState(() { 186 | widget.onCellChanged?.call(_editingCell); 187 | _isEditing = false; 188 | _editingCell = null; 189 | _updateCellsAndChildren(); 190 | }); 191 | } 192 | 193 | void _updateCellsAndChildren() { 194 | _cells.clear(); 195 | _children.clear(); 196 | if (_isEditing || widget.showGrid) { 197 | _addEmptyCellsAndChildren(); 198 | } 199 | _addContentCells(); 200 | _calculateAvailableCells(); 201 | _addContentChildren(); 202 | } 203 | 204 | void _addEmptyCellsAndChildren() { 205 | for (int column = 1; column <= widget.columns; column++) { 206 | for (int row = 1; row <= widget.rows; row++) { 207 | String id = 'SpannableCell-$column-$row'; 208 | _cells[id] = SpannableGridCellData( 209 | id: id, child: null, column: column, row: row); 210 | _children.add(LayoutId( 211 | id: id, 212 | child: SpannableGridEmptyCellView( 213 | data: _cells[id]!, 214 | style: widget.style, 215 | content: widget.emptyCellView, 216 | isEditing: _isEditing, 217 | onAccept: (data) { 218 | setState(() { 219 | if (_cellSize != null) { 220 | int dragColumnOffset = 221 | _dragLocalPosition!.dx ~/ _cellSize!.width; 222 | int dragRowOffset = 223 | _dragLocalPosition!.dy ~/ _cellSize!.height; 224 | data.column = column - dragColumnOffset; 225 | data.row = row - dragRowOffset; 226 | _updateCellsAndChildren(); 227 | } 228 | }); 229 | }, 230 | onWillAccept: (data) { 231 | if (_dragLocalPosition != null && _cellSize != null) { 232 | int dragColumnOffset = 233 | _dragLocalPosition!.dx ~/ _cellSize!.width; 234 | int dragRowOffset = _dragLocalPosition!.dy ~/ _cellSize!.height; 235 | final minY = row - dragRowOffset; 236 | final maxY = row - dragRowOffset + _editingCell!.rowSpan - 1; 237 | for (int y = minY; y <= maxY; y++) { 238 | final minX = column - dragColumnOffset; 239 | final maxX = 240 | column - dragColumnOffset + _editingCell!.columnSpan - 1; 241 | for (int x = minX; x <= maxX; x++) { 242 | if (y - 1 < 0 || 243 | y > widget.rows || 244 | x - 1 < 0 || 245 | x > widget.columns) { 246 | return false; 247 | } 248 | if (!_availableCells[y - 1][x - 1]) { 249 | return false; 250 | } 251 | } 252 | } 253 | return true; 254 | } 255 | return false; 256 | }, 257 | ), 258 | )); 259 | } 260 | } 261 | } 262 | 263 | void _addContentCells() { 264 | for (SpannableGridCellData cell in widget.cells) { 265 | _cells[cell.id] = cell; 266 | } 267 | } 268 | 269 | void _addContentChildren() { 270 | for (SpannableGridCellData cell in widget.cells) { 271 | Widget child = SpannableGridCellView( 272 | data: cell, 273 | editingStrategy: widget.editingStrategy, 274 | style: widget.style, 275 | isEditing: _isEditing, 276 | isSelected: cell.id == _editingCell?.id, 277 | canMove: widget.editingStrategy.moveOnlyToNearby 278 | ? _canMoveNearby(cell) 279 | : true, 280 | onDragStarted: (localPosition) => _dragLocalPosition = localPosition, 281 | onEnterEditing: () => _onEnterEditing(cell), 282 | onExitEditing: _onExitEditing, 283 | size: _cellSize == null 284 | ? const Size(0.0, 0.0) 285 | : Size( 286 | cell.columnSpan * _cellSize!.width - widget.style.spacing * 2, 287 | cell.rowSpan * _cellSize!.height - widget.style.spacing * 2), 288 | ); 289 | _children.add(LayoutId( 290 | id: cell.id, 291 | child: child, 292 | )); 293 | } 294 | } 295 | 296 | void _calculateAvailableCells() { 297 | _availableCells.clear(); 298 | for (int row = 1; row <= widget.rows; row++) { 299 | var rowCells = []; 300 | for (int column = 1; column <= widget.columns; column++) { 301 | rowCells.add(true); 302 | } 303 | _availableCells.add(rowCells); 304 | } 305 | for (SpannableGridCellData cell in _cells.values) { 306 | // Skip empty cells (grid background) and selected cell 307 | if (cell.child == null || cell.id == _editingCell?.id) continue; 308 | for (int row = cell.row; row <= cell.row + cell.rowSpan - 1; row++) { 309 | for (int column = cell.column; 310 | column <= cell.column + cell.columnSpan - 1; 311 | column++) { 312 | _availableCells[row - 1][column - 1] = false; 313 | } 314 | } 315 | } 316 | } 317 | 318 | bool _canMoveNearby(SpannableGridCellData cell) { 319 | final minColumn = cell.column; 320 | final maxColumn = cell.column + cell.columnSpan - 1; 321 | final minRow = cell.row; 322 | final maxRow = cell.row + cell.rowSpan - 1; 323 | // Check top 324 | if (cell.row > 1) { 325 | bool sideResult = true; 326 | for (int column = minColumn; column <= maxColumn; column++) { 327 | if (!_availableCells[cell.row - 2][column - 1]) { 328 | sideResult = false; 329 | break; 330 | } 331 | } 332 | if (sideResult) return true; 333 | } 334 | // Bottom 335 | if (cell.row + cell.rowSpan - 1 < widget.rows) { 336 | bool sideResult = true; 337 | for (int column = minColumn; column <= maxColumn; column++) { 338 | if (!_availableCells[cell.row + cell.rowSpan - 1][column - 1]) { 339 | sideResult = false; 340 | break; 341 | } 342 | } 343 | if (sideResult) return true; 344 | } 345 | // Left 346 | if (cell.column > 1) { 347 | bool sideResult = true; 348 | for (int row = minRow; row <= maxRow; row++) { 349 | if (!_availableCells[row - 1][cell.column - 2]) { 350 | sideResult = false; 351 | break; 352 | } 353 | } 354 | if (sideResult) return true; 355 | } 356 | // Right 357 | if (cell.column + cell.columnSpan - 1 < widget.columns) { 358 | bool sideResult = true; 359 | for (int row = minRow; row <= maxRow; row++) { 360 | if (!_availableCells[row - 1][cell.column + cell.columnSpan - 1]) { 361 | sideResult = false; 362 | break; 363 | } 364 | } 365 | if (sideResult) return true; 366 | } 367 | return false; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 13 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 14 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 15 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 16 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 18 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 19 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXCopyFilesBuildPhase section */ 23 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 24 | isa = PBXCopyFilesBuildPhase; 25 | buildActionMask = 2147483647; 26 | dstPath = ""; 27 | dstSubfolderSpec = 10; 28 | files = ( 29 | 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, 30 | 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, 31 | ); 32 | name = "Embed Frameworks"; 33 | runOnlyForDeploymentPostprocessing = 0; 34 | }; 35 | /* End PBXCopyFilesBuildPhase section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 39 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 40 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 41 | 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 42 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 43 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 44 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 45 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 46 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 47 | 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 48 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 50 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 51 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 52 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 61 | 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 62 | ); 63 | runOnlyForDeploymentPostprocessing = 0; 64 | }; 65 | /* End PBXFrameworksBuildPhase section */ 66 | 67 | /* Begin PBXGroup section */ 68 | 9740EEB11CF90186004384FC /* Flutter */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 3B80C3931E831B6300D905FE /* App.framework */, 72 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 73 | 9740EEBA1CF902C7004384FC /* Flutter.framework */, 74 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 75 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 76 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 77 | ); 78 | name = Flutter; 79 | sourceTree = ""; 80 | }; 81 | 97C146E51CF9000F007C117D = { 82 | isa = PBXGroup; 83 | children = ( 84 | 9740EEB11CF90186004384FC /* Flutter */, 85 | 97C146F01CF9000F007C117D /* Runner */, 86 | 97C146EF1CF9000F007C117D /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | 97C146EF1CF9000F007C117D /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 97C146EE1CF9000F007C117D /* Runner.app */, 94 | ); 95 | name = Products; 96 | sourceTree = ""; 97 | }; 98 | 97C146F01CF9000F007C117D /* Runner */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 102 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 103 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 104 | 97C147021CF9000F007C117D /* Info.plist */, 105 | 97C146F11CF9000F007C117D /* Supporting Files */, 106 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 107 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 108 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 109 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 110 | ); 111 | path = Runner; 112 | sourceTree = ""; 113 | }; 114 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | ); 118 | name = "Supporting Files"; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | 97C146ED1CF9000F007C117D /* Runner */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 127 | buildPhases = ( 128 | 9740EEB61CF901F6004384FC /* Run Script */, 129 | 97C146EA1CF9000F007C117D /* Sources */, 130 | 97C146EB1CF9000F007C117D /* Frameworks */, 131 | 97C146EC1CF9000F007C117D /* Resources */, 132 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 133 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 134 | ); 135 | buildRules = ( 136 | ); 137 | dependencies = ( 138 | ); 139 | name = Runner; 140 | productName = Runner; 141 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 142 | productType = "com.apple.product-type.application"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | 97C146E61CF9000F007C117D /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | LastUpgradeCheck = 1020; 151 | ORGANIZATIONNAME = "The Chromium Authors"; 152 | TargetAttributes = { 153 | 97C146ED1CF9000F007C117D = { 154 | CreatedOnToolsVersion = 7.3.1; 155 | LastSwiftMigration = 1100; 156 | }; 157 | }; 158 | }; 159 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 160 | compatibilityVersion = "Xcode 3.2"; 161 | developmentRegion = en; 162 | hasScannedForEncodings = 0; 163 | knownRegions = ( 164 | en, 165 | Base, 166 | ); 167 | mainGroup = 97C146E51CF9000F007C117D; 168 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 169 | projectDirPath = ""; 170 | projectRoot = ""; 171 | targets = ( 172 | 97C146ED1CF9000F007C117D /* Runner */, 173 | ); 174 | }; 175 | /* End PBXProject section */ 176 | 177 | /* Begin PBXResourcesBuildPhase section */ 178 | 97C146EC1CF9000F007C117D /* Resources */ = { 179 | isa = PBXResourcesBuildPhase; 180 | buildActionMask = 2147483647; 181 | files = ( 182 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 183 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 184 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 185 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 186 | ); 187 | runOnlyForDeploymentPostprocessing = 0; 188 | }; 189 | /* End PBXResourcesBuildPhase section */ 190 | 191 | /* Begin PBXShellScriptBuildPhase section */ 192 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 193 | isa = PBXShellScriptBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | ); 197 | inputPaths = ( 198 | ); 199 | name = "Thin Binary"; 200 | outputPaths = ( 201 | ); 202 | runOnlyForDeploymentPostprocessing = 0; 203 | shellPath = /bin/sh; 204 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; 205 | }; 206 | 9740EEB61CF901F6004384FC /* Run Script */ = { 207 | isa = PBXShellScriptBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | ); 211 | inputPaths = ( 212 | ); 213 | name = "Run Script"; 214 | outputPaths = ( 215 | ); 216 | runOnlyForDeploymentPostprocessing = 0; 217 | shellPath = /bin/sh; 218 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 219 | }; 220 | /* End PBXShellScriptBuildPhase section */ 221 | 222 | /* Begin PBXSourcesBuildPhase section */ 223 | 97C146EA1CF9000F007C117D /* Sources */ = { 224 | isa = PBXSourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 228 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 229 | ); 230 | runOnlyForDeploymentPostprocessing = 0; 231 | }; 232 | /* End PBXSourcesBuildPhase section */ 233 | 234 | /* Begin PBXVariantGroup section */ 235 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 236 | isa = PBXVariantGroup; 237 | children = ( 238 | 97C146FB1CF9000F007C117D /* Base */, 239 | ); 240 | name = Main.storyboard; 241 | sourceTree = ""; 242 | }; 243 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 244 | isa = PBXVariantGroup; 245 | children = ( 246 | 97C147001CF9000F007C117D /* Base */, 247 | ); 248 | name = LaunchScreen.storyboard; 249 | sourceTree = ""; 250 | }; 251 | /* End PBXVariantGroup section */ 252 | 253 | /* Begin XCBuildConfiguration section */ 254 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 255 | isa = XCBuildConfiguration; 256 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 257 | buildSettings = { 258 | ALWAYS_SEARCH_USER_PATHS = NO; 259 | CLANG_ANALYZER_NONNULL = YES; 260 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 261 | CLANG_CXX_LIBRARY = "libc++"; 262 | CLANG_ENABLE_MODULES = YES; 263 | CLANG_ENABLE_OBJC_ARC = YES; 264 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 265 | CLANG_WARN_BOOL_CONVERSION = YES; 266 | CLANG_WARN_COMMA = YES; 267 | CLANG_WARN_CONSTANT_CONVERSION = YES; 268 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 269 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 270 | CLANG_WARN_EMPTY_BODY = YES; 271 | CLANG_WARN_ENUM_CONVERSION = YES; 272 | CLANG_WARN_INFINITE_RECURSION = YES; 273 | CLANG_WARN_INT_CONVERSION = YES; 274 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 275 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 276 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 277 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 278 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 279 | CLANG_WARN_STRICT_PROTOTYPES = YES; 280 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 281 | CLANG_WARN_UNREACHABLE_CODE = YES; 282 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 283 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 284 | COPY_PHASE_STRIP = NO; 285 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 286 | ENABLE_NS_ASSERTIONS = NO; 287 | ENABLE_STRICT_OBJC_MSGSEND = YES; 288 | GCC_C_LANGUAGE_STANDARD = gnu99; 289 | GCC_NO_COMMON_BLOCKS = YES; 290 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 291 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 292 | GCC_WARN_UNDECLARED_SELECTOR = YES; 293 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 294 | GCC_WARN_UNUSED_FUNCTION = YES; 295 | GCC_WARN_UNUSED_VARIABLE = YES; 296 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 297 | MTL_ENABLE_DEBUG_INFO = NO; 298 | SDKROOT = iphoneos; 299 | SUPPORTED_PLATFORMS = iphoneos; 300 | TARGETED_DEVICE_FAMILY = "1,2"; 301 | VALIDATE_PRODUCT = YES; 302 | }; 303 | name = Profile; 304 | }; 305 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 306 | isa = XCBuildConfiguration; 307 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 308 | buildSettings = { 309 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 310 | CLANG_ENABLE_MODULES = YES; 311 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 312 | ENABLE_BITCODE = NO; 313 | FRAMEWORK_SEARCH_PATHS = ( 314 | "$(inherited)", 315 | "$(PROJECT_DIR)/Flutter", 316 | ); 317 | INFOPLIST_FILE = Runner/Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 319 | LIBRARY_SEARCH_PATHS = ( 320 | "$(inherited)", 321 | "$(PROJECT_DIR)/Flutter", 322 | ); 323 | PRODUCT_BUNDLE_IDENTIFIER = spannablegrid.example.com.example; 324 | PRODUCT_NAME = "$(TARGET_NAME)"; 325 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 326 | SWIFT_VERSION = 5.0; 327 | VERSIONING_SYSTEM = "apple-generic"; 328 | }; 329 | name = Profile; 330 | }; 331 | 97C147031CF9000F007C117D /* Debug */ = { 332 | isa = XCBuildConfiguration; 333 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 334 | buildSettings = { 335 | ALWAYS_SEARCH_USER_PATHS = NO; 336 | CLANG_ANALYZER_NONNULL = YES; 337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 338 | CLANG_CXX_LIBRARY = "libc++"; 339 | CLANG_ENABLE_MODULES = YES; 340 | CLANG_ENABLE_OBJC_ARC = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_EMPTY_BODY = YES; 348 | CLANG_WARN_ENUM_CONVERSION = YES; 349 | CLANG_WARN_INFINITE_RECURSION = YES; 350 | CLANG_WARN_INT_CONVERSION = YES; 351 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 352 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 353 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 354 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNREACHABLE_CODE = YES; 359 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 360 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 361 | COPY_PHASE_STRIP = NO; 362 | DEBUG_INFORMATION_FORMAT = dwarf; 363 | ENABLE_STRICT_OBJC_MSGSEND = YES; 364 | ENABLE_TESTABILITY = YES; 365 | GCC_C_LANGUAGE_STANDARD = gnu99; 366 | GCC_DYNAMIC_NO_PIC = NO; 367 | GCC_NO_COMMON_BLOCKS = YES; 368 | GCC_OPTIMIZATION_LEVEL = 0; 369 | GCC_PREPROCESSOR_DEFINITIONS = ( 370 | "DEBUG=1", 371 | "$(inherited)", 372 | ); 373 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 374 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 375 | GCC_WARN_UNDECLARED_SELECTOR = YES; 376 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 377 | GCC_WARN_UNUSED_FUNCTION = YES; 378 | GCC_WARN_UNUSED_VARIABLE = YES; 379 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 380 | MTL_ENABLE_DEBUG_INFO = YES; 381 | ONLY_ACTIVE_ARCH = YES; 382 | SDKROOT = iphoneos; 383 | TARGETED_DEVICE_FAMILY = "1,2"; 384 | }; 385 | name = Debug; 386 | }; 387 | 97C147041CF9000F007C117D /* Release */ = { 388 | isa = XCBuildConfiguration; 389 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 390 | buildSettings = { 391 | ALWAYS_SEARCH_USER_PATHS = NO; 392 | CLANG_ANALYZER_NONNULL = YES; 393 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 394 | CLANG_CXX_LIBRARY = "libc++"; 395 | CLANG_ENABLE_MODULES = YES; 396 | CLANG_ENABLE_OBJC_ARC = YES; 397 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 398 | CLANG_WARN_BOOL_CONVERSION = YES; 399 | CLANG_WARN_COMMA = YES; 400 | CLANG_WARN_CONSTANT_CONVERSION = YES; 401 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 402 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 403 | CLANG_WARN_EMPTY_BODY = YES; 404 | CLANG_WARN_ENUM_CONVERSION = YES; 405 | CLANG_WARN_INFINITE_RECURSION = YES; 406 | CLANG_WARN_INT_CONVERSION = YES; 407 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 408 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 409 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 410 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 411 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 412 | CLANG_WARN_STRICT_PROTOTYPES = YES; 413 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 414 | CLANG_WARN_UNREACHABLE_CODE = YES; 415 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 416 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 417 | COPY_PHASE_STRIP = NO; 418 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 419 | ENABLE_NS_ASSERTIONS = NO; 420 | ENABLE_STRICT_OBJC_MSGSEND = YES; 421 | GCC_C_LANGUAGE_STANDARD = gnu99; 422 | GCC_NO_COMMON_BLOCKS = YES; 423 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 424 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 425 | GCC_WARN_UNDECLARED_SELECTOR = YES; 426 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 427 | GCC_WARN_UNUSED_FUNCTION = YES; 428 | GCC_WARN_UNUSED_VARIABLE = YES; 429 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 430 | MTL_ENABLE_DEBUG_INFO = NO; 431 | SDKROOT = iphoneos; 432 | SUPPORTED_PLATFORMS = iphoneos; 433 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 434 | TARGETED_DEVICE_FAMILY = "1,2"; 435 | VALIDATE_PRODUCT = YES; 436 | }; 437 | name = Release; 438 | }; 439 | 97C147061CF9000F007C117D /* Debug */ = { 440 | isa = XCBuildConfiguration; 441 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 442 | buildSettings = { 443 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 444 | CLANG_ENABLE_MODULES = YES; 445 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 446 | ENABLE_BITCODE = NO; 447 | FRAMEWORK_SEARCH_PATHS = ( 448 | "$(inherited)", 449 | "$(PROJECT_DIR)/Flutter", 450 | ); 451 | INFOPLIST_FILE = Runner/Info.plist; 452 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 453 | LIBRARY_SEARCH_PATHS = ( 454 | "$(inherited)", 455 | "$(PROJECT_DIR)/Flutter", 456 | ); 457 | PRODUCT_BUNDLE_IDENTIFIER = spannablegrid.example.com.example; 458 | PRODUCT_NAME = "$(TARGET_NAME)"; 459 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 460 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 461 | SWIFT_VERSION = 5.0; 462 | VERSIONING_SYSTEM = "apple-generic"; 463 | }; 464 | name = Debug; 465 | }; 466 | 97C147071CF9000F007C117D /* Release */ = { 467 | isa = XCBuildConfiguration; 468 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 469 | buildSettings = { 470 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 471 | CLANG_ENABLE_MODULES = YES; 472 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 473 | ENABLE_BITCODE = NO; 474 | FRAMEWORK_SEARCH_PATHS = ( 475 | "$(inherited)", 476 | "$(PROJECT_DIR)/Flutter", 477 | ); 478 | INFOPLIST_FILE = Runner/Info.plist; 479 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 480 | LIBRARY_SEARCH_PATHS = ( 481 | "$(inherited)", 482 | "$(PROJECT_DIR)/Flutter", 483 | ); 484 | PRODUCT_BUNDLE_IDENTIFIER = spannablegrid.example.com.example; 485 | PRODUCT_NAME = "$(TARGET_NAME)"; 486 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 487 | SWIFT_VERSION = 5.0; 488 | VERSIONING_SYSTEM = "apple-generic"; 489 | }; 490 | name = Release; 491 | }; 492 | /* End XCBuildConfiguration section */ 493 | 494 | /* Begin XCConfigurationList section */ 495 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 496 | isa = XCConfigurationList; 497 | buildConfigurations = ( 498 | 97C147031CF9000F007C117D /* Debug */, 499 | 97C147041CF9000F007C117D /* Release */, 500 | 249021D3217E4FDB00AE95B9 /* Profile */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 506 | isa = XCConfigurationList; 507 | buildConfigurations = ( 508 | 97C147061CF9000F007C117D /* Debug */, 509 | 97C147071CF9000F007C117D /* Release */, 510 | 249021D4217E4FDB00AE95B9 /* Profile */, 511 | ); 512 | defaultConfigurationIsVisible = 0; 513 | defaultConfigurationName = Release; 514 | }; 515 | /* End XCConfigurationList section */ 516 | }; 517 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 518 | } 519 | --------------------------------------------------------------------------------