diff --git a/Crane/.gitignore b/Crane/.gitignore
new file mode 100644
index 0000000000..73652c1603
--- /dev/null
+++ b/Crane/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+/studio/
diff --git a/Crane/README.md b/Crane/README.md
new file mode 100644
index 0000000000..b3c5ffde27
--- /dev/null
+++ b/Crane/README.md
@@ -0,0 +1 @@
+Steps to run this sample in go/compose-samples-howto
\ No newline at end of file
diff --git a/Crane/app/.gitignore b/Crane/app/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/Crane/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/Crane/app/build.gradle b/Crane/app/build.gradle
new file mode 100644
index 0000000000..d21eda652b
--- /dev/null
+++ b/Crane/app/build.gradle
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+// Reads the Google maps key that is used in the AndroidManifest
+Properties properties = new Properties()
+properties.load(project.rootProject.file("local.properties").newDataInputStream())
+
+android {
+ compileSdkVersion 29
+ defaultConfig {
+ applicationId "androidx.compose.samples.crane"
+ minSdkVersion 21
+ targetSdkVersion 29
+ versionCode 1
+ versionName "1.0"
+ vectorDrawables.useSupportLibrary = true
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ manifestPlaceholders = [ googleMapsKey : properties.getProperty("google.maps.key", "") ]
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerVersion "1.3.70-dev-withExperimentalGoogleExtensions-20200424"
+ kotlinCompilerExtensionVersion "0.1.0-SNAPSHOT"
+ }
+}
+
+configurations {
+ ktlint
+}
+
+dependencies {
+ // Actual dependencies of the application
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.61'
+ implementation 'com.google.android.gms:play-services-maps:17.0.0'
+
+ def composeVersion = "0.1.0-SNAPSHOT"
+ implementation ("androidx.compose:compose-runtime:$composeVersion")
+ implementation ("androidx.ui:ui-core:$composeVersion")
+ implementation ("androidx.ui:ui-foundation:$composeVersion")
+ implementation ("androidx.ui:ui-layout:$composeVersion")
+ implementation ("androidx.ui:ui-material:$composeVersion")
+ implementation ("androidx.ui:ui-android-view:$composeVersion")
+ implementation ("androidx.ui:ui-tooling:$composeVersion")
+ implementation ("androidx.ui:ui-android-view:$composeVersion")
+
+ implementation ('androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0')
+ implementation ('androidx.appcompat:appcompat:1.1.0')
+ implementation ('androidx.activity:activity-ktx:1.1.0')
+ implementation ('androidx.lifecycle:lifecycle-extensions:2.2.0')
+
+ implementation ('com.squareup.picasso:picasso:2.71828')
+
+ ktlint "com.pinterest:ktlint:0.36.0"
+}
+
+task ktlint(type: JavaExec, group: "verification") {
+ description = "Check Kotlin code style."
+ main = "com.pinterest.ktlint.Main"
+ classpath = configurations.ktlint
+ // Ignoring paren-spacing as it flags a composable type param as an error
+ args "-F", "src/**/*.kt", "--disabled_rules", "paren-spacing"
+}
+
+check.dependsOn ktlint
+
+task ktlintFormat(type: JavaExec, group: "formatting") {
+ description = "Fix Kotlin code style deviations."
+ main = "com.pinterest.ktlint.Main"
+ classpath = configurations.ktlint
+ // Ignoring paren-spacing as it flags a composable type param as an error
+ args "-F", "src/**/*.kt", "--disabled_rules", "paren-spacing"
+}
\ No newline at end of file
diff --git a/Crane/app/proguard-rules.pro b/Crane/app/proguard-rules.pro
new file mode 100644
index 0000000000..f1b424510d
--- /dev/null
+++ b/Crane/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/Crane/app/src/main/AndroidManifest.xml b/Crane/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..141d1c4998
--- /dev/null
+++ b/Crane/app/src/main/AndroidManifest.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Crane/app/src/main/ic_launcher-playstore.png b/Crane/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000000..acbe14b08a
Binary files /dev/null and b/Crane/app/src/main/ic_launcher-playstore.png differ
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt
new file mode 100644
index 0000000000..28a5526270
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/BaseUserInput.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.annotation.DrawableRes
+import androidx.compose.Composable
+import androidx.compose.getValue
+import androidx.compose.mutableStateOf
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.ui.captionTextStyle
+import androidx.compose.setValue
+import androidx.ui.core.Alignment
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Icon
+import androidx.ui.foundation.Text
+import androidx.ui.foundation.TextField
+import androidx.ui.foundation.TextFieldValue
+import androidx.ui.foundation.contentColor
+import androidx.ui.graphics.Color
+import androidx.ui.layout.Row
+import androidx.ui.layout.Spacer
+import androidx.ui.layout.padding
+import androidx.ui.layout.preferredSize
+import androidx.ui.layout.preferredWidth
+import androidx.ui.material.MaterialTheme
+import androidx.ui.material.Surface
+import androidx.ui.res.vectorResource
+import androidx.ui.tooling.preview.Preview
+import androidx.ui.unit.dp
+
+@Composable
+fun SimpleUserInput(
+ text: String? = null,
+ caption: String? = null,
+ @DrawableRes vectorImageId: Int? = null
+) {
+ CraneUserInput(
+ caption = if (text == null) caption else null,
+ text = text ?: "",
+ vectorImageId = vectorImageId
+ )
+}
+
+@Composable
+fun CraneUserInput(
+ modifier: Modifier = Modifier,
+ text: String,
+ caption: String? = null,
+ @DrawableRes vectorImageId: Int? = null,
+ tint: Color = contentColor()
+) {
+ CraneBaseUserInput(
+ modifier = modifier,
+ caption = caption,
+ vectorImageId = vectorImageId,
+ tintIcon = { text.isNotEmpty() },
+ tint = tint
+ ) {
+ Text(text = text, style = MaterialTheme.typography.body1.copy(color = tint))
+ }
+}
+
+@Composable
+fun CraneEditableUserInput(
+ hint: String,
+ caption: String? = null,
+ @DrawableRes vectorImageId: Int? = null,
+ onInputChanged: (String) -> Unit
+) {
+ var textFieldState by mutableStateOf(TextFieldValue(text = hint))
+ val isHint = { textFieldState.text == hint }
+
+ CraneBaseUserInput(
+ caption = caption,
+ tintIcon = { !isHint() },
+ showCaption = { !isHint() },
+ vectorImageId = vectorImageId
+ ) {
+ TextField(
+ value = textFieldState,
+ onValueChange = {
+ textFieldState = it
+ if (!isHint()) onInputChanged(textFieldState.text)
+ },
+ textStyle = if (isHint()) captionTextStyle else MaterialTheme.typography.body1
+ )
+ }
+}
+
+@Composable
+private fun CraneBaseUserInput(
+ modifier: Modifier = Modifier,
+ caption: String? = null,
+ @DrawableRes vectorImageId: Int? = null,
+ showCaption: () -> Boolean = { true },
+ tintIcon: () -> Boolean,
+ tint: Color = MaterialTheme.colors.onSurface,
+ children: @Composable () -> Unit
+) {
+ Surface(modifier = modifier, color = MaterialTheme.colors.primaryVariant) {
+ Row(Modifier.padding(all = 12.dp)) {
+ if (vectorImageId != null) {
+ Icon(
+ modifier = Modifier.preferredSize(24.dp, 24.dp),
+ asset = vectorResource(id = vectorImageId),
+ tint = if (tintIcon()) tint else Color(0x80FFFFFF)
+ )
+ Spacer(Modifier.preferredWidth(8.dp))
+ }
+ if (caption != null && showCaption()) {
+ Text(
+ modifier = Modifier.gravity(Alignment.CenterVertically),
+ text = caption,
+ style = (captionTextStyle).copy(color = tint)
+ )
+ Spacer(Modifier.preferredWidth(8.dp))
+ }
+ Row(Modifier.weight(1f).gravity(Alignment.CenterVertically)) {
+ children()
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun previewInput() {
+ CraneScaffold {
+ CraneBaseUserInput(
+ tintIcon = { true },
+ vectorImageId = R.drawable.ic_plane,
+ caption = "Caption",
+ showCaption = { true }) {
+ Text(text = "text", style = MaterialTheme.typography.body1)
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt
new file mode 100644
index 0000000000..3ecb2d40e7
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneDrawer.kt
@@ -0,0 +1,38 @@
+package androidx.compose.samples.crane.base
+
+import androidx.compose.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.ui.CraneTheme
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Image
+import androidx.ui.foundation.Text
+import androidx.ui.layout.Column
+import androidx.ui.layout.Spacer
+import androidx.ui.layout.fillMaxSize
+import androidx.ui.layout.padding
+import androidx.ui.layout.preferredHeight
+import androidx.ui.material.MaterialTheme
+import androidx.ui.res.vectorResource
+import androidx.ui.tooling.preview.Preview
+import androidx.ui.unit.dp
+
+private val screens = listOf("Find Trips", "My Trips", "Saved Trips", "Price Alerts", "My Account")
+
+@Composable
+fun CraneDrawer(modifier: Modifier = Modifier) {
+ Column(modifier.fillMaxSize().padding(start = 24.dp, top = 48.dp)) {
+ Image(asset = vectorResource(id = R.drawable.ic_crane_drawer))
+ for (screen in screens) {
+ Spacer(Modifier.preferredHeight(24.dp))
+ Text(text = screen, style = MaterialTheme.typography.h4)
+ }
+ }
+}
+
+@Preview
+@Composable
+fun CraneDrawerPreview() {
+ CraneTheme {
+ CraneDrawer()
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt
new file mode 100644
index 0000000000..eb3cb93761
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/CraneTabs.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.compose.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.home.CraneScreen
+import androidx.ui.core.Alignment
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Border
+import androidx.ui.foundation.Image
+import androidx.ui.foundation.Text
+import androidx.ui.foundation.clickable
+import androidx.ui.foundation.drawBorder
+import androidx.ui.foundation.shape.corner.RoundedCornerShape
+import androidx.ui.graphics.Color
+import androidx.ui.layout.Row
+import androidx.ui.layout.Spacer
+import androidx.ui.layout.padding
+import androidx.ui.layout.preferredWidth
+import androidx.ui.material.MaterialTheme
+import androidx.ui.material.Tab
+import androidx.ui.material.TabRow
+import androidx.ui.material.ripple.ripple
+import androidx.ui.res.vectorResource
+import androidx.ui.unit.dp
+
+@Composable
+fun CraneTabBar(
+ modifier: Modifier = Modifier,
+ onMenuClicked: () -> Unit,
+ children: @Composable (Modifier) -> Unit
+) {
+ Row(modifier) {
+ Row(Modifier.padding(top = 8.dp).ripple(bounded = false)) {
+ Image(
+ modifier = Modifier.padding(top = 8.dp).clickable(onClick = onMenuClicked),
+ asset = vectorResource(id = R.drawable.ic_menu)
+ )
+ Spacer(Modifier.preferredWidth(8.dp))
+ Image(asset = vectorResource(id = R.drawable.ic_crane_logo))
+ }
+ children(Modifier.weight(1f).gravity(Alignment.CenterVertically))
+ }
+}
+
+@Composable
+fun CraneTabs(
+ modifier: Modifier = Modifier,
+ titles: List,
+ tabSelected: CraneScreen,
+ onTabSelected: (CraneScreen) -> Unit
+) {
+ val indicatorContainer = @Composable { tabPositions: List ->
+ TabRow.IndicatorContainer(tabPositions, tabSelected.ordinal) {}
+ }
+
+ TabRow(
+ modifier = modifier,
+ items = titles,
+ selectedIndex = tabSelected.ordinal,
+ contentColor = MaterialTheme.colors.onSurface,
+ indicatorContainer = indicatorContainer,
+ divider = {}
+ ) { index, title ->
+ val selected = index == tabSelected.ordinal
+ val textModifier = if (!selected) {
+ Modifier
+ } else {
+ Modifier.drawBorder(
+ border = Border(2.dp, Color.White),
+ shape = RoundedCornerShape(20.dp)
+ ).padding(top = 8.dp, start = 20.dp, bottom = 8.dp, end = 20.dp)
+ }
+
+ Tab(
+ text = { Text(modifier = textModifier, text = title) },
+ selected = selected,
+ onSelected = { onTabSelected(CraneScreen.values()[index]) }
+ )
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt
new file mode 100644
index 0000000000..347d5ab579
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/ExploreSection.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.compose.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.samples.crane.data.ExploreUiModel
+import androidx.compose.samples.crane.home.OnExploreItemClicked
+import androidx.compose.samples.crane.ui.BottomSheetShape
+import androidx.compose.samples.crane.ui.crane_caption
+import androidx.compose.samples.crane.ui.crane_divider_color
+import androidx.ui.core.Alignment
+import androidx.ui.core.ContentScale
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Image
+import androidx.ui.foundation.Text
+import androidx.ui.foundation.VerticalScroller
+import androidx.ui.foundation.clickable
+import androidx.ui.foundation.shape.corner.RoundedCornerShape
+import androidx.ui.graphics.Color
+import androidx.ui.layout.Column
+import androidx.ui.layout.Row
+import androidx.ui.layout.Spacer
+import androidx.ui.layout.fillMaxSize
+import androidx.ui.layout.fillMaxWidth
+import androidx.ui.layout.padding
+import androidx.ui.layout.preferredHeight
+import androidx.ui.layout.preferredSize
+import androidx.ui.layout.preferredWidth
+import androidx.ui.material.Divider
+import androidx.ui.material.MaterialTheme
+import androidx.ui.material.Surface
+import androidx.ui.res.vectorResource
+import androidx.ui.unit.dp
+
+@Composable
+fun ExploreSection(
+ modifier: Modifier = Modifier,
+ title: String,
+ exploreList: List,
+ onItemClicked: OnExploreItemClicked
+) {
+ Surface(modifier = modifier.fillMaxSize(), color = Color.White, shape = BottomSheetShape) {
+ Column(modifier = Modifier.padding(start = 24.dp, top = 20.dp, end = 24.dp)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.caption.copy(color = crane_caption)
+ )
+ Spacer(Modifier.preferredHeight(8.dp))
+ VerticalScroller(modifier = Modifier.weight(1f)) {
+ Column {
+ exploreList.map { ExploreUiModel(it) }.forEachIndexed { index, item ->
+ ExploreItem(
+ modifier = Modifier.fillMaxWidth(),
+ item = item,
+ onItemClicked = onItemClicked
+ )
+// ------------------- b/137080715
+ if (index != exploreList.size - 1) {
+ Divider(color = crane_divider_color)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ExploreItem(
+ modifier: Modifier = Modifier,
+ item: ExploreUiModel,
+ onItemClicked: OnExploreItemClicked
+) {
+ Row(modifier = modifier.clickable(onClick = {
+ onItemClicked(item.exploreModel)
+ }).padding(top = 12.dp, bottom = 12.dp)) {
+ ExploreImageContainer {
+ if (item.image == null) {
+ Image(asset = vectorResource(id = R.drawable.ic_crane_logo))
+ } else {
+ Image(
+ asset = item.image!!,
+ alignment = Alignment.Center,
+ contentScale = ContentScale.Crop
+ )
+ }
+ }
+ Spacer(Modifier.preferredWidth(24.dp))
+ Column {
+ Text(
+ text = item.exploreModel.city.nameToDisplay,
+ style = MaterialTheme.typography.h6
+ )
+ Spacer(Modifier.preferredHeight(8.dp))
+ Text(
+ text = item.exploreModel.description,
+ style = MaterialTheme.typography.caption.copy(color = crane_caption)
+ )
+ }
+ }
+}
+
+@Composable
+private fun ExploreImageContainer(children: @Composable () -> Unit) {
+ Surface(Modifier.preferredSize(width = 60.dp, height = 60.dp), RoundedCornerShape(4.dp)) {
+ children()
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/Scaffold.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/Scaffold.kt
new file mode 100644
index 0000000000..5b371fe434
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/Scaffold.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.compose.Composable
+import androidx.compose.samples.crane.ui.CraneTheme
+import androidx.ui.material.MaterialTheme
+import androidx.ui.material.Surface
+
+@Composable
+fun CraneScaffold(children: @Composable () -> Unit) {
+ CraneTheme {
+ Surface(color = MaterialTheme.colors.primary) {
+ children()
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/base/ServiceLocator.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/base/ServiceLocator.kt
new file mode 100644
index 0000000000..569d1028ab
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/base/ServiceLocator.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.base
+
+import androidx.compose.samples.crane.calendar.model.DatesSelectedState
+
+object ServiceLocator {
+ val datesSelected = DatesSelectedState()
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/Calendar.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/Calendar.kt
new file mode 100644
index 0000000000..f1ad59eeb6
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/Calendar.kt
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.calendar
+
+import androidx.compose.Composable
+import androidx.compose.samples.crane.calendar.data.january2020
+import androidx.compose.samples.crane.calendar.data.year2020
+import androidx.compose.samples.crane.calendar.model.CalendarDay
+import androidx.compose.samples.crane.calendar.model.CalendarMonth
+import androidx.compose.samples.crane.calendar.model.DayOfWeek
+import androidx.compose.samples.crane.calendar.model.SelectedStatus
+import androidx.compose.samples.crane.ui.CraneTheme
+import androidx.compose.samples.crane.util.Circle
+import androidx.compose.samples.crane.util.SemiRect
+import androidx.ui.core.Alignment
+import androidx.ui.core.Modifier
+import androidx.ui.core.WithConstraints
+import androidx.ui.foundation.Text
+import androidx.ui.foundation.VerticalScroller
+import androidx.ui.foundation.clickable
+import androidx.ui.graphics.Color
+import androidx.ui.layout.Column
+import androidx.ui.layout.Row
+import androidx.ui.layout.Spacer
+import androidx.ui.layout.Stack
+import androidx.ui.layout.fillMaxHeight
+import androidx.ui.layout.fillMaxSize
+import androidx.ui.layout.fillMaxWidth
+import androidx.ui.layout.padding
+import androidx.ui.layout.preferredHeight
+import androidx.ui.layout.preferredHeightIn
+import androidx.ui.layout.preferredSize
+import androidx.ui.layout.wrapContentSize
+import androidx.ui.layout.wrapContentWidth
+import androidx.ui.material.ColorPalette
+import androidx.ui.material.MaterialTheme
+import androidx.ui.material.Surface
+import androidx.ui.tooling.preview.Preview
+import androidx.ui.unit.dp
+
+typealias CalendarWeek = List
+
+@Composable
+fun Calendar(
+ modifier: Modifier = Modifier,
+ onDayClicked: (CalendarDay, CalendarMonth) -> Unit
+) {
+ VerticalScroller(modifier = modifier) {
+ Column {
+ Spacer(Modifier.preferredHeight(32.dp))
+ for (month in year2020) {
+ Month(month = month, onDayClicked = onDayClicked)
+ Spacer(Modifier.preferredHeight(32.dp))
+ }
+ }
+ }
+}
+
+@Composable
+private fun Month(
+ modifier: Modifier = Modifier,
+ month: CalendarMonth,
+ onDayClicked: (CalendarDay, CalendarMonth) -> Unit
+) {
+ Column(modifier = modifier) {
+ MonthHeader(
+ modifier = Modifier.padding(start = 30.dp, end = 30.dp),
+ month = month.name,
+ year = month.year
+ )
+
+ // Expanding width and centering horizontally
+ val contentModifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.CenterHorizontally)
+ DaysOfWeek(modifier = contentModifier)
+ for (week in month.weeks.value) {
+ Week(
+ modifier = contentModifier,
+ week = week,
+ month = month,
+ onDayClicked = { day ->
+ onDayClicked(day, month)
+ }
+ )
+ Spacer(Modifier.preferredHeight(8.dp))
+ }
+ }
+}
+
+@Composable
+private fun MonthHeader(modifier: Modifier = Modifier, month: String, year: String) {
+ Row(modifier = modifier) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = month,
+ style = MaterialTheme.typography.h6
+ )
+ Text(
+ modifier = Modifier.gravity(Alignment.CenterVertically),
+ text = year,
+ style = MaterialTheme.typography.caption
+ )
+ }
+}
+
+@Composable
+private fun Week(
+ modifier: Modifier = Modifier,
+ month: CalendarMonth,
+ week: CalendarWeek,
+ onDayClicked: (CalendarDay) -> Unit
+) {
+ val (leftFillColor, rightFillColor) = getLeftRightWeekColors(week, month)
+
+ Row(modifier = modifier) {
+ val spaceModifiers = Modifier.weight(1f).preferredHeightIn(maxHeight = CELL_SIZE)
+ Surface(modifier = spaceModifiers, color = leftFillColor) {
+ Spacer(Modifier.fillMaxHeight())
+ }
+ for (day in week) {
+ Day(day = day, onDayClicked = onDayClicked)
+ }
+ Surface(modifier = spaceModifiers, color = rightFillColor) {
+ Spacer(Modifier.fillMaxHeight())
+ }
+ }
+}
+
+@Composable
+private fun DaysOfWeek(modifier: Modifier = Modifier) {
+ Row(modifier = modifier) {
+ for (day in DayOfWeek.values()) {
+ Day(name = day.name.take(1))
+ }
+ }
+}
+
+@Composable
+private fun Day(day: CalendarDay, onDayClicked: (CalendarDay) -> Unit) {
+ val enabled = day.status != SelectedStatus.NON_CLICKABLE
+ DayContainer(
+ modifier = Modifier.clickable(onClick = {
+ if (day.status != SelectedStatus.NON_CLICKABLE) onDayClicked(day)
+ }, enabled = enabled),
+ backgroundColor = day.status.color(MaterialTheme.colors)
+ ) {
+ DayStatusContainer(status = day.status) {
+ Text(
+ modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center),
+ text = day.value,
+ style = MaterialTheme.typography.body1.copy(color = Color.White)
+ )
+ }
+ }
+}
+
+@Composable
+private fun Day(name: String) {
+ DayContainer {
+ Text(
+ modifier = Modifier.wrapContentSize(Alignment.Center),
+ text = name,
+ style = MaterialTheme.typography.caption.copy(Color.White.copy(alpha = 0.6f))
+ )
+ }
+}
+
+@Composable
+private fun DayContainer(
+ modifier: Modifier = Modifier,
+ backgroundColor: Color = Color.Transparent,
+ children: @Composable () -> Unit
+) {
+ // What if this doesn't fit the screen? - LayoutFlexible(1f) + LayoutAspectRatio(1f)
+ Surface(
+ modifier = modifier.preferredSize(width = CELL_SIZE, height = CELL_SIZE),
+ color = backgroundColor
+ ) {
+ children()
+ }
+}
+
+@Composable
+private fun DayStatusContainer(
+ status: SelectedStatus,
+ children: @Composable () -> Unit
+) {
+ if (status.isMarked()) {
+ Stack {
+ val color = MaterialTheme.colors.secondary
+
+ WithConstraints {
+ Circle(constraints = constraints, color = color)
+ if (status == SelectedStatus.FIRST_DAY) {
+ SemiRect(constraints = constraints, color = color, lookingLeft = false)
+ } else if (status == SelectedStatus.LAST_DAY) {
+ SemiRect(constraints = constraints, color = color, lookingLeft = true)
+ }
+ children()
+ }
+ }
+ } else {
+ children()
+ }
+}
+
+private fun SelectedStatus.color(theme: ColorPalette): Color = when (this) {
+ SelectedStatus.SELECTED -> theme.secondary
+ else -> Color.Transparent
+}
+
+@Composable
+private fun getLeftRightWeekColors(week: CalendarWeek, month: CalendarMonth): Pair {
+ val materialColors = MaterialTheme.colors
+
+ val firstDayOfTheWeek = week[0].value
+ val leftFillColor = if (firstDayOfTheWeek.isNotEmpty()) {
+ val lastDayPreviousWeek = month.getPreviousDay(firstDayOfTheWeek.toInt())
+ if (lastDayPreviousWeek?.status?.isMarked() == true && week[0].status.isMarked()) {
+ materialColors.secondary
+ } else {
+ Color.Transparent
+ }
+ } else {
+ Color.Transparent
+ }
+
+ val lastDayOfTheWeek = week[6].value
+ val rightFillColor = if (lastDayOfTheWeek.isNotEmpty()) {
+ val firstDayNextWeek = month.getNextDay(lastDayOfTheWeek.toInt())
+ if (firstDayNextWeek?.status?.isMarked() == true && week[6].status.isMarked()) {
+ materialColors.secondary
+ } else {
+ Color.Transparent
+ }
+ } else {
+ Color.Transparent
+ }
+
+ return leftFillColor to rightFillColor
+}
+
+private fun SelectedStatus.isMarked(): Boolean {
+ return when (this) {
+ SelectedStatus.SELECTED -> true
+ SelectedStatus.FIRST_DAY -> true
+ SelectedStatus.LAST_DAY -> true
+ SelectedStatus.FIRST_LAST_DAY -> true
+ else -> false
+ }
+}
+
+private val CELL_SIZE = 48.dp
+
+@Preview
+@Composable
+fun DayPreview() {
+ CraneTheme {
+ Calendar(onDayClicked = { _, _ -> })
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarActivity.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarActivity.kt
new file mode 100644
index 0000000000..8a54060965
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/CalendarActivity.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.calendar
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.base.CraneScaffold
+import androidx.compose.samples.crane.base.ServiceLocator
+import androidx.compose.samples.crane.calendar.model.CalendarDay
+import androidx.compose.samples.crane.calendar.model.CalendarMonth
+import androidx.compose.samples.crane.calendar.model.DaySelected
+import androidx.ui.core.setContent
+import androidx.ui.foundation.Image
+import androidx.ui.foundation.Text
+import androidx.ui.layout.Column
+import androidx.ui.material.IconButton
+import androidx.ui.material.MaterialTheme
+import androidx.ui.material.Surface
+import androidx.ui.material.TopAppBar
+import androidx.ui.res.vectorResource
+
+fun launchCalendarActivity(context: Context) {
+ val intent = Intent(context, CalendarActivity::class.java)
+ context.startActivity(intent)
+}
+
+class CalendarActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ CraneScaffold {
+ Surface {
+ CalendarScreen(onBackPressed = { finish() })
+ }
+ }
+ }
+ }
+}
+
+// Extracted out to a separate variable. If this lambda is used as a trailing lambda in the
+// Calendar function, it recomposes the whole Calendar view when clicked on it.
+private val onDayClicked: (CalendarDay, CalendarMonth) -> Unit = { calendarDay, calendarMonth ->
+ ServiceLocator.datesSelected.daySelected(
+ DaySelected(
+ day = calendarDay.value.toInt(),
+ month = calendarMonth
+ )
+ )
+}
+
+@Composable
+fun CalendarScreen(onBackPressed: () -> Unit) {
+ CraneScaffold {
+ Column {
+ val selectedDatesText = ServiceLocator.datesSelected.toString()
+ TopAppBar(
+ title = {
+ Text(
+ text = if (selectedDatesText.isEmpty()) "Select Dates"
+ else selectedDatesText
+ )
+ },
+ navigationIcon = {
+ IconButton(onClick = { onBackPressed() }) {
+ Image(asset = vectorResource(id = R.drawable.ic_back))
+ }
+ },
+ backgroundColor = MaterialTheme.colors.primaryVariant
+ )
+ Surface {
+ Calendar(onDayClicked = onDayClicked)
+ }
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/data/CalendarData.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/data/CalendarData.kt
new file mode 100644
index 0000000000..f1195315bb
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/data/CalendarData.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.calendar.data
+
+import androidx.compose.samples.crane.calendar.model.CalendarMonth
+import androidx.compose.samples.crane.calendar.model.DayOfWeek
+
+val january2020 = CalendarMonth(
+ name = "January",
+ year = "2020",
+ numDays = 31,
+ monthNumber = 1,
+ startDayOfWeek = DayOfWeek.WEDNESDAY
+)
+val february2020 = CalendarMonth(
+ name = "February",
+ year = "2020",
+ numDays = 29,
+ monthNumber = 2,
+ startDayOfWeek = DayOfWeek.SATURDAY
+)
+val march2020 = CalendarMonth(
+ name = "March",
+ year = "2020",
+ numDays = 31,
+ monthNumber = 3,
+ startDayOfWeek = DayOfWeek.SUNDAY
+)
+val april2020 = CalendarMonth(
+ name = "April",
+ year = "2020",
+ numDays = 30,
+ monthNumber = 4,
+ startDayOfWeek = DayOfWeek.WEDNESDAY
+)
+val may2020 = CalendarMonth(
+ name = "May",
+ year = "2020",
+ numDays = 31,
+ monthNumber = 5,
+ startDayOfWeek = DayOfWeek.FRIDAY
+)
+val june2020 = CalendarMonth(
+ name = "June",
+ year = "2020",
+ numDays = 30,
+ monthNumber = 6,
+ startDayOfWeek = DayOfWeek.MONDAY
+)
+val july2020 = CalendarMonth(
+ name = "July",
+ year = "2020",
+ numDays = 31,
+ monthNumber = 7,
+ startDayOfWeek = DayOfWeek.WEDNESDAY
+)
+val august2020 = CalendarMonth(
+ name = "August",
+ year = "2020",
+ numDays = 30,
+ monthNumber = 8,
+ startDayOfWeek = DayOfWeek.SATURDAY
+)
+val september2020 = CalendarMonth(
+ name = "August",
+ year = "2020",
+ numDays = 31,
+ monthNumber = 9,
+ startDayOfWeek = DayOfWeek.TUESDAY
+)
+val october2020 = CalendarMonth(
+ name = "October",
+ year = "2020",
+ numDays = 31,
+ monthNumber = 10,
+ startDayOfWeek = DayOfWeek.THURSDAY
+)
+val november2020 = CalendarMonth(
+ name = "November",
+ year = "2020",
+ numDays = 30,
+ monthNumber = 11,
+ startDayOfWeek = DayOfWeek.SUNDAY
+)
+val december2020 = CalendarMonth(
+ name = "December",
+ year = "2020",
+ numDays = 31,
+ monthNumber = 12,
+ startDayOfWeek = DayOfWeek.TUESDAY
+)
+
+val year2020 = listOf(
+ january2020,
+ february2020,
+ march2020,
+ april2020,
+ may2020,
+ june2020,
+ july2020,
+ august2020,
+ september2020,
+ october2020,
+ november2020,
+ december2020
+)
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/CalendarDay.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/CalendarDay.kt
new file mode 100644
index 0000000000..f9cc4ef6e4
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/CalendarDay.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.calendar.model
+
+import androidx.compose.getValue
+import androidx.compose.mutableStateOf
+import androidx.compose.setValue
+
+enum class DayOfWeek {
+ SUNDAY,
+ MONDAY,
+ TUESDAY,
+ WEDNESDAY,
+ THURSDAY,
+ FRIDAY,
+ SATURDAY
+}
+
+enum class SelectedStatus {
+ NO_SELECTED,
+ SELECTED,
+ NON_CLICKABLE,
+ FIRST_DAY,
+ LAST_DAY,
+ FIRST_LAST_DAY
+}
+
+class CalendarDay(val value: String, status: SelectedStatus) {
+ var status by mutableStateOf(status)
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/CalendarMonth.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/CalendarMonth.kt
new file mode 100644
index 0000000000..6ee5976b7b
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/CalendarMonth.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.calendar.model
+
+data class CalendarMonth(
+ val name: String,
+ val year: String,
+ val numDays: Int,
+ val monthNumber: Int,
+ val startDayOfWeek: DayOfWeek
+) {
+ val days = mutableListOf().apply {
+ // Add offset of the start of the month
+ for (i in 1..startDayOfWeek.ordinal) {
+ add(
+ CalendarDay(
+ "",
+ SelectedStatus.NON_CLICKABLE
+ )
+ )
+ }
+ // Add days of the month
+ for (i in 1..numDays) {
+ add(
+ CalendarDay(
+ i.toString(),
+ SelectedStatus.NO_SELECTED
+ )
+ )
+ }
+ }.toList()
+
+ fun getDay(day: Int): CalendarDay {
+ return days[day + startDayOfWeek.ordinal - 1]
+ }
+
+ fun getPreviousDay(day: Int): CalendarDay? {
+ if (day <= 1) return null
+ return getDay(day - 1)
+ }
+
+ fun getNextDay(day: Int): CalendarDay? {
+ if (day >= numDays) return null
+ return getDay(day + 1)
+ }
+
+ val weeks = lazy { days.chunked(7).map { completeWeek(it) } }
+
+ private fun completeWeek(list: List): List {
+ var gapsToFill = 7 - list.size
+
+ return if (gapsToFill != 0) {
+ val mutableList = list.toMutableList()
+ while (gapsToFill > 0) {
+ mutableList.add(
+ CalendarDay(
+ "",
+ SelectedStatus.NON_CLICKABLE
+ )
+ )
+ gapsToFill--
+ }
+ mutableList
+ } else {
+ list
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/DatesSelectedState.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/DatesSelectedState.kt
new file mode 100644
index 0000000000..09e5afef01
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/DatesSelectedState.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.calendar.model
+
+import androidx.compose.getValue
+import androidx.compose.mutableStateOf
+import androidx.compose.samples.crane.calendar.data.year2020
+import androidx.compose.setValue
+
+class DatesSelectedState {
+ private var from by mutableStateOf(DaySelectedEmpty)
+ private var to by mutableStateOf(DaySelectedEmpty)
+
+ override fun toString(): String {
+ if (from == DaySelectedEmpty && to == DaySelectedEmpty) return ""
+ var output = from.toString()
+ if (to != DaySelectedEmpty) {
+ output += " - $to"
+ }
+ return output
+ }
+
+ fun daySelected(newDate: DaySelected) {
+ if (from == DaySelectedEmpty && to == DaySelectedEmpty) {
+ setDates(newDate, DaySelectedEmpty)
+ } else if (from != DaySelectedEmpty && to != DaySelectedEmpty) {
+ clearDates()
+ from = DaySelectedEmpty
+ to = DaySelectedEmpty
+ daySelected(newDate = newDate)
+ } else if (from == DaySelectedEmpty) {
+ if (newDate < to) setDates(newDate, to)
+ else if (newDate > to) setDates(to, newDate)
+ } else if (to == DaySelectedEmpty) {
+ if (newDate < from) setDates(newDate, from)
+ else if (newDate > from) setDates(from, newDate)
+ }
+ }
+
+ private fun setDates(newFrom: DaySelected, newTo: DaySelected) {
+ if (newTo == DaySelectedEmpty) {
+ from = newFrom
+ from.calendarDay.value.status = SelectedStatus.FIRST_LAST_DAY
+ } else {
+ from = newFrom.apply { calendarDay.value.status = SelectedStatus.FIRST_DAY }
+ selectDatesInBetween(newFrom, newTo)
+ to = newTo.apply { calendarDay.value.status = SelectedStatus.LAST_DAY }
+ }
+ }
+
+ private fun selectDatesInBetween(from: DaySelected, to: DaySelected) {
+ if (from.month == to.month) {
+ for (i in (from.day + 1) until to.day)
+ from.month.getDay(i).status = SelectedStatus.SELECTED
+ } else {
+ // Fill from's month
+ for (i in (from.day + 1) until from.month.numDays) {
+ from.month.getDay(i).status = SelectedStatus.SELECTED
+ }
+ from.month.getDay(from.month.numDays).status = SelectedStatus.LAST_DAY
+ // Fill in-between months
+ for (i in (from.month.monthNumber + 1) until to.month.monthNumber) {
+ val month = year2020[i - 1]
+ month.getDay(1).status = SelectedStatus.FIRST_DAY
+ for (j in 2 until month.numDays) {
+ month.getDay(j).status = SelectedStatus.SELECTED
+ }
+ month.getDay(month.numDays).status = SelectedStatus.LAST_DAY
+ }
+ // Fill to's month
+ to.month.getDay(1).status = SelectedStatus.FIRST_DAY
+ for (i in 2 until to.day) {
+ to.month.getDay(i).status = SelectedStatus.SELECTED
+ }
+ }
+ }
+
+ private fun clearDates() {
+ if (from != DaySelectedEmpty && to != DaySelectedEmpty) {
+ // Unselect dates from the same month
+ if (from.month == to.month) {
+ for (i in from.day..to.day)
+ from.month.getDay(i).status = SelectedStatus.NO_SELECTED
+ } else {
+ // Unselect from's month
+ for (i in from.day..from.month.numDays) {
+ from.month.getDay(i).status = SelectedStatus.NO_SELECTED
+ }
+ // Fill in-between months
+ for (i in (from.month.monthNumber + 1) until to.month.monthNumber) {
+ val month = year2020[i - 1]
+ for (j in 1..month.numDays) {
+ month.getDay(j).status = SelectedStatus.NO_SELECTED
+ }
+ }
+ // Fill to's month
+ for (i in 1..to.day) {
+ to.month.getDay(i).status = SelectedStatus.NO_SELECTED
+ }
+ }
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/DaySelected.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/DaySelected.kt
new file mode 100644
index 0000000000..228f501f01
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/calendar/model/DaySelected.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.calendar.model
+
+import androidx.compose.samples.crane.calendar.data.january2020
+import androidx.compose.samples.crane.calendar.data.year2020
+
+data class DaySelected(val day: Int, val month: CalendarMonth) {
+ val calendarDay = lazy {
+ month.getDay(day)
+ }
+
+ override fun toString(): String {
+ return "${month.name.substring(0, 3).capitalize()} $day"
+ }
+
+ operator fun compareTo(other: DaySelected): Int {
+ if (day == other.day && month == other.month) return 0
+ if (month == other.month) return day.compareTo(other.day)
+ return (year2020.indexOf(month)).compareTo(
+ year2020.indexOf(other.month)
+ )
+ }
+}
+
+/**
+ * Represents an empty value for [DaySelected]
+ */
+val DaySelectedEmpty = DaySelected(-1, january2020)
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/data/Cities.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/data/Cities.kt
new file mode 100644
index 0000000000..ffec4cae7b
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/data/Cities.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.data
+
+val MADRID = City(
+ name = "Madrid",
+ country = "Spain",
+ latitude = "40.416775",
+ longitude = "-3.703790"
+)
+
+val NAPLES = City(
+ name = "Naples",
+ country = "Italy",
+ latitude = "40.853294",
+ longitude = "14.305573"
+)
+
+val DALLAS = City(
+ name = "Dallas",
+ country = "US",
+ latitude = "32.779167",
+ longitude = "-96.808891"
+)
+
+val CORDOBA = City(
+ name = "Cordoba",
+ country = "Argentina",
+ latitude = "-31.416668",
+ longitude = "-64.183334"
+)
+
+val MALDIVAS = City(
+ name = "Maldivas",
+ country = "South Asia",
+ latitude = "1.924992",
+ longitude = "73.399658"
+)
+
+val ASPEN = City(
+ name = "Aspen",
+ country = "Colorado",
+ latitude = "39.191097",
+ longitude = "-106.817535"
+)
+
+val BALI = City(
+ name = "Bali",
+ country = "Indonesia",
+ latitude = "-8.3405",
+ longitude = "115.0920"
+)
+
+val BIGSUR = City(
+ name = "Big Sur",
+ country = "California",
+ latitude = "36.2704",
+ longitude = "-121.8081"
+)
+
+val KHUMBUVALLEY = City(
+ name = "Khumbu Valley",
+ country = "Nepal",
+ latitude = "27.9320",
+ longitude = "86.8050"
+)
+
+val ROME = City(
+ name = "Rome",
+ country = "Italy",
+ latitude = "41.902782",
+ longitude = "12.496366"
+)
+
+val GRANADA = City(
+ name = "Granada",
+ country = "Spain",
+ latitude = "37.18817",
+ longitude = "-3.60667"
+)
+
+val WASHINGTONDC = City(
+ name = "Washington DC",
+ country = "USA",
+ latitude = "38.9072",
+ longitude = "-77.0369"
+)
+
+val BARCELONA = City(
+ name = "Barcelona",
+ country = "Spain",
+ latitude = "41.390205",
+ longitude = "2.154007"
+)
+
+val CRETE = City(
+ name = "Crete",
+ country = "Greece",
+ latitude = "35.2401",
+ longitude = "24.8093"
+)
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/data/CraneData.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/data/CraneData.kt
new file mode 100644
index 0000000000..6aef1c3492
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/data/CraneData.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.data
+
+private const val DEFAULT_IMAGE_WIDTH = "250"
+
+val craneRestaurants = listOf(
+ ExploreModel(
+ city = NAPLES,
+ description = "1286 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1534308983496-4fabb1a015ee?ixlib=rb-1.2.1&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = DALLAS,
+ description = "2241 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1495749388945-9d6e4e5b67b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = CORDOBA,
+ description = "876 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1562625964-ffe9b2f617fc?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=250&q=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = MADRID,
+ description = "5610 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1515443961218-a51367888e4b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = MALDIVAS,
+ description = "1286 Restaurants",
+ imageUrl = "https://images.unsplash.com/flagged/photo-1556202256-af2687079e51?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = ASPEN,
+ description = "2241 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1542384557-0824d90731ee?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BALI,
+ description = "876 Restaurants",
+ imageUrl = "https://images.unsplash.com/photo-1567337710282-00832b415979?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ )
+)
+
+val craneHotels = listOf(
+ ExploreModel(
+ city = MALDIVAS,
+ description = "1286 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = ASPEN,
+ description = "2241 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1445019980597-93fa8acb246c?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BALI,
+ description = "876 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1570213489059-0aac6626cade?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BIGSUR,
+ description = "5610 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1561409037-c7be81613c1f?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = NAPLES,
+ description = "1286 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1455587734955-081b22074882?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = DALLAS,
+ description = "2241 Available Properties",
+ imageUrl = "https://images.unsplash.com/46/sh3y2u5PSaKq8c4LxB3B_submission-photo-4.jpg?ixlib=rb-1.2.1&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = CORDOBA,
+ description = "876 Available Properties",
+ imageUrl = "https://images.unsplash.com/photo-1570214476695-19bd467e6f7a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ )
+)
+
+val craneDestinations = listOf(
+ ExploreModel(
+ city = KHUMBUVALLEY,
+ description = "Nonstop - 5h 16m+",
+ imageUrl = "https://images.unsplash.com/photo-1544735716-392fe2489ffa?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = MADRID,
+ description = "Nonstop - 2h 12m+",
+ imageUrl = "https://images.unsplash.com/photo-1539037116277-4db20889f2d4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BALI,
+ description = "Nonstop - 6h 20m+",
+ imageUrl = "https://images.unsplash.com/photo-1518548419970-58e3b4079ab2?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = ROME,
+ description = "Nonstop - 2h 38m+",
+ imageUrl = "https://images.unsplash.com/photo-1515542622106-78bda8ba0e5b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = GRANADA,
+ description = "Nonstop - 2h 12m+",
+ imageUrl = "https://images.unsplash.com/photo-1534423839368-1796a4dd1845?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = MALDIVAS,
+ description = "Nonstop - 9h 24m+",
+ imageUrl = "https://images.unsplash.com/photo-1544550581-5f7ceaf7f992?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = WASHINGTONDC,
+ description = "Nonstop - 7h 30m+",
+ imageUrl = "https://images.unsplash.com/photo-1557160854-e1e89fdd3286?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = BARCELONA,
+ description = "Nonstop - 2h 12m+",
+ imageUrl = "https://images.unsplash.com/photo-1562883676-8c7feb83f09b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ ),
+ ExploreModel(
+ city = CRETE,
+ description = "Nonstop - 1h 50m+",
+ imageUrl = "https://images.unsplash.com/photo-1486575008575-27670acb58db?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=$DEFAULT_IMAGE_WIDTH"
+ )
+)
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/data/ExploreModel.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/data/ExploreModel.kt
new file mode 100644
index 0000000000..baa792625f
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/data/ExploreModel.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.data
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import androidx.compose.Immutable
+import androidx.compose.getValue
+import androidx.compose.mutableStateOf
+import androidx.compose.setValue
+import androidx.ui.graphics.ImageAsset
+import androidx.ui.graphics.asImageAsset
+import com.squareup.picasso.Picasso
+import com.squareup.picasso.Target
+
+@Immutable
+data class City(
+ val name: String,
+ val country: String,
+ val latitude: String,
+ val longitude: String
+) {
+ val nameToDisplay = "$name, $country"
+}
+
+@Immutable
+data class ExploreModel(
+ val city: City,
+ val description: String,
+ val imageUrl: String
+)
+
+class ExploreUiModel(val exploreModel: ExploreModel) {
+
+ var image by mutableStateOf(null)
+ private set
+
+ init {
+ val picasso = Picasso.get()
+ val target = object : Target {
+ override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}
+ override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {}
+ override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
+ image = bitmap?.asImageAsset()
+ }
+ }
+ picasso
+ .load(exploreModel.imageUrl)
+ .into(target)
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt
new file mode 100644
index 0000000000..3c81ba94c4
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/details/DetailsActivity.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.details
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.base.CraneScaffold
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.ui.core.Alignment
+import androidx.ui.core.Modifier
+import androidx.ui.core.setContent
+import androidx.ui.foundation.Box
+import androidx.ui.foundation.Text
+import androidx.ui.layout.Arrangement
+import androidx.ui.layout.Column
+import androidx.ui.layout.Spacer
+import androidx.ui.layout.padding
+import androidx.ui.layout.preferredHeight
+import androidx.ui.material.MaterialTheme
+import androidx.ui.unit.dp
+import androidx.ui.viewinterop.AndroidView
+import com.google.android.gms.maps.CameraUpdateFactory
+import com.google.android.gms.maps.SupportMapFragment
+import com.google.android.gms.maps.model.LatLng
+import com.google.android.gms.maps.model.MarkerOptions
+
+private const val DETAILS_NAME = "DETAILS_NAME"
+private const val DETAILS_DESCRIPTION = "DETAILS_DESCRIPTION"
+private const val DETAILS_LATITUDE = "DETAILS_LATITUDE"
+private const val DETAILS_LONGITUDE = "DETAILS_LONGITUDE"
+
+fun launchDetailsActivity(context: Context, item: ExploreModel) {
+ val intent = Intent(context, DetailsActivity::class.java)
+ intent.putExtra(DETAILS_NAME, item.city.nameToDisplay)
+ intent.putExtra(DETAILS_DESCRIPTION, item.description)
+ intent.putExtra(DETAILS_LATITUDE, item.city.latitude)
+ intent.putExtra(DETAILS_LONGITUDE, item.city.longitude)
+ context.startActivity(intent)
+}
+
+data class DetailsActivityArg(
+ val name: String,
+ val description: String,
+ val latitude: String,
+ val longitude: String
+)
+
+class DetailsActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val args = DetailsActivityArg(
+ name = intent.getStringExtra(DETAILS_NAME)!!,
+ description = intent.getStringExtra(DETAILS_DESCRIPTION)!!,
+ latitude = intent.getStringExtra(DETAILS_LATITUDE)!!,
+ longitude = intent.getStringExtra(DETAILS_LONGITUDE)!!
+ )
+
+ setContent {
+ CraneScaffold {
+ DetailsScreen(args = args)
+ }
+ }
+ }
+}
+
+@Composable
+fun DetailsScreen(args: DetailsActivityArg) {
+ Column(verticalArrangement = Arrangement.Center) {
+ Spacer(Modifier.preferredHeight(32.dp))
+ Text(
+ modifier = Modifier.gravity(Alignment.CenterHorizontally),
+ text = args.name,
+ style = MaterialTheme.typography.h4
+ )
+ Text(
+ modifier = Modifier.gravity(Alignment.CenterHorizontally),
+ text = args.description,
+ style = MaterialTheme.typography.h6
+ )
+ Spacer(Modifier.preferredHeight(16.dp))
+ Box(Modifier.padding(all = 8.dp)) {
+ // Interop between Compose and the Android UI toolkit as we need to show
+ // a MapView which extends an androidx Fragment
+
+ // For the map to work, you need to get an API key from
+ // https://developers.google.com/maps/documentation/android-sdk/get-api-key
+ // and put it in the google.maps.key variable of your local properties
+ AndroidView(resId = R.layout.layout_details_map, postInflationCallback = { view ->
+ val fragment = (view.context as AppCompatActivity).supportFragmentManager
+ .findFragmentById(R.id.map)
+
+ (fragment as SupportMapFragment).getMapAsync { map ->
+ val position = LatLng(args.latitude.toDouble(), args.longitude.toDouble())
+ map.addMarker(MarkerOptions().position(position).title("Marker in ${args.name}"))
+ map.moveCamera(CameraUpdateFactory.newLatLng(position))
+ }
+ })
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/CraneHome.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/CraneHome.kt
new file mode 100644
index 0000000000..f06f07664b
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/CraneHome.kt
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.Composable
+import androidx.compose.MutableState
+import androidx.compose.State
+import androidx.compose.samples.crane.base.CraneDrawer
+import androidx.compose.samples.crane.base.CraneTabBar
+import androidx.compose.samples.crane.base.CraneTabs
+import androidx.compose.samples.crane.base.ExploreSection
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.samples.crane.ui.BackdropFrontLayerDraggable
+import androidx.compose.state
+import androidx.ui.core.Modifier
+import androidx.ui.layout.Column
+import androidx.ui.material.DrawerState
+import androidx.ui.material.ModalDrawerLayout
+
+typealias OnExploreItemClicked = (ExploreModel) -> Unit
+
+enum class CraneScreen {
+ FLY, SLEEP, EAT
+}
+
+@Composable
+fun MainContent(
+ modifier: Modifier = Modifier,
+ destinations: List,
+ onExploreItemClicked: OnExploreItemClicked,
+ onDateSelectionClicked: () -> Unit,
+ viewModel: MainViewModel
+) {
+ CraneHome(
+ modifier = modifier,
+ viewModel = viewModel,
+ onExploreItemClicked = onExploreItemClicked,
+ onDateSelectionClicked = onDateSelectionClicked,
+ suggestedDestinations = destinations
+ )
+}
+
+@Composable
+private fun CraneHome(
+ modifier: Modifier = Modifier,
+ viewModel: MainViewModel,
+ onExploreItemClicked: OnExploreItemClicked,
+ onDateSelectionClicked: () -> Unit,
+ suggestedDestinations: List
+) {
+ val (drawerState, onDrawerStateChange) = state { DrawerState.Closed }
+ ModalDrawerLayout(
+ drawerState = drawerState,
+ onStateChange = onDrawerStateChange,
+ gesturesEnabled = drawerState == DrawerState.Opened,
+ drawerContent = { CraneDrawer() },
+ bodyContent = {
+ CraneHomeContent(
+ modifier = modifier,
+ viewModel = viewModel,
+ suggestedDestinations = suggestedDestinations,
+ onExploreItemClicked = onExploreItemClicked,
+ onDateSelectionClicked = onDateSelectionClicked,
+ openDrawer = { onDrawerStateChange(DrawerState.Opened) }
+ )
+ }
+ )
+}
+
+@Composable
+fun CraneHomeContent(
+ modifier: Modifier = Modifier,
+ viewModel: MainViewModel,
+ suggestedDestinations: List,
+ onExploreItemClicked: OnExploreItemClicked,
+ onDateSelectionClicked: () -> Unit,
+ openDrawer: () -> Unit
+) {
+ val onPeopleChanged: (Int) -> Unit = { viewModel.updatePeople(it) }
+ val tabSelected = state { CraneScreen.FLY }
+
+ BackdropFrontLayerDraggable(
+ modifier = modifier,
+ staticChildren = { staticModifier ->
+ Column(modifier = staticModifier) {
+ HomeTabBar(openDrawer, tabSelected)
+ SearchContent(
+ tabSelected,
+ viewModel,
+ onPeopleChanged,
+ onDateSelectionClicked,
+ onExploreItemClicked
+ )
+ }
+ },
+ backdropChildren = { backdropModifier ->
+ when (tabSelected.value) {
+ CraneScreen.FLY -> {
+ ExploreSection(
+ modifier = backdropModifier,
+ title = "Explore Flights by Destination",
+ exploreList = suggestedDestinations,
+ onItemClicked = onExploreItemClicked
+ )
+ }
+ CraneScreen.SLEEP -> {
+ ExploreSection(
+ modifier = backdropModifier,
+ title = "Explore Properties by Destination",
+ exploreList = viewModel.hotels,
+ onItemClicked = onExploreItemClicked
+ )
+ }
+ CraneScreen.EAT -> {
+ ExploreSection(
+ modifier = backdropModifier,
+ title = "Explore Restaurants by Destination",
+ exploreList = viewModel.restaurants,
+ onItemClicked = onExploreItemClicked
+ )
+ }
+ }
+ }
+ )
+}
+
+@Composable
+private fun HomeTabBar(
+ openDrawer: () -> Unit,
+ tabSelected: MutableState,
+ modifier: Modifier = Modifier
+) {
+ CraneTabBar(
+ modifier = modifier,
+ onMenuClicked = openDrawer
+ ) { tabBarModifier ->
+ CraneTabs(
+ modifier = tabBarModifier,
+ titles = CraneScreen.values().map { it.name },
+ tabSelected = tabSelected.value,
+ onTabSelected = { newTab ->
+ tabSelected.value = CraneScreen.values()[newTab.ordinal]
+ }
+ )
+ }
+}
+
+@Composable
+private fun SearchContent(
+ tabSelected: State,
+ viewModel: MainViewModel,
+ onPeopleChanged: (Int) -> Unit,
+ onDateSelectionClicked: () -> Unit,
+ onExploreItemClicked: OnExploreItemClicked
+) {
+ when (tabSelected.value) {
+ CraneScreen.FLY -> FlySearchContent(
+ searchUpdates = FlySearchContentUpdates(
+ onPeopleChanged = onPeopleChanged,
+ onToDestinationChanged = { viewModel.toDestinationChanged(it) },
+ onDateSelectionClicked = onDateSelectionClicked,
+ onExploreItemClicked = onExploreItemClicked
+ )
+ )
+ CraneScreen.SLEEP -> SleepSearchContent(
+ sleepUpdates = SleepSearchContentUpdates(
+ onPeopleChanged = onPeopleChanged,
+ onDateSelectionClicked = onDateSelectionClicked,
+ onExploreItemClicked = onExploreItemClicked
+ )
+ )
+ CraneScreen.EAT -> EatSearchContent(
+ eatUpdates = EatSearchContentUpdates(
+ onPeopleChanged = onPeopleChanged,
+ onDateSelectionClicked = onDateSelectionClicked,
+ onExploreItemClicked = onExploreItemClicked
+ )
+ )
+ }
+}
+
+data class FlySearchContentUpdates(
+ val onPeopleChanged: (Int) -> Unit,
+ val onToDestinationChanged: (String) -> Unit,
+ val onDateSelectionClicked: () -> Unit,
+ val onExploreItemClicked: OnExploreItemClicked
+)
+
+data class SleepSearchContentUpdates(
+ val onPeopleChanged: (Int) -> Unit,
+ val onDateSelectionClicked: () -> Unit,
+ val onExploreItemClicked: OnExploreItemClicked
+)
+
+data class EatSearchContentUpdates(
+ val onPeopleChanged: (Int) -> Unit,
+ val onDateSelectionClicked: () -> Unit,
+ val onExploreItemClicked: OnExploreItemClicked
+)
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/HomeFeatures.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/HomeFeatures.kt
new file mode 100644
index 0000000000..2872c10939
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/HomeFeatures.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.Composable
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.base.SimpleUserInput
+import androidx.ui.core.Modifier
+import androidx.ui.layout.Column
+import androidx.ui.layout.Spacer
+import androidx.ui.layout.padding
+import androidx.ui.layout.preferredHeight
+import androidx.ui.unit.dp
+
+@Composable
+fun FlySearchContent(searchUpdates: FlySearchContentUpdates) {
+ CraneSearch {
+ PeopleUserInput(
+ titleSuffix = ", Economy",
+ onPeopleChanged = searchUpdates.onPeopleChanged
+ )
+ Spacer(Modifier.preferredHeight(8.dp))
+ FromDestination()
+ Spacer(Modifier.preferredHeight(8.dp))
+ ToDestinationUserInput(onToDestinationChanged = searchUpdates.onToDestinationChanged)
+ Spacer(Modifier.preferredHeight(8.dp))
+ DatesUserInput(onDateSelectionClicked = searchUpdates.onDateSelectionClicked)
+ }
+}
+
+@Composable
+fun SleepSearchContent(sleepUpdates: SleepSearchContentUpdates) {
+ CraneSearch {
+ PeopleUserInput(onPeopleChanged = { sleepUpdates.onPeopleChanged })
+ Spacer(Modifier.preferredHeight(8.dp))
+ DatesUserInput(onDateSelectionClicked = sleepUpdates.onDateSelectionClicked)
+ Spacer(Modifier.preferredHeight(8.dp))
+ SimpleUserInput(caption = "Select Location", vectorImageId = R.drawable.ic_hotel)
+ }
+}
+
+@Composable
+fun EatSearchContent(eatUpdates: EatSearchContentUpdates) {
+ CraneSearch {
+ PeopleUserInput(onPeopleChanged = { eatUpdates.onPeopleChanged })
+ Spacer(Modifier.preferredHeight(8.dp))
+ DatesUserInput(onDateSelectionClicked = eatUpdates.onDateSelectionClicked)
+ Spacer(Modifier.preferredHeight(8.dp))
+ SimpleUserInput(caption = "Select Time", vectorImageId = R.drawable.ic_time)
+ Spacer(Modifier.preferredHeight(8.dp))
+ SimpleUserInput(caption = "Select Location", vectorImageId = R.drawable.ic_restaurant)
+ }
+}
+
+@Composable
+private fun CraneSearch(searchItems: @Composable () -> Unit) {
+ Column(Modifier.padding(start = 24.dp, top = 0.dp, end = 24.dp, bottom = 12.dp)) {
+ searchItems()
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt
new file mode 100644
index 0000000000..6db655db36
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/LandingScreen.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import android.os.Handler
+import android.os.Looper
+import androidx.compose.Composable
+import androidx.compose.MutableState
+import androidx.compose.onCommit
+import androidx.compose.samples.crane.R
+import androidx.core.os.postDelayed
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Box
+import androidx.ui.foundation.ContentGravity
+import androidx.ui.foundation.Image
+import androidx.ui.layout.fillMaxSize
+import androidx.ui.res.vectorResource
+
+private const val SPLASH_WAIT_TIME: Long = 2000
+
+@Composable
+fun LandingScreen(modifier: Modifier = Modifier, splashShownState: MutableState) {
+ Box(modifier = modifier.fillMaxSize(), gravity = ContentGravity.Center) {
+ onCommit {
+ Handler(Looper.getMainLooper()).postDelayed(SPLASH_WAIT_TIME) {
+ splashShownState.value = SplashState.COMPLETED
+ }
+ }
+ Image(asset = vectorResource(id = R.drawable.ic_crane_drawer))
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt
new file mode 100644
index 0000000000..d41c172a23
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainActivity.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.animation.FloatPropKey
+import androidx.animation.Spring.StiffnessLow
+import androidx.animation.transitionDefinition
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.Composable
+import androidx.compose.remember
+import androidx.compose.samples.crane.base.CraneScaffold
+import androidx.compose.samples.crane.calendar.launchCalendarActivity
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.samples.crane.details.launchDetailsActivity
+import androidx.compose.samples.crane.util.observe
+import androidx.ui.animation.DpPropKey
+import androidx.ui.core.Modifier
+import androidx.ui.core.setContent
+import androidx.ui.layout.Column
+import androidx.ui.layout.Spacer
+import androidx.ui.layout.padding
+import androidx.ui.unit.Dp
+import androidx.ui.unit.dp
+
+class MainActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val viewModel by viewModels()
+
+ setContent {
+ CraneScaffold {
+ val destinations = observe(viewModel.suggestedDestinations, this)
+
+ val onExploreItemClicked: OnExploreItemClicked = remember {
+ { launchDetailsActivity(context = this, item = it) }
+ }
+ val onDateSelectionClicked = remember {
+ { launchCalendarActivity(this) }
+ }
+
+ // FIXME: Removing Splash animation because of b/154198289
+ // Quick explanation: MainContentWrapper is being emitted twice to the screen
+
+// val splashShown = state { SplashState.SHOWN }
+// Transition(
+// definition = splashTransitionDefinition,
+// toState = splashShown.value
+// ) { state ->
+// Stack {
+// LandingScreen(
+// modifier = Modifier.drawOpacity(state[splashAlphaKey]),
+// splashShownState = splashShown
+// )
+ MainContentWrapper(
+// modifier = Modifier.drawOpacity(state[contentAlphaKey]),
+// topPadding = state[contentTopPaddingKey],
+ onExploreItemClicked = onExploreItemClicked,
+ onDateSelectionClicked = onDateSelectionClicked,
+ destinations = destinations ?: emptyList(),
+ viewModel = viewModel
+ )
+// }
+// }
+ }
+ }
+ }
+}
+
+@Composable
+private fun MainContentWrapper(
+ modifier: Modifier = Modifier,
+ topPadding: Dp = 0.dp,
+ viewModel: MainViewModel,
+ destinations: List,
+ onExploreItemClicked: OnExploreItemClicked,
+ onDateSelectionClicked: () -> Unit
+) {
+ Column(modifier = modifier) {
+ Spacer(Modifier.padding(top = topPadding))
+ MainContent(
+ viewModel = viewModel,
+ destinations = destinations,
+ onExploreItemClicked = onExploreItemClicked,
+ onDateSelectionClicked = onDateSelectionClicked
+ )
+ }
+}
+
+enum class SplashState { SHOWN, COMPLETED }
+
+private val splashAlphaKey = FloatPropKey()
+private val contentAlphaKey = FloatPropKey()
+private val contentTopPaddingKey = DpPropKey()
+
+private val splashTransitionDefinition = transitionDefinition {
+ state(SplashState.SHOWN) {
+ this[splashAlphaKey] = 1f
+ this[contentAlphaKey] = 0f
+ this[contentTopPaddingKey] = 100.dp
+ }
+ state(SplashState.COMPLETED) {
+ this[splashAlphaKey] = 0f
+ this[contentAlphaKey] = 1f
+ this[contentTopPaddingKey] = 0.dp
+ }
+ transition {
+ splashAlphaKey using tween {
+ duration = 100
+ }
+ contentAlphaKey using tween {
+ duration = 300
+ }
+ contentTopPaddingKey using physics {
+ stiffness = StiffnessLow
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt
new file mode 100644
index 0000000000..5158de6dea
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/MainViewModel.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.compose.samples.crane.data.ExploreModel
+import androidx.compose.samples.crane.data.craneDestinations
+import androidx.compose.samples.crane.data.craneHotels
+import androidx.compose.samples.crane.data.craneRestaurants
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import kotlin.random.Random
+
+const val MAX_PEOPLE = 4
+
+class MainViewModel : ViewModel() {
+
+ val hotels = craneHotels
+ val restaurants = craneRestaurants
+
+ private val _suggestedDestinations = MutableLiveData>()
+ val suggestedDestinations: LiveData>
+ get() = _suggestedDestinations
+
+ init {
+ _suggestedDestinations.value = craneDestinations
+ }
+
+ fun updatePeople(people: Int) {
+ if (people > MAX_PEOPLE) {
+ _suggestedDestinations.value = emptyList()
+ } else {
+ // Making Random more random
+ _suggestedDestinations.value =
+ craneDestinations.shuffled(Random(people * (1..100).shuffled().first()))
+ }
+ }
+
+ fun toDestinationChanged(newDestination: String) {
+ _suggestedDestinations.value =
+ craneDestinations.filter { it.city.nameToDisplay.contains(newDestination) }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt
new file mode 100644
index 0000000000..9c644da884
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/home/SearchUserInput.kt
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.home
+
+import androidx.animation.transitionDefinition
+import androidx.compose.Composable
+import androidx.compose.getValue
+import androidx.compose.mutableStateOf
+import androidx.compose.remember
+import androidx.compose.samples.crane.R
+import androidx.compose.samples.crane.base.CraneEditableUserInput
+import androidx.compose.samples.crane.base.CraneUserInput
+import androidx.compose.samples.crane.base.ServiceLocator
+import androidx.compose.setValue
+import androidx.ui.animation.ColorPropKey
+import androidx.ui.animation.Transition
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Text
+import androidx.ui.foundation.clickable
+import androidx.ui.graphics.Color
+import androidx.ui.layout.Column
+import androidx.ui.material.MaterialTheme
+
+class PeopleUserInputState {
+ var people by mutableStateOf(1)
+ private set
+
+ var animationState: PeopleUserInputAnimationState = PeopleUserInputAnimationState.Valid
+ private set
+
+ fun addPerson() {
+ people = (people % (MAX_PEOPLE + 1)) + 1
+ updateAnimationState()
+ }
+
+ private fun updateAnimationState() {
+ val newState =
+ if (people > MAX_PEOPLE) PeopleUserInputAnimationState.Invalid
+ else PeopleUserInputAnimationState.Valid
+
+ if (animationState != newState) animationState = newState
+ }
+}
+
+@Composable
+fun PeopleUserInput(
+ titleSuffix: String? = "",
+ onPeopleChanged: (Int) -> Unit,
+ peopleState: PeopleUserInputState = remember { PeopleUserInputState() }
+) {
+ Column {
+ val validColor = MaterialTheme.colors.onSurface
+ val invalidColor = MaterialTheme.colors.secondary
+ val transitionDefinition =
+ remember(validColor, invalidColor) {
+ generateTransitionDefinition(
+ validColor,
+ invalidColor
+ )
+ }
+
+ Transition(
+ definition = transitionDefinition,
+ toState = peopleState.animationState
+ ) { state ->
+ val people = peopleState.people
+ CraneUserInput(
+ modifier = Modifier.clickable(onClick = {
+ peopleState.addPerson()
+ onPeopleChanged(peopleState.people)
+ }),
+ text = if (people == 1) "$people Adult$titleSuffix" else "$people Adults$titleSuffix",
+ vectorImageId = R.drawable.ic_person,
+ tint = state[tintKey]
+ )
+ if (peopleState.animationState == PeopleUserInputAnimationState.Invalid) {
+ Text(
+ text = "Error: We don't support more than $MAX_PEOPLE people",
+ style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun FromDestination() {
+ CraneUserInput(text = "Seoul, South Korea", vectorImageId = R.drawable.ic_location)
+}
+
+@Composable
+fun ToDestinationUserInput(onToDestinationChanged: (String) -> Unit) {
+ CraneEditableUserInput(
+ hint = "Choose Destination",
+ caption = "To",
+ vectorImageId = R.drawable.ic_plane,
+ onInputChanged = onToDestinationChanged
+ )
+}
+
+@Composable
+fun DatesUserInput(onDateSelectionClicked: () -> Unit) {
+ val datesSelectedText = ServiceLocator.datesSelected.toString()
+ CraneUserInput(
+ modifier = Modifier.clickable(onClick = onDateSelectionClicked),
+ caption = if (datesSelectedText.isEmpty()) "Select Dates" else null,
+ text = datesSelectedText,
+ vectorImageId = R.drawable.ic_calendar
+ )
+}
+
+private val tintKey = ColorPropKey()
+
+enum class PeopleUserInputAnimationState { Valid, Invalid }
+
+private fun generateTransitionDefinition(
+ validColor: Color,
+ invalidColor: Color
+) = transitionDefinition {
+ state(PeopleUserInputAnimationState.Valid) {
+ this[tintKey] = validColor
+ }
+ state(PeopleUserInputAnimationState.Invalid) {
+ this[tintKey] = invalidColor
+ }
+ transition(fromState = PeopleUserInputAnimationState.Valid to PeopleUserInputAnimationState.Invalid) {
+ tintKey using tween {
+ duration = 300
+ }
+ }
+ transition(fromState = PeopleUserInputAnimationState.Invalid to PeopleUserInputAnimationState.Valid) {
+ tintKey using tween {
+ duration = 300
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/ui/BackdropFrontLayerDraggable.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/BackdropFrontLayerDraggable.kt
new file mode 100644
index 0000000000..2b888c7cd3
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/BackdropFrontLayerDraggable.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.ui
+
+import androidx.animation.PhysicsBuilder
+import androidx.compose.Composable
+import androidx.compose.getValue
+import androidx.compose.mutableStateOf
+import androidx.compose.remember
+import androidx.compose.setValue
+import androidx.ui.core.Constraints
+import androidx.ui.core.DensityAmbient
+import androidx.ui.core.Modifier
+import androidx.ui.core.WithConstraints
+import androidx.ui.core.onPositioned
+import androidx.ui.foundation.Box
+import androidx.ui.foundation.Canvas
+import androidx.ui.foundation.gestures.DragDirection
+import androidx.ui.layout.DpConstraints
+import androidx.ui.layout.Stack
+import androidx.ui.layout.fillMaxSize
+import androidx.ui.layout.offset
+import androidx.ui.layout.preferredSizeIn
+import androidx.ui.material.MaterialTheme
+import androidx.ui.unit.IntPx
+import androidx.ui.unit.IntPxSize
+import androidx.ui.unit.dp
+
+enum class FullScreenState {
+ MINIMISED,
+ COLLAPSED,
+ EXPANDED,
+}
+
+class DraggableBackdropState(value: FullScreenState = FullScreenState.MINIMISED) {
+ var value by mutableStateOf(value)
+}
+
+@Composable
+fun BackdropFrontLayerDraggable(
+ modifier: Modifier = Modifier,
+ backdropState: DraggableBackdropState = remember { DraggableBackdropState() },
+ staticChildren: @Composable (Modifier) -> Unit,
+ backdropChildren: @Composable (Modifier) -> Unit
+) {
+ var backgroundChildrenSize by mutableStateOf(IntPxSize(IntPx.Zero, IntPx.Zero))
+
+ Box(modifier) {
+ WithConstraints {
+ val fullHeight = constraints.maxHeight.value.toFloat()
+ val anchors = remember(backgroundChildrenSize.height) {
+ getAnchors(backgroundChildrenSize, fullHeight)
+ }
+
+ CraneStateDraggable(
+ state = backdropState.value,
+ onStateChange = { newExploreState -> backdropState.value = newExploreState },
+ anchorsToState = anchors,
+ animationBuilder = AnimationBuilder,
+ dragDirection = DragDirection.Vertical,
+ minValue = VerticalExplorePadding,
+ maxValue = fullHeight,
+ enabled = true
+ ) { model ->
+ Stack {
+ staticChildren(Modifier.onPositioned { coordinates ->
+ if (backgroundChildrenSize.height == IntPx.Zero) {
+ backdropState.value = FullScreenState.COLLAPSED
+ }
+ if (backgroundChildrenSize != coordinates.size) {
+ backgroundChildrenSize = coordinates.size
+ }
+ })
+
+ val shadowColor = MaterialTheme.colors.surface.copy(alpha = 0.8f)
+ val revealValue = backgroundChildrenSize.height.value.toFloat() / 2
+ if (model.value < revealValue) {
+ Canvas(Modifier.fillMaxSize()) {
+ drawRect(size = size, color = shadowColor)
+ }
+ }
+
+ val yOffset = with(DensityAmbient.current) {
+ model.value.toDp()
+ }
+
+ backdropChildren(
+ Modifier.offset(0.dp, yOffset)
+ .preferredSizeIn(currentConstraints(constraints))
+ )
+ }
+ }
+ }
+ }
+}
+
+private const val ANCHOR_BOTTOM_OFFSET = 130f
+
+private fun getAnchors(
+ searchChildrenSize: IntPxSize,
+ fullHeight: Float
+): List> {
+ val mediumValue = searchChildrenSize.height.value.toFloat() + 50.dp.value
+ val maxValue = fullHeight - ANCHOR_BOTTOM_OFFSET
+ return listOf(
+ 0f to FullScreenState.EXPANDED,
+ mediumValue to FullScreenState.COLLAPSED,
+ maxValue to FullScreenState.MINIMISED
+ )
+}
+
+@Composable
+private fun currentConstraints(pxConstraints: Constraints): DpConstraints {
+ return with(DensityAmbient.current) {
+ DpConstraints(pxConstraints)
+ }
+}
+
+private val AnimationBuilder = PhysicsBuilder().apply { stiffness = ExploreStiffness }
+private const val ExploreStiffness = 1000f
+private const val VerticalExplorePadding = 0f
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/ui/CraneStateDraggable.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/CraneStateDraggable.kt
new file mode 100644
index 0000000000..1e888c8d12
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/CraneStateDraggable.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.ui
+
+import androidx.animation.AnimatedFloat
+import androidx.animation.AnimationBuilder
+import androidx.animation.AnimationEndReason
+import androidx.compose.Composable
+import androidx.compose.onCommit
+import androidx.compose.remember
+import androidx.compose.state
+import androidx.ui.animation.animatedFloat
+import androidx.ui.core.Modifier
+import androidx.ui.core.PassThroughLayout
+import androidx.ui.foundation.animation.AnchorsFlingConfig
+import androidx.ui.foundation.animation.fling
+import androidx.ui.foundation.gestures.DragDirection
+import androidx.ui.foundation.gestures.draggable
+
+/**
+ * Higher-level component that allows dragging around anchored positions binded to different states
+ *
+ * Example might be a Switch which you can drag between two states (true or false).
+ *
+ * Additional features compared to regular [draggable] modifier:
+ * 1. The AnimatedFloat hosted inside and its value will be in sync with call site state
+ * 2. When the anchor is reached, [onStateChange] will be called with state mapped to this anchor
+ * 3. When the anchor is reached and [onStateChange] with corresponding state is called, but
+ * call site didn't update state to the reached one for some reason,
+ * this component performs rollback to the previous (correct) state.
+ * 4. When new [state] is provided, component will be animated to state's anchor
+ *
+ * children of this composable will receive [AnimatedFloat] class from which
+ * they can read current value when they need or manually animate.
+ *
+ * @param T type with which state is represented
+ * @param state current state to represent Float value with
+ * @param onStateChange callback to update call site's state
+ * @param anchorsToState pairs of anchors to states to map anchors to state and vise versa
+ * @param animationBuilder animation which will be used for animations
+ * @param dragDirection direction in which drag should be happening.
+ * Either [DragDirection.Vertical] or [DragDirection.Horizontal]
+ * @param minValue lower bound for draggable value in this component
+ * @param maxValue upper bound for draggable value in this component
+ * @param enabled whether or not this Draggable is enabled and should consume events
+ */
+// TODO(malkov/tianliu) (figure our how to make it better and make public)
+@Composable
+internal fun CraneStateDraggable(
+ state: T,
+ onStateChange: (T) -> Unit,
+ anchorsToState: List>,
+ animationBuilder: AnimationBuilder,
+ dragDirection: DragDirection,
+ enabled: Boolean = true,
+ minValue: Float = Float.MIN_VALUE,
+ maxValue: Float = Float.MAX_VALUE,
+ content: @Composable (AnimatedFloat) -> Unit
+) {
+ val forceAnimationCheck = state { true }
+
+ val anchors = remember(anchorsToState) { anchorsToState.map { it.first } }
+ val currentValue = anchorsToState.firstOrNull { it.second == state }!!.first
+ val flingConfig =
+ AnchorsFlingConfig(anchors, animationBuilder, onAnimationEnd = { reason, finalValue, _ ->
+ if (reason != AnimationEndReason.Interrupted) {
+ val newState = anchorsToState.firstOrNull { it.first == finalValue }?.second
+ if (newState != null && newState != state) {
+ onStateChange(newState)
+ forceAnimationCheck.value = !forceAnimationCheck.value
+ }
+ }
+ })
+ val position = animatedFloat(currentValue)
+ position.setBounds(minValue, maxValue)
+
+ // This state is to force this component to be recomposed and trigger onCommit below
+ // This is needed to stay in sync with drag state that caller side holds
+ onCommit(currentValue, forceAnimationCheck.value) {
+ position.animateTo(currentValue, animationBuilder)
+ }
+ val draggable = Modifier.draggable(
+ dragDirection = dragDirection,
+ onDragDeltaConsumptionRequested = { delta ->
+ val old = position.value
+ position.snapTo(position.value + delta)
+ position.value - old
+ },
+ onDragStopped = { position.fling(flingConfig, it) },
+ enabled = enabled,
+ startDragImmediately = position.isRunning
+ )
+ // TODO(b/150706555): This layout is temporary and should be removed once Semantics
+ // is implemented with modifiers.
+ @Suppress("DEPRECATION")
+ PassThroughLayout(draggable) {
+ content(position)
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/ui/CraneTheme.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/CraneTheme.kt
new file mode 100644
index 0000000000..bd122ce323
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/CraneTheme.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.ui
+
+import androidx.compose.Composable
+import androidx.ui.foundation.shape.corner.CornerSize
+import androidx.ui.foundation.shape.corner.RoundedCornerShape
+import androidx.ui.foundation.shape.corner.ZeroCornerSize
+import androidx.ui.graphics.Color
+import androidx.ui.material.MaterialTheme
+import androidx.ui.material.lightColorPalette
+import androidx.ui.unit.dp
+
+val crane_caption = Color.DarkGray
+val crane_divider_color = Color.LightGray
+private val crane_red = Color(0xFFE30425)
+private val crane_white = Color.White
+private val crane_purple_700 = Color(0xFF720D5D)
+private val crane_purple_800 = Color(0xFF5D1049)
+private val crane_purple_900 = Color(0xFF4E0D3A)
+
+val craneColors = lightColorPalette(
+ primary = crane_purple_800,
+ secondary = crane_red,
+ surface = crane_purple_900,
+ onSurface = crane_white,
+ primaryVariant = crane_purple_700
+)
+
+val BottomSheetShape = RoundedCornerShape(
+ topLeft = CornerSize(20.dp),
+ topRight = CornerSize(20.dp),
+ bottomLeft = ZeroCornerSize,
+ bottomRight = ZeroCornerSize
+)
+
+@Composable
+fun CraneTheme(children: @Composable () -> Unit) {
+ MaterialTheme(colors = craneColors, typography = craneTypography) {
+ children()
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Images.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Images.kt
new file mode 100644
index 0000000000..2501dbbba0
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Images.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.ui
+
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
+import androidx.compose.Composable
+import androidx.compose.getValue
+import androidx.compose.mutableStateOf
+import androidx.compose.onCommit
+import androidx.compose.samples.crane.R
+import androidx.compose.setValue
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Image
+import androidx.ui.foundation.shape.corner.RoundedCornerShape
+import androidx.ui.graphics.ImageAsset
+import androidx.ui.graphics.asImageAsset
+import androidx.ui.layout.preferredSize
+import androidx.ui.material.Surface
+import androidx.ui.res.vectorResource
+import androidx.ui.unit.Dp
+import androidx.ui.unit.dp
+import com.squareup.picasso.Picasso
+import com.squareup.picasso.Target
+
+@Composable
+fun NetworkImage(url: String, width: Dp, height: Dp) {
+ var image by mutableStateOf(null)
+
+ onCommit(url) {
+ val picasso = Picasso.get()
+ val target = object : Target {
+ override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}
+ override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {}
+ override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
+ image = bitmap?.asImageAsset()
+ }
+ }
+ picasso
+ .load(url)
+ .into(target)
+
+ onDispose {
+ image = null
+ picasso.cancelRequest(target)
+ }
+ }
+ Surface(
+ modifier = Modifier.preferredSize(width = width, height = height),
+ shape = RoundedCornerShape(4.dp)
+ ) {
+ val loadedImage = image
+ if (loadedImage != null) {
+ Image(asset = loadedImage)
+ } else {
+ Image(asset = vectorResource(id = R.drawable.ic_crane_logo))
+ }
+ }
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt
new file mode 100644
index 0000000000..1a7b93c3c1
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/ui/Typography.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.ui
+
+import androidx.compose.samples.crane.R
+import androidx.ui.material.Typography
+import androidx.ui.text.TextStyle
+import androidx.ui.text.font.FontWeight
+import androidx.ui.text.font.font
+import androidx.ui.text.font.fontFamily
+import androidx.ui.unit.sp
+
+private val light = font(R.font.raleway_light, FontWeight.W300)
+private val regular = font(R.font.raleway_regular, FontWeight.W400)
+private val medium = font(R.font.raleway_medium, FontWeight.W500)
+private val semibold = font(R.font.raleway_semibold, FontWeight.W600)
+
+private val craneFontFamily = fontFamily(fonts = listOf(light, regular, medium, semibold))
+
+val captionTextStyle = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 16.sp
+)
+
+val craneTypography = Typography(
+ h1 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W300,
+ fontSize = 96.sp
+ ),
+ h2 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 60.sp
+ ),
+ h3 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 48.sp
+ ),
+ h4 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 34.sp
+ ),
+ h5 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 24.sp
+ ),
+ h6 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 20.sp
+ ),
+ subtitle1 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 16.sp
+ ),
+ subtitle2 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 14.sp
+ ),
+ body1 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 16.sp
+ ),
+ body2 = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 14.sp
+ ),
+ button = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W600,
+ fontSize = 14.sp
+ ),
+ caption = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W500,
+ fontSize = 12.sp
+ ),
+ overline = TextStyle(
+ fontFamily = craneFontFamily,
+ fontWeight = FontWeight.W400,
+ fontSize = 12.sp
+ )
+)
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/util/Observation.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/util/Observation.kt
new file mode 100644
index 0000000000..bc4f815df8
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/util/Observation.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.util
+
+import androidx.compose.Composable
+import androidx.compose.onCommit
+import androidx.compose.state
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+
+@Composable
+fun observe(data: LiveData, owner: LifecycleOwner): T? {
+ val current = state { data.value }
+ onCommit(data) {
+ val observer = object : Observer {
+ override fun onChanged(t: T) {
+ current.value = t
+ }
+ }
+ data.observe(owner, observer)
+ onDispose {
+ data.removeObserver(observer)
+ }
+ }
+ return current.value
+}
diff --git a/Crane/app/src/main/java/androidx/compose/samples/crane/util/Shapes.kt b/Crane/app/src/main/java/androidx/compose/samples/crane/util/Shapes.kt
new file mode 100644
index 0000000000..da98d7d603
--- /dev/null
+++ b/Crane/app/src/main/java/androidx/compose/samples/crane/util/Shapes.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.samples.crane.util
+
+import androidx.compose.Composable
+import androidx.ui.core.Constraints
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Canvas
+import androidx.ui.geometry.Offset
+import androidx.ui.geometry.Size
+import androidx.ui.graphics.Color
+import androidx.ui.layout.fillMaxSize
+import androidx.ui.layout.preferredSizeIn
+import androidx.ui.unit.Dp
+import androidx.ui.unit.toPx
+import kotlin.math.min
+
+@Composable
+fun Circle(constraints: Constraints, color: Color) {
+ Canvas(modifier = constraints.toCanvasModifier()) {
+ drawCircle(
+ center = Offset(size.width / 2, size.height / 2),
+ radius = min(size.height, size.width) / 2,
+ color = color
+ )
+ }
+}
+
+@Composable
+fun SemiRect(constraints: Constraints, color: Color, lookingLeft: Boolean = true) {
+ Canvas(modifier = constraints.toCanvasModifier()) {
+ val offset = if (lookingLeft) {
+ Offset(0f, 0f)
+ } else {
+ Offset(size.width / 2, 0f)
+ }
+ val size = Size(width = size.width / 2, height = size.height)
+
+ drawRect(size = size, topLeft = offset, color = color)
+ }
+}
+
+private fun Constraints.toCanvasModifier(): Modifier {
+ return Modifier.preferredSizeIn(
+ minWidth = Dp(minWidth.toPx().value),
+ minHeight = Dp(minHeight.toPx().value),
+ maxWidth = Dp(maxWidth.toPx().value),
+ maxHeight = Dp(maxHeight.toPx().value)
+ ).fillMaxSize()
+}
diff --git a/Crane/app/src/main/res/drawable/ic_back.xml b/Crane/app/src/main/res/drawable/ic_back.xml
new file mode 100644
index 0000000000..2989fdadae
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_back.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_calendar.xml b/Crane/app/src/main/res/drawable/ic_calendar.xml
new file mode 100644
index 0000000000..49dc42f7d9
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_calendar.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_crane_drawer.xml b/Crane/app/src/main/res/drawable/ic_crane_drawer.xml
new file mode 100644
index 0000000000..43d2052ef3
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_crane_drawer.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_crane_logo.xml b/Crane/app/src/main/res/drawable/ic_crane_logo.xml
new file mode 100644
index 0000000000..fae4ebb91f
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_crane_logo.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_hotel.xml b/Crane/app/src/main/res/drawable/ic_hotel.xml
new file mode 100644
index 0000000000..1ae921185c
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_hotel.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_launcher_foreground.xml b/Crane/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..499e509030
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_location.xml b/Crane/app/src/main/res/drawable/ic_location.xml
new file mode 100644
index 0000000000..76676634e1
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_location.xml
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_menu.xml b/Crane/app/src/main/res/drawable/ic_menu.xml
new file mode 100644
index 0000000000..3137ba3502
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_menu.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_person.xml b/Crane/app/src/main/res/drawable/ic_person.xml
new file mode 100644
index 0000000000..b728d4db9e
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_person.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_plane.xml b/Crane/app/src/main/res/drawable/ic_plane.xml
new file mode 100644
index 0000000000..056c486e9d
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_plane.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_restaurant.xml b/Crane/app/src/main/res/drawable/ic_restaurant.xml
new file mode 100644
index 0000000000..89977ee7dc
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_restaurant.xml
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/Crane/app/src/main/res/drawable/ic_time.xml b/Crane/app/src/main/res/drawable/ic_time.xml
new file mode 100644
index 0000000000..0db2db0fb6
--- /dev/null
+++ b/Crane/app/src/main/res/drawable/ic_time.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/Crane/app/src/main/res/font/raleway_light.ttf b/Crane/app/src/main/res/font/raleway_light.ttf
new file mode 100755
index 0000000000..b5ec486060
Binary files /dev/null and b/Crane/app/src/main/res/font/raleway_light.ttf differ
diff --git a/Crane/app/src/main/res/font/raleway_medium.ttf b/Crane/app/src/main/res/font/raleway_medium.ttf
new file mode 100755
index 0000000000..070ac7691f
Binary files /dev/null and b/Crane/app/src/main/res/font/raleway_medium.ttf differ
diff --git a/Crane/app/src/main/res/font/raleway_regular.ttf b/Crane/app/src/main/res/font/raleway_regular.ttf
new file mode 100755
index 0000000000..746c242383
Binary files /dev/null and b/Crane/app/src/main/res/font/raleway_regular.ttf differ
diff --git a/Crane/app/src/main/res/font/raleway_semibold.ttf b/Crane/app/src/main/res/font/raleway_semibold.ttf
new file mode 100755
index 0000000000..34db420617
Binary files /dev/null and b/Crane/app/src/main/res/font/raleway_semibold.ttf differ
diff --git a/Crane/app/src/main/res/layout/layout_details_map.xml b/Crane/app/src/main/res/layout/layout_details_map.xml
new file mode 100644
index 0000000000..3643dedee8
--- /dev/null
+++ b/Crane/app/src/main/res/layout/layout_details_map.xml
@@ -0,0 +1,31 @@
+
+
+
diff --git a/Crane/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Crane/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..7353dbd1fd
--- /dev/null
+++ b/Crane/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Crane/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Crane/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..c3bc12a985
Binary files /dev/null and b/Crane/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/Crane/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Crane/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..ac3662d8db
Binary files /dev/null and b/Crane/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/Crane/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Crane/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..d16629b965
Binary files /dev/null and b/Crane/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/Crane/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Crane/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..5cb5cfe5b3
Binary files /dev/null and b/Crane/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/Crane/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Crane/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..ba761daff2
Binary files /dev/null and b/Crane/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/Crane/app/src/main/res/values/colors.xml b/Crane/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..cc7848b999
--- /dev/null
+++ b/Crane/app/src/main/res/values/colors.xml
@@ -0,0 +1,22 @@
+
+
+
+ #5D1049
+ #3D0A2C
+ #E30425
+
\ No newline at end of file
diff --git a/Crane/app/src/main/res/values/ic_launcher_background.xml b/Crane/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000000..ec64c6ca8d
--- /dev/null
+++ b/Crane/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,20 @@
+
+
+
+ #5D1049
+
\ No newline at end of file
diff --git a/Crane/app/src/main/res/values/strings.xml b/Crane/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..58d56fb370
--- /dev/null
+++ b/Crane/app/src/main/res/values/strings.xml
@@ -0,0 +1,19 @@
+
+
+ Crane
+
\ No newline at end of file
diff --git a/Crane/app/src/main/res/values/styles.xml b/Crane/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..79df355fd1
--- /dev/null
+++ b/Crane/app/src/main/res/values/styles.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Crane/app/src/release/res/values/google_maps_api.xml b/Crane/app/src/release/res/values/google_maps_api.xml
new file mode 100644
index 0000000000..b4a93a82c6
--- /dev/null
+++ b/Crane/app/src/release/res/values/google_maps_api.xml
@@ -0,0 +1,19 @@
+
+
+ YOUR_KEY_HERE
+
\ No newline at end of file
diff --git a/Crane/build.gradle.kts b/Crane/build.gradle.kts
new file mode 100644
index 0000000000..4338a00bf2
--- /dev/null
+++ b/Crane/build.gradle.kts
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019 Google, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath("com.android.tools.build:gradle:4.2.0-alpha01")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61")
+ }
+}
+
+allprojects {
+ val snapshotUrl =
+ "https://androidx-dev-prod.appspot.com/snapshots/builds/6534738/artifacts/ui/repository/"
+ repositories {
+ maven {
+ url = uri(snapshotUrl)
+ }
+ google()
+ jcenter()
+ }
+ // Don't require parens on fun type annotations e.g. `@Composable~()~ () -> Unit`. Remove with KT1.4
+ tasks.withType().configureEach {
+ kotlinOptions {
+ // Treat all Kotlin warnings as errors
+ allWarningsAsErrors = true
+
+ freeCompilerArgs = freeCompilerArgs +
+ listOf("-XXLanguage:+NonParenthesizedAnnotationsOnFunctionalTypes")
+
+ // Compose is now based on the Kotlin 1.4 compiler, but we need to use the 1.3.x Kotlin
+ // library due to library compatibility, etc. Therefore we explicit set our apiVersion
+ // to 1.3 to fix any warnings. Binary dependencies (such as Compose) can continue to
+ // use 1.4 if built with that library.
+ // TODO: remove this once we move to Kotlin 1.4
+ apiVersion = "1.3"
+ }
+ }
+}
diff --git a/Crane/gradle.properties b/Crane/gradle.properties
new file mode 100644
index 0000000000..23339e0df6
--- /dev/null
+++ b/Crane/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
diff --git a/Crane/gradle/wrapper/gradle-wrapper.jar b/Crane/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..f6b961fd5a
Binary files /dev/null and b/Crane/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Crane/gradle/wrapper/gradle-wrapper.properties b/Crane/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..4c476a8d49
--- /dev/null
+++ b/Crane/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Jun 15 11:35:59 CEST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-rc-1-bin.zip
diff --git a/Crane/gradlew b/Crane/gradlew
new file mode 100755
index 0000000000..cccdd3d517
--- /dev/null
+++ b/Crane/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/Crane/gradlew.bat b/Crane/gradlew.bat
new file mode 100644
index 0000000000..e95643d6a2
--- /dev/null
+++ b/Crane/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/Crane/settings.gradle b/Crane/settings.gradle
new file mode 100644
index 0000000000..e7b4def49c
--- /dev/null
+++ b/Crane/settings.gradle
@@ -0,0 +1 @@
+include ':app'