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'