diff --git a/Owl/.google/packaging.yaml b/Owl/.google/packaging.yaml
new file mode 100644
index 00000000..08ccc281
--- /dev/null
+++ b/Owl/.google/packaging.yaml
@@ -0,0 +1,26 @@
+# Copyright 2020 The Android Open Source Project
+#
+# 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.
+#
+# This file is used by Google as part of our samples packaging process.
+# End users may safely ignore this file. It has no relevance to other systems.
+---
+status: PUBLISHED
+technologies: [Android]
+categories: [Material Components]
+languages: [Kotlin]
+solutions: [Mobile]
+github: material-components/material-components-android-examples
+branch: develop
+level: ADVANCED
+license: apache2
diff --git a/Owl/README.md b/Owl/README.md
index e71131d9..edd560e0 100644
--- a/Owl/README.md
+++ b/Owl/README.md
@@ -3,29 +3,28 @@
## Introduction
-Owl is an educational app that uses Material Design components and [Material Theming](https://material.io/design/material-theming) to create an energetic, motivational brand experience.
-This project is an Android implementation of [Owl](https://material.io/design/material-studies/owl.html), a Material study meant to showcase componentry and theming using the [Material Components for Android library](https://github.com/material-components/material-components-android).
+This project is an Android implementation of [Owl](https://material.io/design/material-studies/owl.html), a Material Study showcasing the possibilities of using Material Theming and Material Components for Android.
## Screenshots
## Material Theming
-Owl employs material theming to reflect the energy and excitement of learning a new skill, using a bold aesthetic and unfilled shapes that invite the user to populate them with new content and courses. The app customizes [color](https://material.io/develop/android/theming/color/), [shape](https://material.io/develop/android/theming/shape/) and [typography](https://material.io/develop/android/theming/typography/) to achieve this.
+Owl uses Material Theming to customize the app’s [color](https://material.io/develop/android/theming/color/), [shape](https://material.io/develop/android/theming/shape/) and [typography](https://material.io/develop/android/theming/typography/).
### Color
-Owl has three primary colors. Each color is used to create a distinct visual theme for each section. See [colors.xml](https://github.com/material-components/material-components-android-examples/blob/develop/Owl/app/src/main/res/values/color.xml) and how they are used in [light](https://github.com/material-components/material-components-android-examples/blob/develop/Owl/app/src/main/res/values/theme.xml#L58-L86) and [dark](https://github.com/material-components/material-components-android-examples/blob/develop/Owl/app/src/main/res/values-night/theme.xml) themes.
+Owl has three primary colors which are used to create distinct visual themes for each section. See [color.xml](Owl/app/src/main/res/values/color.xml) for the full color scheme and how colors are applied across [default](app/src/main/res/values/theme.xml#L58-L86) and [dark](app/src/main/res/values-night/theme.xml) themes.
### Shape
-Owl defines small, medium and large shape categories for different sized components. See [shape.xml](https://github.com/material-components/material-components-android-examples/blob/develop/Owl/app/src/main/res/values/shape.xml) which defines the `ShapeAppearance`s, which are then [set in the theme](https://github.com/material-components/material-components-android-examples/blob/develop/Owl/app/src/main/res/values/theme.xml#L20-L23) and picked up by all components or referred to directly.
+Owl defines small, medium and large shape categories for different sized components. See [shape.xml](app/src/main/res/values/shape.xml) which defines the `ShapeAppearance`s, which are then [set in the theme](app/src/main/res/values/theme.xml#L20-L23) and picked up by all components or referred to directly.
### Typography
-Owl’s type scale provides the typographic variety necessary for the app content. All items in the type scale use [Rubik](https://fonts.google.com/specimen/Rubik) as the typeface, and make use of the variety of weights available by using Rubik Regular, Medium, and Bold. See [type.xml](https://github.com/material-components/material-components-android-examples/blob/develop/Owl/app/src/main/res/values/type.xml) which defines `TextAppearance`s which are then [set in the theme](https://github.com/material-components/material-components-android-examples/blob/develop/Owl/app/src/main/res/values/theme.xml#L25-L38) and referred to using `?attr/textAppearance[...]` throughout.
+Owl’s type scale provides the typographic variety necessary for the app content. All items in the type scale use [Rubik](https://fonts.google.com/specimen/Rubik) as the typeface, and make use of the variety of weights available by using Rubik Regular, Medium, and Bold. See [type.xml](app/src/main/res/values/type.xml) which defines `TextAppearance`s which are then [set in the theme](app/src/main/res/values/theme.xml#L25-L38) and referred to using `?attr/textAppearance[...]` throughout.
## License
diff --git a/Owl/app/build.gradle b/Owl/app/build.gradle
index fb1c4496..3fde4e10 100644
--- a/Owl/app/build.gradle
+++ b/Owl/app/build.gradle
@@ -18,46 +18,50 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'androidx.navigation.safeargs.kotlin'
android {
- compileSdkVersion 29
+ compileSdkVersion 30
defaultConfig {
applicationId 'com.materialstudies.owl'
minSdkVersion 23
- targetSdkVersion 29
+ targetSdkVersion 30
versionCode 1
versionName '1.0'
vectorDrawables.useSupportLibrary = true
}
+
dataBinding {
enabled true
}
+
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"
}
}
dependencies {
- implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.fragment:fragment:1.2.0-beta02'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- implementation 'androidx.core:core-ktx:1.1.0'
- implementation 'com.google.android.material:material:1.1.0-beta01'
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.fragment:fragment-ktx:1.2.5'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation 'androidx.core:core-ktx:1.5.0-alpha04'
+ implementation 'com.google.android.material:material:1.4.0-alpha02'
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.navigation:navigation-runtime-ktx:$nav_version"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation 'androidx.dynamicanimation:dynamicanimation:1.0.0'
- implementation 'com.github.bumptech.glide:glide:4.9.0'
- annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
+ implementation 'com.github.bumptech.glide:glide:4.11.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}
diff --git a/Owl/app/src/main/java/com/materialstudies/owl/ui/MainActivity.kt b/Owl/app/src/main/java/com/materialstudies/owl/ui/MainActivity.kt
index 64d0304b..8c457372 100644
--- a/Owl/app/src/main/java/com/materialstudies/owl/ui/MainActivity.kt
+++ b/Owl/app/src/main/java/com/materialstudies/owl/ui/MainActivity.kt
@@ -20,6 +20,7 @@ import android.os.Bundle
import android.view.View.GONE
import android.view.View.VISIBLE
import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.Navigation
import androidx.navigation.ui.setupWithNavController
import com.materialstudies.owl.R
@@ -39,10 +40,12 @@ class MainActivity : AppCompatActivity() {
bottomNav.setupWithNavController(navController)
// Hide bottom nav on screens which don't require it
- navController.addOnDestinationChangedListener { _, destination, _ ->
- when (destination.id) {
- R.id.myCourses, R.id.featured, R.id.search -> bottomNav.show()
- else -> bottomNav.hide()
+ lifecycleScope.launchWhenResumed {
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ when (destination.id) {
+ R.id.myCourses, R.id.featured, R.id.search -> bottomNav.show()
+ else -> bottomNav.hide()
+ }
}
}
}
diff --git a/Owl/app/src/main/java/com/materialstudies/owl/ui/lessons/LessonBottomSheetBehavior.kt b/Owl/app/src/main/java/com/materialstudies/owl/ui/lessons/LessonBottomSheetBehavior.kt
new file mode 100644
index 00000000..33998541
--- /dev/null
+++ b/Owl/app/src/main/java/com/materialstudies/owl/ui/lessons/LessonBottomSheetBehavior.kt
@@ -0,0 +1,22 @@
+package com.materialstudies.owl.ui.lessons
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+
+/**
+ * A [BottomSheetBehavior] that helps to restrict touchable area depends on translationX
+ */
+class LessonBottomSheetBehavior(context: Context, attrs: AttributeSet?) :
+ BottomSheetBehavior(context, attrs) {
+
+ override fun onTouchEvent(parent: CoordinatorLayout, child: T, event: MotionEvent): Boolean {
+ if (event.x < child.translationX) {
+ return false
+ }
+ return super.onTouchEvent(parent, child, event)
+ }
+}
\ No newline at end of file
diff --git a/Owl/app/src/main/java/com/materialstudies/owl/ui/lessons/LessonsSheetFragment.kt b/Owl/app/src/main/java/com/materialstudies/owl/ui/lessons/LessonsSheetFragment.kt
index 4ee5f6e5..28945f12 100644
--- a/Owl/app/src/main/java/com/materialstudies/owl/ui/lessons/LessonsSheetFragment.kt
+++ b/Owl/app/src/main/java/com/materialstudies/owl/ui/lessons/LessonsSheetFragment.kt
@@ -27,6 +27,7 @@ import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.annotation.ColorInt
import androidx.annotation.Px
+import androidx.core.view.WindowInsetsCompat.Type
import androidx.core.view.doOnLayout
import androidx.core.view.forEach
import androidx.core.view.postDelayed
@@ -148,7 +149,7 @@ class LessonsSheetFragment : Fragment() {
}
})
lessonsSheet.doOnApplyWindowInsets { _, insets, _, _ ->
- behavior.peekHeight = peek + insets.systemWindowInsetBottom
+ behavior.peekHeight = peek + insets.getInsets(Type.navigationBars()).bottom
}
}
collapsePlaylist.setOnClickListener {
diff --git a/Owl/app/src/main/java/com/materialstudies/owl/util/BindingAdapters.kt b/Owl/app/src/main/java/com/materialstudies/owl/util/BindingAdapters.kt
index 1af559bc..270e4fcf 100644
--- a/Owl/app/src/main/java/com/materialstudies/owl/util/BindingAdapters.kt
+++ b/Owl/app/src/main/java/com/materialstudies/owl/util/BindingAdapters.kt
@@ -19,8 +19,9 @@ package com.materialstudies.owl.util
import android.graphics.drawable.Drawable
import android.view.View
import android.view.ViewGroup
-import android.view.WindowInsets
import android.widget.ImageView
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.databinding.BindingAdapter
import com.bumptech.glide.Glide
@@ -55,6 +56,9 @@ fun View.bindElevationOverlay(previousElevation: Float, elevation: Float) {
@BindingAdapter("layoutFullscreen")
fun View.bindLayoutFullscreen(previousFullscreen: Boolean, fullscreen: Boolean) {
if (previousFullscreen != fullscreen && fullscreen) {
+ @Suppress("DEPRECATION")
+ // The new alternative is WindowCompat.setDecorFitsSystemWindows, but we can't
+ // always get access to the window from a view.
systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
@@ -87,10 +91,11 @@ fun View.applySystemWindowInsetsPadding(
}
doOnApplyWindowInsets { view, insets, padding, _ ->
- val left = if (applyLeft) insets.systemWindowInsetLeft else 0
- val top = if (applyTop) insets.systemWindowInsetTop else 0
- val right = if (applyRight) insets.systemWindowInsetRight else 0
- val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val left = if (applyLeft) systemBars.left else 0
+ val top = if (applyTop) systemBars.top else 0
+ val right = if (applyRight) systemBars.right else 0
+ val bottom = if (applyBottom) systemBars.bottom else 0
view.setPadding(
padding.left + left,
@@ -127,10 +132,11 @@ fun View.applySystemWindowInsetsMargin(
}
doOnApplyWindowInsets { view, insets, _, margin ->
- val left = if (applyLeft) insets.systemWindowInsetLeft else 0
- val top = if (applyTop) insets.systemWindowInsetTop else 0
- val right = if (applyRight) insets.systemWindowInsetRight else 0
- val bottom = if (applyBottom) insets.systemWindowInsetBottom else 0
+ val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val left = if (applyLeft) systemBars.left else 0
+ val top = if (applyTop) systemBars.top else 0
+ val right = if (applyRight) systemBars.right else 0
+ val bottom = if (applyBottom) systemBars.bottom else 0
view.updateLayoutParams {
leftMargin = margin.left + left
@@ -142,14 +148,14 @@ fun View.applySystemWindowInsetsMargin(
}
fun View.doOnApplyWindowInsets(
- block: (View, WindowInsets, InitialPadding, InitialMargin) -> Unit
+ block: (View, WindowInsetsCompat, InitialPadding, InitialMargin) -> Unit
) {
// Create a snapshot of the view's padding & margin states
val initialPadding = recordInitialPaddingForView(this)
val initialMargin = recordInitialMarginForView(this)
// Set an actual OnApplyWindowInsetsListener which proxies to the given
// lambda, also passing in the original padding & margin states
- setOnApplyWindowInsetsListener { v, insets ->
+ ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
block(v, insets, initialPadding, initialMargin)
// Always return the insets, so that children can also use them
insets
diff --git a/Owl/app/src/main/res/layout/activity_main.xml b/Owl/app/src/main/res/layout/activity_main.xml
index eacd808d..43c9d1f7 100644
--- a/Owl/app/src/main/res/layout/activity_main.xml
+++ b/Owl/app/src/main/res/layout/activity_main.xml
@@ -39,9 +39,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
- app:paddingLeftSystemWindowInsets="@{true}"
- app:paddingRightSystemWindowInsets="@{true}"
- app:paddingBottomSystemWindowInsets="@{true}"
app:menu="@menu/main"/>
diff --git a/Owl/app/src/main/res/layout/fragment_lessons_sheet.xml b/Owl/app/src/main/res/layout/fragment_lessons_sheet.xml
index 59c786bc..2551a80a 100644
--- a/Owl/app/src/main/res/layout/fragment_lessons_sheet.xml
+++ b/Owl/app/src/main/res/layout/fragment_lessons_sheet.xml
@@ -34,7 +34,7 @@
android:id="@+id/lessons_sheet"
android:layout_height="match_parent"
android:layout_width="match_parent"
- app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
+ app:layout_behavior="com.materialstudies.owl.ui.lessons.LessonBottomSheetBehavior"
app:behavior_peekHeight="56dp">
+
+The apps in this repository are Android implementations of fictional [Material Studies](https://material.io/design/material-studies/). Each one is built using the [MDC-Android library](http://github.com/material-components/material-components-android/) and showcases customizations of color, typography, and shape made with [Material Theming](https://material.io/design/material-theming/).
+
+* [Reply](Reply)
+* [Owl](Owl)
+* [Coming Soon] Basil
+
+## License
+
+```
+Copyright 2019 Google, Inc.
+
+Licensed to the Apache Software Foundation (ASF) under one or more contributor
+license agreements. See the NOTICE file distributed with this work for
+additional information regarding copyright ownership. The ASF licenses this
+file to you 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.
+```
diff --git a/Reply/.google/packaging.yaml b/Reply/.google/packaging.yaml
new file mode 100644
index 00000000..08ccc281
--- /dev/null
+++ b/Reply/.google/packaging.yaml
@@ -0,0 +1,26 @@
+# Copyright 2020 The Android Open Source Project
+#
+# 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.
+#
+# This file is used by Google as part of our samples packaging process.
+# End users may safely ignore this file. It has no relevance to other systems.
+---
+status: PUBLISHED
+technologies: [Android]
+categories: [Material Components]
+languages: [Kotlin]
+solutions: [Mobile]
+github: material-components/material-components-android-examples
+branch: develop
+level: ADVANCED
+license: apache2
diff --git a/Reply/README.md b/Reply/README.md
index dce7b68c..9b5bcd5f 100644
--- a/Reply/README.md
+++ b/Reply/README.md
@@ -2,8 +2,7 @@
## Introduction
-Reply is an email app that uses Material Design components and Material Theming to create an on-brand communication experience.
-This project is the Android implementation of [Reply](https://material.io/design/material-studies/reply.html), a Material Study meant to showcase componentry and Theming using the Material Components for Android library.
+This project is the Android implementation of [Reply](https://material.io/design/material-studies/reply.html), a Material Study showcasing the possibilities of using Material Theming and Material Components for Android.
## Screenshots
@@ -11,23 +10,23 @@ This project is the Android implementation of [Reply](https://material.io/design
## Material Theming
-Reply uses Material Theming to create a content-forward experience through the customization of [color](https://material.io/develop/android/theming/color/), [typography](https://material.io/develop/android/theming/typography/) and [shape](https://material.io/develop/android/theming/shape/).
+Reply uses Material Theming to customize the app’s [color](https://material.io/develop/android/theming/color/), [typography](https://material.io/develop/android/theming/typography/) and [shape](https://material.io/develop/android/theming/shape/).
### Color
-Reply uses a simple, subtle color scheme to save emphasis for email content. Reply’s color palette is defined in [color.xml](https://github.com/material-components/material-components-android-examples/blob/develop/Reply/app/src/main/res/values/color.xml) and applied globally via the app’s [light](https://github.com/material-components/material-components-android-examples/blob/develop/Reply/app/src/main/res/values/themes.xml#L23-L37) and [dark](https://github.com/material-components/material-components-android-examples/blob/develop/Reply/app/src/main/res/values-night/themes.xml#L22-L36) themes.
+Reply uses a simple, subtle color scheme to save emphasis for email content. Reply’s color palette is defined in [color.xml](Reply/app/src/main/res/values/color.xml) and applied globally via the app’s [default](Reply/app/src/main/res/values/themes.xml#L23-L37) and [dark](Reply/app/src/main/res/values-night/themes.xml#L22-L36) themes.
### Typography
Reply uses [Work Sans](https://fonts.google.com/specimen/Work+Sans)
- as its typeface. All items in the type scale provide the typographic variety necessary for Reply’s content. See [type.xml](https://github.com/material-components/material-components-android-examples/blob/develop/Reply/app/src/main/res/values/type.xml) which defines `TextAppearance`s which are then [set in the theme](https://github.com/material-components/material-components-android-examples/blob/develop/Reply/app/src/main/res/values/themes.xml#L48-L59) and referred to using `?attr/textAppearance[...]` throughout.
+ as its typeface. All items in the type scale provide the typographic variety necessary for Reply’s content. See [type.xml](Reply/app/src/main/res/values/type.xml) which defines `TextAppearance`s which are then [set in the theme](Reply/app/src/main/res/values/themes.xml#L48-L59) and referred to using `?attr/textAppearance[...]` throughout.
### Shape
-Reply defines small, medium and large shape categories for different sized components. See [shape.xml](https://github.com/material-components/material-components-android-examples/blob/develop/Reply/app/src/main/res/values/shape.xml) which defines the `ShapeAppearance`s, which are then [set in the theme](https://github.com/material-components/material-components-android-examples/blob/develop/Reply/app/src/main/res/values/themes.xml#L43-L45) and picked up by all components or referred to directly.
+Reply defines small, medium, and large shape categories for different sized components. See [shape.xml](Reply/app/src/main/res/values/shape.xml) which defines the `ShapeAppearance`s, which are then [set in the theme](Reply/app/src/main/res/values/themes.xml#L43-L45) and picked up by all components or referred to directly.
## License
@@ -48,4 +47,4 @@ 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.
-```
\ No newline at end of file
+```
diff --git a/Reply/app/build.gradle b/Reply/app/build.gradle
index 401e916f..027515bd 100644
--- a/Reply/app/build.gradle
+++ b/Reply/app/build.gradle
@@ -18,11 +18,11 @@ apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
android {
- compileSdkVersion 29
+ compileSdkVersion 30
defaultConfig {
applicationId "com.materialstudies.reply"
minSdkVersion 23
- targetSdkVersion 29
+ targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -46,29 +46,26 @@ android {
}
dependencies {
- implementation fileTree(dir: 'libs', include: ['*.jar'])
-
// Kotlin
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
// AndroidX
- implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta2'
- implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'androidx.recyclerview:recyclerview:1.1.0-beta05'
- implementation 'androidx.core:core-ktx:1.1.0'
- implementation 'androidx.lifecycle:lifecycle-livedata-core-ktx:2.2.0-beta01'
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'androidx.core:core-ktx:1.3.2'
+ implementation 'androidx.lifecycle:lifecycle-livedata-core-ktx:2.2.0'
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
// Glide
- implementation "com.github.bumptech.glide:glide:4.9.0"
+ implementation "com.github.bumptech.glide:glide:4.11.0"
// Material Components
- implementation 'com.google.android.material:material:1.1.0-beta01'
+ implementation 'com.google.android.material:material:1.2.1'
// Testing
- testImplementation 'junit:junit:4.12'
- androidTestImplementation 'androidx.test:runner:1.2.0'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ testImplementation 'junit:junit:4.13.1'
+ androidTestImplementation 'androidx.test:runner:1.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/data/Account.kt b/Reply/app/src/main/java/com/materialstudies/reply/data/Account.kt
index f5d2def0..e7405f48 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/data/Account.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/data/Account.kt
@@ -30,6 +30,7 @@ data class Account(
val firstName: String,
val lastName: String,
val email: String,
+ val altEmail: String,
@DrawableRes val avatar: Int,
var isCurrentAccount: Boolean = false
) {
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/data/AccountStore.kt b/Reply/app/src/main/java/com/materialstudies/reply/data/AccountStore.kt
index a38fd194..8da74366 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/data/AccountStore.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/data/AccountStore.kt
@@ -33,6 +33,7 @@ object AccountStore {
"Jeff",
"Hansen",
"hikingfan@gmail.com",
+ "hkngfan@outside.com",
R.drawable.avatar_10,
true
),
@@ -42,6 +43,7 @@ object AccountStore {
"Jeff",
"H",
"jeffersonloveshiking@gmail.com",
+ "jeffersonloveshiking@work.com",
R.drawable.avatar_2
),
Account(
@@ -50,6 +52,7 @@ object AccountStore {
"Jeff",
"Hansen",
"jeffersonc@google.com",
+ "jeffersonc@gmail.com",
R.drawable.avatar_9
)
)
@@ -61,6 +64,7 @@ object AccountStore {
"Tracy",
"Alvarez",
"tracealvie@gmail.com",
+ "tracealvie@gravity.com",
R.drawable.avatar_1
),
Account(
@@ -69,6 +73,7 @@ object AccountStore {
"Allison",
"Trabucco",
"atrabucco222@gmail.com",
+ "atrabucco222@work.com",
R.drawable.avatar_3
),
Account(
@@ -77,6 +82,7 @@ object AccountStore {
"Ali",
"Connors",
"aliconnors@gmail.com",
+ "aliconnors@android.com",
R.drawable.avatar_5
),
Account(
@@ -85,6 +91,7 @@ object AccountStore {
"Alberto",
"Williams",
"albertowilliams124@gmail.com",
+ "albertowilliams124@chromeos.com",
R.drawable.avatar_0
),
Account(
@@ -93,6 +100,7 @@ object AccountStore {
"Kim",
"Alen",
"alen13@gmail.com",
+ "alen13@mountainview.gov",
R.drawable.avatar_7
),
Account(
@@ -101,6 +109,7 @@ object AccountStore {
"Google",
"Express",
"express@google.com",
+ "express@gmail.com",
R.drawable.avatar_express
),
Account(
@@ -109,6 +118,7 @@ object AccountStore {
"Sandra",
"Adams",
"sandraadams@gmail.com",
+ "sandraadams@textera.com",
R.drawable.avatar_2
),
Account(
@@ -117,6 +127,7 @@ object AccountStore {
"Trevor",
"Hansen",
"trevorhandsen@gmail.com",
+ "trevorhandsen@express.com",
R.drawable.avatar_8
),
Account(
@@ -125,6 +136,7 @@ object AccountStore {
"Sean",
"Holt",
"sholt@gmail.com",
+ "sholt@art.com",
R.drawable.avatar_6
),
Account(
@@ -133,6 +145,7 @@ object AccountStore {
"Frank",
"Hawkins",
"fhawkank@gmail.com",
+ "fhawkank@thisisme.com",
R.drawable.avatar_4
)
)
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/data/Email.kt b/Reply/app/src/main/java/com/materialstudies/reply/data/Email.kt
index 2b680fed..f5a8875d 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/data/Email.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/data/Email.kt
@@ -17,6 +17,7 @@
package com.materialstudies.reply.data
import androidx.recyclerview.widget.DiffUtil
+import com.materialstudies.reply.ui.home.Mailbox
/**
* A simple data class to represent an Email.
@@ -29,7 +30,8 @@ data class Email(
val body: String = "",
val attachments: List = emptyList(),
var isImportant: Boolean = false,
- var isStarred: Boolean = false
+ var isStarred: Boolean = false,
+ var mailbox: Mailbox = Mailbox.INBOX
) {
val senderPreview: String = "${sender.fullName} - 4 hrs ago"
val hasBody: Boolean = body.isNotBlank()
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/data/EmailStore.kt b/Reply/app/src/main/java/com/materialstudies/reply/data/EmailStore.kt
index 90cc9347..407e31d3 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/data/EmailStore.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/data/EmailStore.kt
@@ -18,7 +18,9 @@ package com.materialstudies.reply.data
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
import com.materialstudies.reply.R
+import com.materialstudies.reply.ui.home.Mailbox
/**
* A static data store of [Email]s.
@@ -80,7 +82,8 @@ object EmailStore {
I was at the grocery store on Sunday night.. when I ran into Genie Williams! I almost didn't recognize her afer 20 years!
Anyway, it turns out she is on the organizing committee for the high school reunion this fall. I don't know if you were planning on going or not, but she could definitely use our help in trying to track down lots of missing alums. If you can make it, we're doing a little phone-tree party at her place next Saturday, hoping that if we can find one person, thee more will...
- """.trimIndent()
+ """.trimIndent(),
+ mailbox = Mailbox.SENT
),
Email(
4L,
@@ -113,7 +116,8 @@ object EmailStore {
listOf(AccountStore.getDefaultUserAccount()),
"Recipe to try",
"Raspberry Pie: We should make this pie recipe tonight! The filling is " +
- "very quick to put together."
+ "very quick to put together.",
+ mailbox = Mailbox.SENT
),
Email(
7L,
@@ -121,17 +125,102 @@ object EmailStore {
listOf(AccountStore.getDefaultUserAccount()),
"Delivered",
"Your shoes should be waiting for you at home!"
+ ),
+ Email(
+ 8L,
+ AccountStore.getContactAccountById(13L),
+ listOf(AccountStore.getDefaultUserAccount()),
+ "Your update on Google Play Store is live!",
+ """
+ Your update, 0.1.1, is now live on the Play Store and available for your alpha users to start testing.
+
+ Your alpha testers will be automatically notified. If you'd rather send them a link directly, go to your Google Play Console and follow the instructions for obtaining an open alpha testing link.
+ """.trimIndent(),
+ mailbox = Mailbox.TRASH
+ ),
+ Email(
+ 9L,
+ AccountStore.getContactAccountById(10L),
+ listOf(AccountStore.getDefaultUserAccount()),
+ "(No subject)",
+ """
+ Hey,
+
+ Wanted to email and see what you thought of
+ """.trimIndent(),
+ mailbox = Mailbox.DRAFTS
+ ),
+ Email(
+ 10L,
+ AccountStore.getContactAccountById(5L),
+ listOf(AccountStore.getDefaultUserAccount()),
+ "Try a free TrailGo account",
+ """
+ Looking for the best hiking trails in your area? TrailGo gets you on the path to the outdoors faster than you can pack a sandwich.
+
+ Whether you're an experienced hiker or just looking to get outside for the afternoon, there's a segment that suits you.
+ """.trimIndent(),
+ mailbox = Mailbox.TRASH
+ ),
+ Email(
+ 10L,
+ AccountStore.getContactAccountById(5L),
+ listOf(AccountStore.getDefaultUserAccount()),
+ "Free money",
+ """
+ You've been selected as a winner in our latest raffle! To claim your prize, click on the link.
+ """.trimIndent(),
+ mailbox = Mailbox.SPAM
)
)
private val _emails: MutableLiveData> = MutableLiveData()
- val emails: LiveData>
- get() = _emails
+
+ private val inbox: LiveData>
+ get() = Transformations.map(_emails) { emails ->
+ emails.filter { it.mailbox == Mailbox.INBOX }
+ }
+
+ private val starred: LiveData>
+ get() = Transformations.map(_emails) { emails ->
+ emails.filter { it.isStarred }
+ }
+
+ private val sent: LiveData>
+ get() = Transformations.map(_emails) { emails ->
+ emails.filter { it.mailbox == Mailbox.SENT }
+ }
+
+ private val trash: LiveData>
+ get() = Transformations.map(_emails) { emails ->
+ emails.filter { it.mailbox == Mailbox.TRASH }
+ }
+
+ private val spam: LiveData>
+ get() = Transformations.map(_emails) { emails ->
+ emails.filter { it.mailbox == Mailbox.SPAM }
+ }
+
+ private val drafts: LiveData>
+ get() = Transformations.map(_emails) { emails ->
+ emails.filter { it.mailbox == Mailbox.DRAFTS }
+ }
init {
_emails.value = allEmails
}
+ fun getEmails(mailbox: Mailbox): LiveData> {
+ return when (mailbox) {
+ Mailbox.INBOX -> inbox
+ Mailbox.STARRED -> starred
+ Mailbox.SENT -> sent
+ Mailbox.TRASH -> trash
+ Mailbox.SPAM -> spam
+ Mailbox.DRAFTS -> drafts
+ }
+ }
+
/**
* Get an [Email] with the given [id].
*/
@@ -168,8 +257,7 @@ object EmailStore {
* Delete the [Email] with the given [id].
*/
fun delete(id: Long) {
- allEmails.removeAll { it.id == id }
- _emails.value = allEmails
+ update(id) { mailbox = Mailbox.TRASH }
}
/**
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/data/SearchSuggestion.kt b/Reply/app/src/main/java/com/materialstudies/reply/data/SearchSuggestion.kt
new file mode 100644
index 00000000..ed447dd9
--- /dev/null
+++ b/Reply/app/src/main/java/com/materialstudies/reply/data/SearchSuggestion.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+ *
+ * https://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 com.materialstudies.reply.data
+
+import androidx.annotation.DrawableRes
+
+/**
+ * An object which represents a search suggestion.
+ */
+data class SearchSuggestion(
+ @DrawableRes val iconResId: Int,
+ val title: String,
+ val subtitle: String
+)
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/data/SearchSuggestionStore.kt b/Reply/app/src/main/java/com/materialstudies/reply/data/SearchSuggestionStore.kt
new file mode 100644
index 00000000..b97ff1a8
--- /dev/null
+++ b/Reply/app/src/main/java/com/materialstudies/reply/data/SearchSuggestionStore.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+ *
+ * https://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 com.materialstudies.reply.data
+
+import com.materialstudies.reply.R
+
+/**
+ * A static data store of [SearchSuggestion]s.
+ */
+object SearchSuggestionStore {
+
+ val YESTERDAY_SUGGESTIONS = listOf(
+ SearchSuggestion(
+ R.drawable.ic_schedule,
+ "481 Van Brunt Street",
+ "Brooklyn, NY"
+ ),
+ SearchSuggestion(
+ R.drawable.ic_home,
+ "Home",
+ "199 Pacific Street, Brooklyn, NY"
+ )
+ )
+
+ val THIS_WEEK_SUGGESTIONS = listOf(
+ SearchSuggestion(
+ R.drawable.ic_schedule,
+ "BEP GA",
+ "Forsyth Street, New York, NY"
+ ),
+ SearchSuggestion(
+ R.drawable.ic_schedule,
+ "Sushi Nakazawa",
+ "Commerce Street, New York, NY"
+ ),
+ SearchSuggestion(
+ R.drawable.ic_schedule,
+ "IFC Center",
+ "6th Avenue, New York, NY"
+ )
+ )
+}
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/MainActivity.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/MainActivity.kt
index 09090b31..e8b8df7b 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/MainActivity.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/MainActivity.kt
@@ -16,33 +16,46 @@
package com.materialstudies.reply.ui
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import androidx.annotation.MenuRes
+import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.Toolbar
-import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.findNavController
+import com.google.android.material.transition.MaterialElevationScale
+import com.google.android.material.transition.MaterialFadeThrough
+import com.google.android.material.transition.MaterialSharedAxis
import com.materialstudies.reply.R
+import com.materialstudies.reply.data.EmailStore
import com.materialstudies.reply.databinding.ActivityMainBinding
import com.materialstudies.reply.ui.compose.ComposeFragmentDirections
import com.materialstudies.reply.ui.email.EmailFragmentArgs
+import com.materialstudies.reply.ui.home.HomeFragmentDirections
+import com.materialstudies.reply.ui.home.Mailbox
import com.materialstudies.reply.ui.nav.AlphaSlideAction
import com.materialstudies.reply.ui.nav.BottomNavDrawerFragment
import com.materialstudies.reply.ui.nav.ChangeSettingsMenuStateAction
import com.materialstudies.reply.ui.nav.HalfClockwiseRotateSlideAction
import com.materialstudies.reply.ui.nav.HalfCounterClockwiseRotateSlideAction
+import com.materialstudies.reply.ui.nav.NavigationAdapter
+import com.materialstudies.reply.ui.nav.NavigationModelItem
import com.materialstudies.reply.ui.nav.ShowHideFabStateAction
+import com.materialstudies.reply.ui.search.SearchFragmentDirections
import com.materialstudies.reply.util.contentView
import kotlin.LazyThreadSafetyMode.NONE
class MainActivity : AppCompatActivity(),
- Toolbar.OnMenuItemClickListener,
- NavController.OnDestinationChangedListener {
+ Toolbar.OnMenuItemClickListener,
+ NavController.OnDestinationChangedListener,
+ NavigationAdapter.NavigationAdapterListener {
private val binding: ActivityMainBinding by contentView(R.layout.activity_main)
private val bottomNavDrawer: BottomNavDrawerFragment by lazy(NONE) {
@@ -53,6 +66,12 @@ class MainActivity : AppCompatActivity(),
// to ComposeFragment when this Activity's FAB is clicked.
private var currentEmailId = -1L
+ val currentNavigationFragment: Fragment?
+ get() = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
+ ?.childFragmentManager
+ ?.fragments
+ ?.first()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpBottomNavigationAndFab()
@@ -72,8 +91,7 @@ class MainActivity : AppCompatActivity(),
setShowMotionSpecResource(R.animator.fab_show)
setHideMotionSpecResource(R.animator.fab_hide)
setOnClickListener {
- findNavController(R.id.nav_host_fragment)
- .navigate(ComposeFragmentDirections.actionGlobalComposeFragment(currentEmailId))
+ navigateToCompose()
}
}
@@ -92,6 +110,7 @@ class MainActivity : AppCompatActivity(),
})
addOnSandwichSlideAction(HalfCounterClockwiseRotateSlideAction(binding.bottomAppBarChevron))
+ addNavigationListener(this@MainActivity)
}
// Set up the BottomAppBar menu
@@ -131,6 +150,10 @@ class MainActivity : AppCompatActivity(),
currentEmailId = -1
setBottomAppBarForCompose()
}
+ R.id.searchFragment -> {
+ currentEmailId = -1
+ setBottomAppBarForSearch()
+ }
}
}
@@ -177,29 +200,104 @@ class MainActivity : AppCompatActivity(),
}
private fun setBottomAppBarForCompose() {
+ hideBottomAppBar()
+ }
+
+ private fun setBottomAppBarForSearch() {
+ hideBottomAppBar()
+ binding.fab.hide()
+ }
+
+ private fun hideBottomAppBar() {
binding.run {
bottomAppBar.performHide()
- fab.hide()
- // Hide the BottomAppBar to avoid it showing above the keyboard
- // when composing a new email.
- bottomAppBar.visibility = View.GONE
+ // Get a handle on the animator that hides the bottom app bar so we can wait to hide
+ // the fab and bottom app bar until after it's exit animation finishes.
+ bottomAppBar.animate().setListener(object : AnimatorListenerAdapter() {
+ var isCanceled = false
+ override fun onAnimationEnd(animation: Animator?) {
+ if (isCanceled) return
+
+ // Hide the BottomAppBar to avoid it showing above the keyboard
+ // when composing a new email.
+ bottomAppBar.visibility = View.GONE
+ fab.visibility = View.INVISIBLE
+ }
+ override fun onAnimationCancel(animation: Animator?) {
+ isCanceled = true
+ }
+ })
}
}
+ override fun onNavMenuItemClicked(item: NavigationModelItem.NavMenuItem) {
+ // Swap the list of emails for the given mailbox
+ navigateToHome(item.titleRes, item.mailbox)
+ }
+
+ override fun onNavEmailFolderClicked(folder: NavigationModelItem.NavEmailFolder) {
+ // Do nothing
+ }
+
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_settings -> {
bottomNavDrawer.close()
showDarkThemeMenu()
}
+ R.id.menu_search -> navigateToSearch()
+ R.id.menu_email_star -> {
+ EmailStore.update(currentEmailId) { isStarred = !isStarred }
+ }
+ R.id.menu_email_delete -> {
+ EmailStore.delete(currentEmailId)
+ findNavController(R.id.nav_host_fragment).popBackStack()
+ }
}
return true
}
private fun showDarkThemeMenu() {
- MenuBottomSheetDialogFragment(R.menu.dark_theme_bottom_sheet_menu) {
- onDarkThemeMenuItemSelected(it.itemId)
- }.show(supportFragmentManager, null)
+ MenuBottomSheetDialogFragment
+ .newInstance(R.menu.dark_theme_bottom_sheet_menu)
+ .show(supportFragmentManager, null)
+ }
+
+ fun navigateToHome(@StringRes titleRes: Int, mailbox: Mailbox) {
+ binding.bottomAppBarTitle.text = getString(titleRes)
+ currentNavigationFragment?.apply {
+ exitTransition = MaterialFadeThrough().apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ }
+ val directions = HomeFragmentDirections.actionGlobalHomeFragment(mailbox)
+ findNavController(R.id.nav_host_fragment).navigate(directions)
+ }
+
+ private fun navigateToCompose() {
+ currentNavigationFragment?.apply {
+ exitTransition = MaterialElevationScale(false).apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ reenterTransition = MaterialElevationScale(true).apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ }
+ val directions = ComposeFragmentDirections.actionGlobalComposeFragment(currentEmailId)
+ findNavController(R.id.nav_host_fragment).navigate(directions)
+ }
+
+ private fun navigateToSearch() {
+ currentNavigationFragment?.apply {
+ exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true).apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false).apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ }
+ val directions = SearchFragmentDirections.actionGlobalSearchFragment()
+ findNavController(R.id.nav_host_fragment).navigate(directions)
}
/**
@@ -217,4 +315,5 @@ class MainActivity : AppCompatActivity(),
delegate.localNightMode = nightMode
return true
}
+
}
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/MenuBottomSheetDialogFragment.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/MenuBottomSheetDialogFragment.kt
index eb475161..b3b63ec2 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/MenuBottomSheetDialogFragment.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/MenuBottomSheetDialogFragment.kt
@@ -18,19 +18,25 @@ package com.materialstudies.reply.ui
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import androidx.annotation.MenuRes
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.navigation.NavigationView
import com.materialstudies.reply.R
-class MenuBottomSheetDialogFragment(
- private val menuRes: Int,
- private val onNavigationItemSelected: (MenuItem) -> Boolean
-) : BottomSheetDialogFragment() {
+/**
+ * A bottom sheet dialog for displaying a simple list of action items.
+ */
+class MenuBottomSheetDialogFragment : BottomSheetDialogFragment() {
private lateinit var navigationView: NavigationView
+ @MenuRes private var menuResId: Int = 0
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ menuResId = arguments?.getInt(KEY_MENU_RES_ID, 0) ?: 0
+ }
override fun onCreateView(
inflater: LayoutInflater,
@@ -47,11 +53,24 @@ class MenuBottomSheetDialogFragment(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navigationView = view.findViewById(R.id.navigation_view)
- navigationView.inflateMenu(menuRes)
+ navigationView.inflateMenu(menuResId)
navigationView.setNavigationItemSelectedListener {
- val consumed = onNavigationItemSelected(it)
- if (consumed) dismiss()
- consumed
+ dismiss()
+ true
+ }
+ }
+
+ companion object {
+
+ private const val KEY_MENU_RES_ID = "MenuBottomSheetDialogFragment_menuResId"
+
+ fun newInstance(@MenuRes menuResId: Int): MenuBottomSheetDialogFragment {
+ val fragment = MenuBottomSheetDialogFragment()
+ val bundle = Bundle().apply {
+ putInt(KEY_MENU_RES_ID, menuResId)
+ }
+ fragment.arguments = bundle
+ return fragment
}
}
}
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/compose/ComposeFragment.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/compose/ComposeFragment.kt
index 52b6793d..1998335c 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/compose/ComposeFragment.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/compose/ComposeFragment.kt
@@ -16,16 +16,19 @@
package com.materialstudies.reply.ui.compose
+import android.graphics.Color
import android.os.Bundle
-import android.transition.Slide
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.view.animation.AccelerateInterpolator
import android.widget.ArrayAdapter
+import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
+import androidx.transition.Slide
+import androidx.transition.TransitionManager
+import com.google.android.material.transition.MaterialContainerTransform
import com.materialstudies.reply.R
import com.materialstudies.reply.data.Account
import com.materialstudies.reply.data.AccountStore
@@ -33,8 +36,7 @@ import com.materialstudies.reply.data.Email
import com.materialstudies.reply.data.EmailStore
import com.materialstudies.reply.databinding.ComposeRecipientChipBinding
import com.materialstudies.reply.databinding.FragmentComposeBinding
-import com.materialstudies.reply.util.themeInterpolator
-import com.materialstudies.reply.util.transition.MaterialContainerTransition
+import com.materialstudies.reply.util.themeColor
import kotlin.LazyThreadSafetyMode.NONE
/**
@@ -54,9 +56,17 @@ class ComposeFragment : Fragment() {
if (id == -1L) EmailStore.create() else EmailStore.createReplyTo(id)
}
+ // Handle closing an expanded recipient card when on back is pressed.
+ private val closeRecipientCardOnBackPressed = object : OnBackPressedCallback(false) {
+ var expandedChip: View? = null
+ override fun handleOnBackPressed() {
+ expandedChip?.let { collapseChip(it) }
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- prepareTransitions()
+ requireActivity().onBackPressedDispatcher.addCallback(this, closeRecipientCardOnBackPressed)
}
override fun onCreateView(
@@ -80,13 +90,31 @@ class ComposeFragment : Fragment() {
R.layout.spinner_item_layout,
AccountStore.getAllUserAccounts().map { it.email }
)
- }
- startTransitions()
+ // Set transitions here so we are able to access Fragment's binding views.
+ enterTransition = MaterialContainerTransform().apply {
+ // Manually add the Views to be shared since this is not a standard Fragment to
+ // Fragment shared element transition.
+ startView = requireActivity().findViewById(R.id.fab)
+ endView = emailCardView
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ scrimColor = Color.TRANSPARENT
+ containerColor = requireContext().themeColor(R.attr.colorSurface)
+ startContainerColor = requireContext().themeColor(R.attr.colorSecondary)
+ endContainerColor = requireContext().themeColor(R.attr.colorSurface)
+ }
+ returnTransition = Slide().apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_medium).toLong()
+ addTarget(R.id.email_card_view)
+ }
+ }
}
/**
* Add a chip for the given [Account] to the recipients chip group.
+ *
+ * This method also sets up the ability for expanding/collapsing the chip into a recipient
+ * address selection dialog.
*/
private fun addRecipientChip(acnt: Account) {
binding.recipientChipGroup.run {
@@ -96,35 +124,73 @@ class ComposeFragment : Fragment() {
false
).apply {
account = acnt
+ root.setOnClickListener {
+ // Bind the views in the expanded card view to this account's details when
+ // clicked and expand.
+ binding.focusedRecipient = acnt
+ expandChip(it)
+ }
}
addView(chipBinding.root)
}
}
- private fun prepareTransitions() {
- postponeEnterTransition()
- }
+ /**
+ * Expand the recipient [chip] into a popup with a list of contact addresses to choose from.
+ */
+ private fun expandChip(chip: View) {
+ // Configure the analogous collapse transform back to the recipient chip. This should
+ // happen when the card is clicked, any region outside of the card (the card's transparent
+ // scrim) is clicked, or when the back button is pressed.
+ binding.run {
+ recipientCardView.setOnClickListener { collapseChip(chip) }
+ recipientCardScrim.visibility = View.VISIBLE
+ recipientCardScrim.setOnClickListener { collapseChip(chip) }
+ }
+ closeRecipientCardOnBackPressed.expandedChip = chip
+ closeRecipientCardOnBackPressed.isEnabled = true
- private fun startTransitions() {
- binding.executePendingBindings()
- // Delay creating the enterTransition until after we have inflated this Fragment's binding
- // and are able to access the view to be transitioned to.
- enterTransition = MaterialContainerTransition(
- correctForZOrdering = true
- ).apply {
- // Manually add the Views to be shared since this is not a standard Fragment to Fragment
- // shared element transition.
- setSharedElementViews(
- requireActivity().findViewById(R.id.fab),
- binding.emailCardView
+ val transform = MaterialContainerTransform().apply {
+ startView = chip
+ endView = binding.recipientCardView
+ scrimColor = Color.TRANSPARENT
+ // Have the transform match the endView card's native elevation as closely as possible.
+ endElevation = requireContext().resources.getDimension(
+ R.dimen.email_recipient_card_popup_elevation_compat
)
- duration = resources.getInteger(R.integer.reply_motion_default_large).toLong()
- interpolator = requireContext().themeInterpolator(R.attr.motionInterpolatorPersistent)
+ // Avoid having this transform from running on both the start and end views by setting
+ // its target to the endView.
+ addTarget(binding.recipientCardView)
}
- returnTransition = Slide().apply {
- duration = resources.getInteger(R.integer.reply_motion_duration_medium).toLong()
- interpolator = requireContext().themeInterpolator(R.attr.motionInterpolatorOutgoing)
+
+ TransitionManager.beginDelayedTransition(binding.composeConstraintLayout, transform)
+ binding.recipientCardView.visibility = View.VISIBLE
+ // Using INVISIBLE instead of GONE ensures the chip's parent layout won't shift during
+ // the transition due to chips being effectively removed.
+ chip.visibility = View.INVISIBLE
+ }
+
+ /**
+ * Collapse the recipient card back into its [chip] form.
+ */
+ private fun collapseChip(chip: View) {
+ // Remove the scrim view and on back pressed callbacks
+ binding.recipientCardScrim.visibility = View.GONE
+ closeRecipientCardOnBackPressed.expandedChip = null
+ closeRecipientCardOnBackPressed.isEnabled = false
+
+ val transform = MaterialContainerTransform().apply {
+ startView = binding.recipientCardView
+ endView = chip
+ scrimColor = Color.TRANSPARENT
+ startElevation = requireContext().resources.getDimension(
+ R.dimen.email_recipient_card_popup_elevation_compat
+ )
+ addTarget(chip)
}
- startPostponedEnterTransition()
+
+ TransitionManager.beginDelayedTransition(binding.composeConstraintLayout, transform)
+ chip.visibility = View.VISIBLE
+ binding.recipientCardView.visibility = View.INVISIBLE
}
}
\ No newline at end of file
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/email/EmailFragment.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/email/EmailFragment.kt
index cdcb2ee6..0fdf60bd 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/email/EmailFragment.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/email/EmailFragment.kt
@@ -16,6 +16,7 @@
package com.materialstudies.reply.ui.email
+import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -24,11 +25,11 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialContainerTransform
import com.materialstudies.reply.R
import com.materialstudies.reply.data.EmailStore
import com.materialstudies.reply.databinding.FragmentEmailBinding
-import com.materialstudies.reply.util.themeInterpolator
-import com.materialstudies.reply.util.transition.MaterialContainerTransition
+import com.materialstudies.reply.util.themeColor
import kotlin.LazyThreadSafetyMode.NONE
private const val MAX_GRID_SPANS = 3
@@ -46,7 +47,15 @@ class EmailFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- prepareTransitions()
+
+ sharedElementEnterTransition = MaterialContainerTransform().apply {
+ // Scope the transition to a view in the hierarchy so we know it will be added under
+ // the bottom app bar but over the elevation scale of the exiting HomeFragment.
+ drawingViewId = R.id.nav_host_fragment
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ scrimColor = Color.TRANSPARENT
+ setAllContainerColors(requireContext().themeColor(R.attr.colorSurface))
+ }
}
override fun onCreateView(
@@ -84,32 +93,6 @@ class EmailFragment : Fragment() {
attachmentRecyclerView.adapter = attachmentAdapter
attachmentAdapter.submitList(email.attachments)
}
-
- startTransitions()
- }
-
- private fun prepareTransitions() {
- postponeEnterTransition()
-
- sharedElementEnterTransition = MaterialContainerTransition(
- R.id.nested_scroll_view,
- correctForZOrdering = true
- ).apply {
- duration = resources.getInteger(R.integer.reply_motion_default_large).toLong()
- interpolator = requireContext().themeInterpolator(R.attr.motionInterpolatorPersistent)
- }
- sharedElementReturnTransition = MaterialContainerTransition(
- R.id.recycler_view,
- correctForZOrdering = true
- ).apply {
- duration = resources.getInteger(R.integer.reply_motion_default_large).toLong()
- interpolator = requireContext().themeInterpolator(R.attr.motionInterpolatorPersistent)
- }
- }
-
- private fun startTransitions() {
- binding.executePendingBindings()
- startPostponedEnterTransition()
}
private fun showError() {
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/home/EmailViewHolder.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/home/EmailViewHolder.kt
index 0d4324c3..b51e1d2f 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/home/EmailViewHolder.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/home/EmailViewHolder.kt
@@ -36,6 +36,9 @@ class EmailViewHolder(
= R.layout.email_attachment_preview_item_layout
}
+ private val starredCornerSize =
+ itemView.resources.getDimension(R.dimen.reply_small_component_corner_radius)
+
override val reboundableView: View = binding.cardView
init {
@@ -69,7 +72,7 @@ class EmailViewHolder(
// rounded or squared. Since all other corners are set to 0dp rounded, they are
// not affected.
val interpolation = if (email.isStarred) 1F else 0F
- binding.cardView.progress = interpolation
+ updateCardViewTopLeftCornerSize(interpolation)
binding.executePendingBindings()
}
@@ -89,7 +92,7 @@ class EmailViewHolder(
// Animate the top left corner radius of the email card as swipe happens.
val interpolation = (currentSwipePercentage / swipeThreshold).coerceIn(0F, 1F)
val adjustedInterpolation = abs((if (isStarred) 1F else 0F) - interpolation)
- binding.cardView.progress = adjustedInterpolation
+ updateCardViewTopLeftCornerSize(adjustedInterpolation)
// Start the background animation once the threshold is met.
val thresholdMet = currentSwipePercentage >= swipeThreshold
@@ -105,4 +108,16 @@ class EmailViewHolder(
val email = binding.email ?: return
binding.listener?.onEmailStarChanged(email, !email.isStarred)
}
+
+ // We have to update the shape appearance itself to have the MaterialContainerTransform pick up
+ // the correct shape appearance, since it doesn't have access to the MaterialShapeDrawable
+ // interpolation. If you don't need this work around, prefer using MaterialShapeDrawable's
+ // interpolation property, or in the case of MaterialCardView, the progress property.
+ private fun updateCardViewTopLeftCornerSize(interpolation: Float) {
+ binding.cardView.apply {
+ shapeAppearanceModel = shapeAppearanceModel.toBuilder()
+ .setTopLeftCornerSize(interpolation * starredCornerSize)
+ .build()
+ }
+ }
}
\ No newline at end of file
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/home/HomeFragment.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/home/HomeFragment.kt
index 8e06efec..a564f9b7 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/home/HomeFragment.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/home/HomeFragment.kt
@@ -20,27 +20,52 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
import androidx.lifecycle.observe
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.ItemTouchHelper
+import com.google.android.material.transition.MaterialElevationScale
+import com.google.android.material.transition.MaterialFadeThrough
import com.materialstudies.reply.R
import com.materialstudies.reply.data.Email
import com.materialstudies.reply.data.EmailStore
import com.materialstudies.reply.databinding.FragmentHomeBinding
+import com.materialstudies.reply.ui.MainActivity
import com.materialstudies.reply.ui.MenuBottomSheetDialogFragment
+import com.materialstudies.reply.ui.nav.NavigationModel
/**
* A [Fragment] that displays a list of emails.
*/
class HomeFragment : Fragment(), EmailAdapter.EmailAdapterListener {
+ private val args: HomeFragmentArgs by navArgs()
+
private lateinit var binding: FragmentHomeBinding
private val emailAdapter = EmailAdapter(this)
+ // An on back pressed callback that handles replacing any non-Inbox HomeFragment with inbox
+ // on back pressed.
+ private val nonInboxOnBackCallback = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ NavigationModel.setNavigationMenuItemChecked(NavigationModel.INBOX_ID)
+ (requireActivity() as MainActivity)
+ .navigateToHome(R.string.navigation_inbox, Mailbox.INBOX);
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialFadeThrough().apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ }
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -57,6 +82,14 @@ class HomeFragment : Fragment(), EmailAdapter.EmailAdapterListener {
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }
+ // Only enable the on back callback if this home fragment is a mailbox other than Inbox.
+ // This is to make sure we always navigate back to Inbox before exiting the app.
+ nonInboxOnBackCallback.isEnabled = args.mailbox != Mailbox.INBOX
+ requireActivity().onBackPressedDispatcher.addCallback(
+ viewLifecycleOwner,
+ nonInboxOnBackCallback
+ )
+
binding.recyclerView.apply {
val itemTouchHelper = ItemTouchHelper(ReboundingSwipeActionCallback())
itemTouchHelper.attachToRecyclerView(this)
@@ -64,23 +97,30 @@ class HomeFragment : Fragment(), EmailAdapter.EmailAdapterListener {
}
binding.recyclerView.adapter = emailAdapter
- EmailStore.emails.observe(this) {
+ EmailStore.getEmails(args.mailbox).observe(viewLifecycleOwner) {
emailAdapter.submitList(it)
}
}
override fun onEmailClicked(cardView: View, email: Email) {
- val extras = FragmentNavigatorExtras(cardView to cardView.transitionName)
+ // Set exit and reenter transitions here as opposed to in onCreate because these transitions
+ // will be set and overwritten on HomeFragment for other navigation actions.
+ exitTransition = MaterialElevationScale(false).apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ reenterTransition = MaterialElevationScale(true).apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ val emailCardDetailTransitionName = getString(R.string.email_card_detail_transition_name)
+ val extras = FragmentNavigatorExtras(cardView to emailCardDetailTransitionName)
val directions = HomeFragmentDirections.actionHomeFragmentToEmailFragment(email.id)
findNavController().navigate(directions, extras)
}
override fun onEmailLongPressed(email: Email): Boolean {
- MenuBottomSheetDialogFragment(R.menu.email_bottom_sheet_menu) {
- // Do nothing.
- true
- }.show(requireFragmentManager(), null)
-
+ MenuBottomSheetDialogFragment
+ .newInstance(R.menu.email_bottom_sheet_menu)
+ .show(parentFragmentManager, null)
return true
}
@@ -91,4 +131,4 @@ class HomeFragment : Fragment(), EmailAdapter.EmailAdapterListener {
override fun onEmailArchived(email: Email) {
EmailStore.delete(email.id)
}
-}
\ No newline at end of file
+}
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/home/Mailbox.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/home/Mailbox.kt
new file mode 100644
index 00000000..b1dbec7c
--- /dev/null
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/home/Mailbox.kt
@@ -0,0 +1,8 @@
+package com.materialstudies.reply.ui.home
+
+/**
+ * An enumeration of mailboxes into which emails can be placed.
+ */
+enum class Mailbox {
+ INBOX, STARRED, SENT, TRASH, SPAM, DRAFTS
+}
\ No newline at end of file
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/BottomNavDrawerFragment.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/BottomNavDrawerFragment.kt
index 354c5c91..7c1dfc0a 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/BottomNavDrawerFragment.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/BottomNavDrawerFragment.kt
@@ -83,6 +83,9 @@ class BottomNavDrawerFragment :
private val sandwichSlideActions = mutableListOf()
+ private val navigationListeners: MutableList =
+ mutableListOf()
+
private val backgroundShapeDrawable: MaterialShapeDrawable by lazy(NONE) {
val backgroundContext = binding.backgroundContainer.context
MaterialShapeDrawable(
@@ -220,14 +223,14 @@ class BottomNavDrawerFragment :
val adapter = NavigationAdapter(this@BottomNavDrawerFragment)
navRecyclerView.adapter = adapter
- NavigationModel.navigationList.observe(this@BottomNavDrawerFragment) {
+ NavigationModel.navigationList.observe(viewLifecycleOwner) {
adapter.submitList(it)
}
NavigationModel.setNavigationMenuItemChecked(0)
val accountAdapter = AccountAdapter(this@BottomNavDrawerFragment)
accountRecyclerView.adapter = accountAdapter
- AccountStore.userAccounts.observe(this@BottomNavDrawerFragment) {
+ AccountStore.userAccounts.observe(viewLifecycleOwner) {
accountAdapter.submitList(it)
currentUserAccount = it.first { acc -> acc.isCurrentAccount }
}
@@ -261,6 +264,10 @@ class BottomNavDrawerFragment :
bottomSheetCallback.addOnStateChangedAction(action)
}
+ fun addNavigationListener(listener: NavigationAdapter.NavigationAdapterListener) {
+ navigationListeners.add(listener)
+ }
+
/**
* Add actions to be run when the slide offset (animation progress) or the sandwiching account
* picker has changed.
@@ -270,11 +277,13 @@ class BottomNavDrawerFragment :
}
override fun onNavMenuItemClicked(item: NavigationModelItem.NavMenuItem) {
- if (NavigationModel.setNavigationMenuItemChecked(item.id)) close()
+ NavigationModel.setNavigationMenuItemChecked(item.id)
+ close()
+ navigationListeners.forEach { it.onNavMenuItemClicked(item) }
}
override fun onNavEmailFolderClicked(folder: NavigationModelItem.NavEmailFolder) {
- // Do nothing
+ navigationListeners.forEach { it.onNavEmailFolderClicked(folder) }
}
override fun onAccountClicked(account: Account) {
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/NavigationModel.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/NavigationModel.kt
index 95da22f1..16764ad2 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/NavigationModel.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/NavigationModel.kt
@@ -20,48 +20,62 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.materialstudies.reply.R
import com.materialstudies.reply.data.EmailStore
+import com.materialstudies.reply.ui.home.Mailbox
/**
* A class which maintains and generates a navigation list to be displayed by [NavigationAdapter].
*/
object NavigationModel {
+ const val INBOX_ID = 0
+ const val STARRED_ID = 1
+ const val SENT_ID = 2
+ const val TRASH_ID = 3
+ const val SPAM_ID = 4
+ const val DRAFTS_ID = 5
+
private var navigationMenuItems = mutableListOf(
NavigationModelItem.NavMenuItem(
- id = 0,
+ id = INBOX_ID,
icon = R.drawable.ic_twotone_inbox,
titleRes = R.string.navigation_inbox,
- checked = false
+ checked = false,
+ mailbox = Mailbox.INBOX
),
NavigationModelItem.NavMenuItem(
- id = 1,
+ id = STARRED_ID,
icon = R.drawable.ic_twotone_stars,
titleRes = R.string.navigation_starred,
- checked = false
+ checked = false,
+ mailbox = Mailbox.STARRED
),
NavigationModelItem.NavMenuItem(
- id = 2,
+ id = SENT_ID,
icon = R.drawable.ic_twotone_send,
titleRes = R.string.navigation_sent,
- checked = false
+ checked = false,
+ mailbox = Mailbox.SENT
),
NavigationModelItem.NavMenuItem(
- id = 3,
+ id = TRASH_ID,
icon = R.drawable.ic_twotone_delete,
titleRes = R.string.navigation_trash,
- checked = false
+ checked = false,
+ mailbox = Mailbox.TRASH
),
NavigationModelItem.NavMenuItem(
- id = 4,
+ id = SPAM_ID,
icon = R.drawable.ic_twotone_error,
titleRes = R.string.navigation_spam,
- checked = false
+ checked = false,
+ mailbox = Mailbox.SPAM
),
NavigationModelItem.NavMenuItem(
- id = 5,
+ id = DRAFTS_ID,
icon = R.drawable.ic_twotone_drafts,
titleRes = R.string.navigation_drafts,
- checked = false
+ checked = false,
+ mailbox = Mailbox.DRAFTS
)
)
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/NavigationModelItem.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/NavigationModelItem.kt
index 13aa0e51..245e8809 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/NavigationModelItem.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/nav/NavigationModelItem.kt
@@ -21,6 +21,7 @@ import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import com.materialstudies.reply.data.EmailFolder
import com.materialstudies.reply.data.EmailFolderDiff
+import com.materialstudies.reply.ui.home.Mailbox
/**
* A sealed class which encapsulates all objects [NavigationAdapter] is able to display.
@@ -34,12 +35,13 @@ sealed class NavigationModelItem {
val id: Int,
@DrawableRes val icon: Int,
@StringRes val titleRes: Int,
+ val mailbox: Mailbox,
var checked: Boolean
) : NavigationModelItem()
/**
* A class which is used to show a section divider (a subtitle and underline) between
- * sections of differen NavigationModelItem types.
+ * sections of different NavigationModelItem types.
*/
data class NavDivider(val title: String) : NavigationModelItem()
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/ui/search/SearchFragment.kt b/Reply/app/src/main/java/com/materialstudies/reply/ui/search/SearchFragment.kt
new file mode 100644
index 00000000..e06d9ace
--- /dev/null
+++ b/Reply/app/src/main/java/com/materialstudies/reply/ui/search/SearchFragment.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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
+ *
+ * https://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 com.materialstudies.reply.ui.search
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.transition.MaterialSharedAxis
+import com.materialstudies.reply.R
+import com.materialstudies.reply.data.SearchSuggestion
+import com.materialstudies.reply.data.SearchSuggestionStore
+import com.materialstudies.reply.databinding.FragmentSearchBinding
+import com.materialstudies.reply.databinding.SearchSuggestionItemBinding
+import com.materialstudies.reply.databinding.SearchSuggestionTitleBinding
+
+/**
+ * A [Fragment] that displays search.
+ */
+class SearchFragment : Fragment() {
+
+ private lateinit var binding: FragmentSearchBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true).apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false).apply {
+ duration = resources.getInteger(R.integer.reply_motion_duration_large).toLong()
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = FragmentSearchBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ binding.searchToolbar.setNavigationOnClickListener { findNavController().navigateUp() }
+ setUpSuggestions(binding.searchSuggestionContainer)
+ }
+
+ private fun setUpSuggestions(suggestionContainer: ViewGroup) {
+ addSuggestionTitleView(suggestionContainer, R.string.search_suggestion_title_yesterday)
+ addSuggestionItemViews(suggestionContainer, SearchSuggestionStore.YESTERDAY_SUGGESTIONS)
+ addSuggestionTitleView(suggestionContainer, R.string.search_suggestion_title_this_week)
+ addSuggestionItemViews(suggestionContainer, SearchSuggestionStore.THIS_WEEK_SUGGESTIONS)
+ }
+
+ private fun addSuggestionTitleView(parent: ViewGroup, @StringRes titleResId: Int) {
+ val inflater = LayoutInflater.from(parent.context)
+ val titleBinding = SearchSuggestionTitleBinding.inflate(inflater, parent, false)
+ titleBinding.title = titleResId
+ parent.addView(titleBinding.root)
+ }
+
+ private fun addSuggestionItemViews(parent: ViewGroup, suggestions: List) {
+ suggestions.forEach {
+ val inflater = LayoutInflater.from(parent.context)
+ val suggestionBinding = SearchSuggestionItemBinding.inflate(inflater, parent, false)
+ suggestionBinding.suggestion = it
+ parent.addView(suggestionBinding.root)
+ }
+ }
+}
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/util/AnimationUtils.kt b/Reply/app/src/main/java/com/materialstudies/reply/util/AnimationUtils.kt
index 1ea5b304..69ab20dc 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/util/AnimationUtils.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/util/AnimationUtils.kt
@@ -85,56 +85,6 @@ fun lerp(
return lerp(startValue, endValue, (fraction - startFraction) / (endFraction - startFraction))
}
-/**
- * Linearly interpolate between two [CornerRounding]s when the fraction is in a given range.
- */
-fun lerp(
- startValue: CornerRounding,
- endValue: CornerRounding,
- @FloatRange(
- from = 0.0,
- fromInclusive = true,
- to = 1.0,
- toInclusive = false
- ) startFraction: Float,
- @FloatRange(from = 0.0, fromInclusive = false, to = 1.0, toInclusive = true) endFraction: Float,
- @FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true) fraction: Float
-): CornerRounding {
- if (fraction < startFraction) return startValue
- if (fraction > endFraction) return endValue
-
- return CornerRounding(
- lerp(
- startValue.topLeftRadius,
- endValue.topLeftRadius,
- startFraction,
- endFraction,
- fraction
- ),
- lerp(
- startValue.topRightRadius,
- endValue.topRightRadius,
- startFraction,
- endFraction,
- fraction
- ),
- lerp(
- startValue.bottomRightRadius,
- endValue.bottomRightRadius,
- startFraction,
- endFraction,
- fraction
- ),
- lerp(
- startValue.bottomLeftRadius,
- endValue.bottomLeftRadius,
- startFraction,
- endFraction,
- fraction
- )
- )
-}
-
/**
* Linearly interpolate between two colors when the fraction is in a given range.
*/
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/util/GraphicsExtensions.kt b/Reply/app/src/main/java/com/materialstudies/reply/util/GraphicsExtensions.kt
deleted file mode 100644
index f2532f04..00000000
--- a/Reply/app/src/main/java/com/materialstudies/reply/util/GraphicsExtensions.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * 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
- *
- * https://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 com.materialstudies.reply.util
-
-import android.graphics.Canvas
-import android.graphics.Rect
-import android.graphics.RectF
-import androidx.annotation.FloatRange
-import com.google.android.material.shape.ShapeAppearanceModel
-import kotlin.math.roundToInt
-
-class CornerRounding(
- val topLeftRadius: Float = 0f,
- val topRightRadius: Float = 0f,
- val bottomRightRadius: Float = 0f,
- val bottomLeftRadius: Float = 0f
-)
-
-// To FloatArray suitable for Path#addRoundRect
-fun CornerRounding.toFloatArray(): FloatArray {
- return floatArrayOf(
- topLeftRadius, topLeftRadius,
- topRightRadius, topRightRadius,
- bottomRightRadius, bottomRightRadius,
- bottomLeftRadius, bottomLeftRadius
- )
-}
-
-fun ShapeAppearanceModel?.toCornerRounding(bounds: RectF): CornerRounding {
- if (this == null) return CornerRounding()
- return CornerRounding(
- topLeftCornerSize.getCornerSize(bounds),
- topRightCornerSize.getCornerSize(bounds),
- bottomRightCornerSize.getCornerSize(bounds),
- bottomLeftCornerSize.getCornerSize(bounds)
- )
-}
-
-private val boundsRectF = RectF()
-fun Canvas.withAlpha(
- bounds: Rect,
- @FloatRange(from = 0.0, fromInclusive = true, to = 1.0, toInclusive = true) alpha: Float,
- block: Canvas.() -> Unit
-) {
- boundsRectF.set(bounds)
- val checkpoint = saveLayerAlpha(boundsRectF, (alpha * 255).roundToInt())
- block()
- restoreToCount(checkpoint)
-}
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/util/ViewExtensions.kt b/Reply/app/src/main/java/com/materialstudies/reply/util/ViewExtensions.kt
index 5ee30c06..3e852372 100644
--- a/Reply/app/src/main/java/com/materialstudies/reply/util/ViewExtensions.kt
+++ b/Reply/app/src/main/java/com/materialstudies/reply/util/ViewExtensions.kt
@@ -40,81 +40,3 @@ fun TextView.setTextAppearanceCompat(context: Context, resId: Int) {
setTextAppearance(context, resId)
}
}
-
-/**
- * Search this view and any children for a [ColorDrawable] `background` and return it's `color`,
- * else return `colorSurface`.
- */
-@ColorInt
-fun View.descendantBackgroundColor(): Int {
- val bg = backgroundColor()
- if (bg != null) {
- return bg
- } else if (this is ViewGroup) {
- forEach {
- val childBg = descendantBackgroundColorOrNull()
- if (childBg != null) {
- return childBg
- }
- }
- }
- return context.themeColor(R.attr.colorSurface)
-}
-
-@ColorInt
-private fun View.descendantBackgroundColorOrNull(): Int? {
- val bg = backgroundColor()
- if (bg != null) {
- return bg
- } else if (this is ViewGroup) {
- forEach {
- val childBg = backgroundColor()
- if (childBg != null) {
- return childBg
- }
- }
- }
- return null
-}
-
-/**
- * Check if this [View]'s `background` is a [ColorDrawable] and if so, return it's `color`,
- * otherwise `null`.
- */
-@ColorInt
-fun View.backgroundColor(): Int? {
- val bg = background
- if (bg is ColorDrawable) {
- return bg.color
- } else {
- val tint = backgroundTintList?.defaultColor
- if (tint != null && tint != -1) return tint
- }
- return null
-}
-
-/**
- * Walk up from a [View] looking for an ancestor with a given `id`.
- */
-fun View.findAncestorById(@IdRes ancestorId: Int): View {
- return when {
- id == ancestorId -> this
- parent is View -> (parent as View).findAncestorById(ancestorId)
- else -> throw IllegalArgumentException("$ancestorId not a valid ancestor")
- }
-}
-
-/**
- * A copy of the KTX method, adding the ability to add extra padding the bottom of the [Bitmap];
- * useful when it will be used in a [android.graphics.BitmapShader] with
- * a [android.graphics.Shader.TileMode.CLAMP][CLAMP tile mode].
- */
-fun View.drawToBitmap(@Px extraPaddingBottom: Int = 0): Bitmap {
- check(ViewCompat.isLaidOut(this)) {
- "View needs to be laid out before calling drawToBitmap()"
- }
- return Bitmap.createBitmap(width, height + extraPaddingBottom, ARGB_8888).applyCanvas {
- translate(-scrollX.toFloat(), -scrollY.toFloat())
- draw(this)
- }
-}
diff --git a/Reply/app/src/main/java/com/materialstudies/reply/util/transition/MaterialContainerTransition.kt b/Reply/app/src/main/java/com/materialstudies/reply/util/transition/MaterialContainerTransition.kt
deleted file mode 100644
index 3c2f8eb2..00000000
--- a/Reply/app/src/main/java/com/materialstudies/reply/util/transition/MaterialContainerTransition.kt
+++ /dev/null
@@ -1,453 +0,0 @@
-/*
- * Copyright 2019 Google LLC
- *
- * 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
- *
- * https://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 com.materialstudies.reply.util.transition
-
-import android.animation.Animator
-import android.animation.ObjectAnimator
-import android.annotation.SuppressLint
-import android.graphics.Bitmap
-import android.graphics.BitmapShader
-import android.graphics.Canvas
-import android.graphics.ColorFilter
-import android.graphics.Paint
-import android.graphics.Path
-import android.graphics.PixelFormat
-import android.graphics.RectF
-import android.graphics.Shader.TileMode.CLAMP
-import android.graphics.drawable.Drawable
-import android.transition.Transition
-import android.transition.TransitionValues
-import android.util.Property
-import android.view.View
-import android.view.ViewGroup
-import androidx.annotation.ColorInt
-import androidx.annotation.IdRes
-import androidx.annotation.Px
-import androidx.core.animation.doOnEnd
-import androidx.core.animation.doOnStart
-import androidx.core.graphics.withScale
-import androidx.core.graphics.withTranslation
-import androidx.core.view.forEach
-import com.google.android.material.shape.ShapeAppearanceModel
-import com.google.android.material.shape.Shapeable
-import com.materialstudies.reply.R
-import com.materialstudies.reply.util.CornerRounding
-import com.materialstudies.reply.util.descendantBackgroundColor
-import com.materialstudies.reply.util.drawToBitmap
-import com.materialstudies.reply.util.findAncestorById
-import com.materialstudies.reply.util.lerp
-import com.materialstudies.reply.util.toCornerRounding
-import com.materialstudies.reply.util.toFloatArray
-import com.materialstudies.reply.util.transition.MaterialContainerTransitionDrawable.PROGRESS
-import com.materialstudies.reply.util.withAlpha
-
-@Px private const val BITMAP_PADDING_BOTTOM = 1
-private const val PROP_BOUNDS = "materialContainerTransition:bounds"
-private const val PROP_SHAPE_APPEARANCE = "materialContainerTransition:shapeAppearance"
-private const val PROP_BACKGROUND_BOUNDS = "materialContainerTransition:backgroundBounds"
-private const val PROP_BACKGROUND_BITMAP = "materialContainerTransition:backgroundBitmap"
-private val TRANSITION_PROPS = arrayOf(PROP_BOUNDS, PROP_SHAPE_APPEARANCE)
-
-/**
- * A [Transition] which implements the Material Container pattern from
- * https://medium.com/google-design/motion-design-doesnt-have-to-be-hard-33089196e6c2
- */
-class MaterialContainerTransition(
- @IdRes private val drawInId: Int = android.R.id.content,
- private val correctForZOrdering: Boolean = false
-) : Transition() {
-
- private var fromView: View? = null
- private var toView: View? = null
-
- override fun getTransitionProperties() = TRANSITION_PROPS
-
- fun setSharedElementViews(from: View, to: View) {
- fromView = from
- toView = to
- }
-
- override fun captureStartValues(transitionValues: TransitionValues) {
- if (fromView != null) transitionValues.view = fromView
- captureValues(transitionValues)
- }
-
- override fun captureEndValues(transitionValues: TransitionValues) {
- if (toView != null) transitionValues.view = toView
- captureValues(transitionValues)
- }
-
- @SuppressLint("Recycle")
- private fun captureValues(
- transitionValues: TransitionValues
- ) {
- val view = transitionValues.view
-
- if (view.isLaidOut || view.width != 0 || view.height != 0) {
- // Capture location in screen co-ordinates
- val loc = IntArray(2)
- view.getLocationOnScreen(loc)
- val left = loc[0].toFloat() - view.translationX
- val top = loc[1].toFloat() - view.translationY
- transitionValues.values[PROP_BOUNDS] = RectF(
- left,
- top,
- left + view.width,
- top + view.height
- )
-
- // Clear any ripples or effects caused by clicks, long presses, etc.
- view.jumpDrawablesToCurrentState()
-
- // Store the view's shape appearance; either from a [Shapeable] view; else checking
- // the `transitionShapeAppearance` theme attr.
- val shapeAppearance: ShapeAppearanceModel? = when (view) {
- is Shapeable -> view.shapeAppearanceModel
- else -> {
- val ta = view.context.obtainStyledAttributes(
- intArrayOf(R.attr.transitionShapeAppearance)
- )
- val shapeAppId = ta.getResourceId(0, -1)
- ta.recycle()
- if (shapeAppId != -1) {
- ShapeAppearanceModel.builder(
- view.context,
- shapeAppId,
- 0
- ).build()
- } else {
- null
- }
- }
- }
- transitionValues.values[PROP_SHAPE_APPEARANCE] = shapeAppearance
-
- // Take a bitmap snapshot of the entire layout hierarchy of the start and end scenes.
- // This is used to draw the start layout under all in-progress animations during
- // MaterialContainerTransitionDrawable.draw(). This is used only while Fragment
- // transactions are improperly ordered (related to aosp/987385) and should be removed
- // in favor of a recede transition once fixed.
-
- // Exit early if this transition has not opted in for z order correction faking.
- if (!correctForZOrdering) return
-
- val root = view.rootView
- val rootLoc = IntArray(2)
- root.getLocationOnScreen(rootLoc)
- val rootLeft = rootLoc[0].toFloat() - root.translationX
- val rootTop = rootLoc[1].toFloat() - root.translationY
- transitionValues.values[PROP_BACKGROUND_BOUNDS] = RectF(
- rootLeft,
- rootTop,
- rootLeft + root.width,
- rootTop + root.height
- )
- transitionValues.values[PROP_BACKGROUND_BITMAP] = root.drawToBitmap(
- BITMAP_PADDING_BOTTOM
- )
- }
- }
-
- override fun createAnimator(
- sceneRoot: ViewGroup,
- startValues: TransitionValues?,
- endValues: TransitionValues?
- ): Animator? {
- if (startValues == null || endValues == null || endValues.view !is ViewGroup) {
- return null
- }
-
- val startView = startValues.view
- val endView = endValues.view as ViewGroup
- // Draw in the given ancestor view's overlay. This allows us to draw beyond the bounds of
- // the shared element view, which we might need to do to animate hierarchy changes e.g.
- // from a full screen view to a grid item.
- val drawIn = endView.findAncestorById(drawInId) as ViewGroup
-
- val startBounds = startValues.values[PROP_BOUNDS] as RectF
- val endBounds = endValues.values[PROP_BOUNDS] as RectF
-
- // Account for location of drawIn view, which could be offset by elements
- // like the status bar
- val loc = IntArray(2)
- drawIn.getLocationOnScreen(loc)
- val drawInLeft = loc[0] - drawIn.translationX
- val drawInTop = loc[1] - drawIn.translationY
- startBounds.offset(-drawInLeft, -drawInTop)
- endBounds.offset(-drawInLeft, -drawInTop)
-
- val dr = MaterialContainerTransitionDrawable(
- startView,
- startValues.values[PROP_BACKGROUND_BITMAP] as? Bitmap,
- startValues.values[PROP_BACKGROUND_BOUNDS] as? RectF,
- startBounds,
- (startValues.values[PROP_SHAPE_APPEARANCE] as ShapeAppearanceModel?)
- .toCornerRounding(startBounds),
- endView,
- endValues.values[PROP_BACKGROUND_BITMAP] as? Bitmap,
- endValues.values[PROP_BACKGROUND_BOUNDS] as? RectF,
- endBounds,
- (endValues.values[PROP_SHAPE_APPEARANCE] as ShapeAppearanceModel?)
- .toCornerRounding(endBounds),
- startView.descendantBackgroundColor(),
- endView.descendantBackgroundColor()
- )
-
- return ObjectAnimator.ofFloat(dr, PROGRESS, 0f, 1f).apply {
- doOnStart {
- dr.setBounds(0, 0, drawIn.width, drawIn.height)
- drawIn.overlay.add(dr)
- // Hide the view during the transition
- drawIn.forEach { it.alpha = 0f }
- }
- doOnEnd {
- drawIn.forEach { it.alpha = 1f }
- drawIn.overlay.remove(dr)
- }
- }
- }
-}
-
-/**
- * A [Drawable] which cross-fades between `startImage` and `endImage`, scaling between `startBounds`
- * and `endBounds`.
- *
- * Additionally it draws a scrim over non-shared elements and a background to the container.
- */
-private class MaterialContainerTransitionDrawable(
- private val startView: View,
- private val startBackgroundImage: Bitmap?,
- private val startBackgroundBounds: RectF?,
- private val startBounds: RectF,
- private val startRadii: CornerRounding,
- private val endView: View,
- private val endBackgroundImage: Bitmap?,
- private val endBackgroundBounds: RectF?,
- private val endBounds: RectF,
- private val endRadii: CornerRounding,
- @ColorInt val containerStartColor: Int = 0xffffffff.toInt(),
- @ColorInt val containerEndColor: Int = 0xffffffff.toInt()
-) : Drawable() {
-
- private val imagePaint = Paint(Paint.FILTER_BITMAP_FLAG)
-
- private val startBackgroundShader: BitmapShader?
- get() = if (startBackgroundImage != null) {
- BitmapShader(startBackgroundImage, CLAMP, CLAMP)
- } else {
- null
- }
-
- private val endBackgroundShader: BitmapShader?
- get() = if (endBackgroundImage != null) {
- BitmapShader(endBackgroundImage, CLAMP, CLAMP)
- } else {
- null
- }
- private val startContainerPaint = Paint().apply {
- style = Paint.Style.FILL
- color = containerStartColor
- }
- private val endContainerPaint = Paint().apply {
- style = Paint.Style.FILL
- color = containerEndColor
- }
- private val currentBounds = RectF(startBounds)
- private val currentPath = Path()
- private val entering = endBounds.height() > startBounds.height()
-
- // Values which define the fraction during which animations start and end for the outgoing and
- // incoming bitmaps based on a total progress of 0.0-1.0
- // The fading out of the outgoing element
- private val alphaOutStartPoint = 0.0F
- private val alphaOutEndPoint = 0.3F
- //The fading in of the incoming element
- private val alphaInStartPoint = 0.3F
- private val alphaInEndPoint = 1.0F
- // The corner shape animation of the container
- private val shapeStartPoint = 0.0F
- private val shapeEndPoint = 0.8F
-
-
- // Transition is driven by setting this property [0–1]
- private var progress = 0f
- set(value) {
- if (value != field) {
- field = value
- // Update the container bounds
- currentBounds.set(
- lerp(startBounds.left, endBounds.left, value),
- lerp(startBounds.top, endBounds.top, value),
- lerp(startBounds.right, endBounds.right, value),
- lerp(startBounds.bottom, endBounds.bottom, value)
- )
-
- // Update the path that is going to be used to clip all items inside the
- // container that is transforming.
- val cornerRadii = lerp(
- startRadii,
- endRadii,
- shapeStartPoint,
- shapeEndPoint,
- progress
- )
- currentPath.apply {
- reset()
- addRoundRect(
- currentBounds,
- cornerRadii.toFloatArray(),
- Path.Direction.CW
- )
- }
-
- invalidateSelf()
- }
- }
-
-
- init {
- // Avoid the state where draw is called before progress is set for the first time and
- // has the chance to lerp, scale and translate values necessary to draw the scene correctly.
- progress = 0.000001F
- }
-
- override fun draw(canvas: Canvas) {
- drawRecedingBackgroundBitmap(canvas)
-
- // Clip all of the following draw operations to the bounds of the current path (a path
- // created using the currentBounds, the transitioning host container, and the transitioning
- // corner radii). This will clip all subsequent operations to a rounded Rect.
- canvas.clipPath(currentPath)
-
- // Draw a background for the start view which is able to fill the container as it expands.
- // This is useful for transitions such as FABs->Full screen cards where the FAB's rounded
- // corners fail to fill the expanding corner radii of the container.
- canvas.drawRect(currentBounds, startContainerPaint)
-
- // Fade out the startView while pinning it to the top of currentBounds and scaling it to
- // fit the width of currentBounds.
- val startAlpha = lerp(1F, 0F, alphaOutStartPoint, alphaOutEndPoint, progress)
- // Translate to pin to top
- if (startAlpha > 0F) {
- canvas.withTranslation(currentBounds.left, currentBounds.top) {
- // Scale to match container width
- val scale = currentBounds.width() / startBounds.width()
- scale(scale, scale)
- // Fade out all views
- withAlpha(bounds, startAlpha) {
- startView.draw(this)
- }
- }
- }
-
- // Draw a background which matches the end view's background and occludes the
- // startContainerPaint as it becomes opaque.
- endContainerPaint.alpha = lerp(0, 255, alphaInStartPoint, alphaInEndPoint, progress)
- canvas.drawRect(currentBounds, endContainerPaint)
-
- // Fade in the endView while pinning it to the top of currentBounds and scaling it to fit
- // the width of currentBounds.
- val endAlpha = lerp(0F, 1F, alphaInStartPoint, alphaInEndPoint, progress)
- if (endAlpha > 0F) {
- // Translate to pin to top
- canvas.withTranslation(currentBounds.left, currentBounds.top) {
- // Scale to match container width
- val scale = currentBounds.width() / endBounds.width()
- scale(scale, scale)
- // Fade in all views
- withAlpha(bounds, endAlpha) {
- endView.draw(this)
- }
- }
- }
- }
-
- /**
- * Draw the bitmap of the root start or end screen (depending on if this container is entering
- * or exiting) to work around Transition animation ordering.
- *
- * By default Transition exiting animations are played above entering animations. In some cases,
- * such as cross-fading, this is the desired behavior. For container transitions (or slides),
- * the entering transition should be played on top of the exiting transition. To make up for
- * this behavior, [MaterialContainerTransition] captures the start and end bitmap of the root
- * start and end scene and draws them first, at the bottom of the
- * [MaterialContainerTransitionDrawable] and avoids using seperate enter and exit transitions
- * altogether.
- *
- * If z ordering of Transitions becomes configurable, this should be removed in favor of a
- * dedicated enter or exit transition.
- */
- private fun drawRecedingBackgroundBitmap(canvas: Canvas) {
- // Fake the background of the transition by manually drawing the background bitmap of the
- // start screen or end screen.
- if (entering && startBackgroundShader != null && startBackgroundBounds != null) {
- imagePaint.alpha = lerp(255, 0, 0F, 1F, progress)
- imagePaint.shader = startBackgroundShader
-
- val bgScale = lerp(1F, 0.9F, progress)
- canvas.withScale(
- bgScale,
- bgScale,
- bounds.width() / 2F,
- bounds.height() / 2F
- ) {
- canvas.drawRect(
- startBackgroundBounds,
- imagePaint
- )
- }
- } else if (!entering && endBackgroundShader != null && endBackgroundBounds != null) {
- imagePaint.alpha = lerp(0, 255, 0F, 1F, progress)
- imagePaint.shader = endBackgroundShader
-
- val bgScale = lerp(0.9F, 1F, progress)
- canvas.withScale(
- bgScale,
- bgScale,
- bounds.width() / 2F,
- bounds.height() / 2F
- ) {
- canvas.drawRect(
- endBackgroundBounds,
- imagePaint
- )
- }
-
- }
- }
-
- override fun setAlpha(alpha: Int) {
- imagePaint.alpha = alpha
- }
-
- override fun getOpacity() = PixelFormat.TRANSLUCENT
-
- override fun setColorFilter(colorFilter: ColorFilter?) {
- imagePaint.colorFilter = colorFilter
- }
-
- object PROGRESS : Property(
- Float::class.java,
- "progress"
- ) {
- override fun get(drawable: MaterialContainerTransitionDrawable) = drawable.progress
-
- override fun set(drawable: MaterialContainerTransitionDrawable, value: Float) {
- drawable.progress = value
- }
- }
-}
diff --git a/Reply/app/src/main/res/drawable/ic_arrow_back.xml b/Reply/app/src/main/res/drawable/ic_arrow_back.xml
new file mode 100644
index 00000000..9010d5a8
--- /dev/null
+++ b/Reply/app/src/main/res/drawable/ic_arrow_back.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/Reply/app/src/main/res/drawable/ic_circle.xml b/Reply/app/src/main/res/drawable/ic_circle.xml
new file mode 100644
index 00000000..034efc39
--- /dev/null
+++ b/Reply/app/src/main/res/drawable/ic_circle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Reply/app/src/main/res/drawable/ic_close.xml b/Reply/app/src/main/res/drawable/ic_close.xml
index 6f3ecca2..a5a582fc 100644
--- a/Reply/app/src/main/res/drawable/ic_close.xml
+++ b/Reply/app/src/main/res/drawable/ic_close.xml
@@ -1,6 +1,6 @@
+
+
+
diff --git a/Reply/app/src/main/res/drawable/ic_home.xml b/Reply/app/src/main/res/drawable/ic_home.xml
new file mode 100644
index 00000000..b67a592d
--- /dev/null
+++ b/Reply/app/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/Reply/app/src/main/res/drawable/ic_mic.xml b/Reply/app/src/main/res/drawable/ic_mic.xml
new file mode 100644
index 00000000..af66ae73
--- /dev/null
+++ b/Reply/app/src/main/res/drawable/ic_mic.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/Reply/app/src/main/res/drawable/ic_schedule.xml b/Reply/app/src/main/res/drawable/ic_schedule.xml
new file mode 100644
index 00000000..072727fc
--- /dev/null
+++ b/Reply/app/src/main/res/drawable/ic_schedule.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/Reply/app/src/main/res/layout/email_item_layout.xml b/Reply/app/src/main/res/layout/email_item_layout.xml
index 5d77e90a..b9d97f87 100644
--- a/Reply/app/src/main/res/layout/email_item_layout.xml
+++ b/Reply/app/src/main/res/layout/email_item_layout.xml
@@ -45,7 +45,6 @@
android:onClick="@{(view) -> listener.onEmailClicked(view, email)}"
android:onLongClick="@{(view) -> listener.onEmailLongPressed(email)}"
app:cardPreventCornerOverlap="false"
- app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Reply.MediumComponent.Marked"
android:transitionName="@{@string/email_card_transition_name(email.id)}">
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools">
+
@@ -196,6 +201,123 @@
android:textAppearance="?attr/textAppearanceBody1"
app:lineHeight="24sp" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Reply/app/src/main/res/layout/fragment_email.xml b/Reply/app/src/main/res/layout/fragment_email.xml
index 99685fb0..05be8841 100644
--- a/Reply/app/src/main/res/layout/fragment_email.xml
+++ b/Reply/app/src/main/res/layout/fragment_email.xml
@@ -37,7 +37,7 @@
android:id="@+id/email_card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:transitionName="@{@string/email_card_transition_name(email.id)}">
+ android:transitionName="@string/email_card_detail_transition_name">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Reply/app/src/main/res/layout/search_suggestion_item.xml b/Reply/app/src/main/res/layout/search_suggestion_item.xml
new file mode 100644
index 00000000..cb27fc89
--- /dev/null
+++ b/Reply/app/src/main/res/layout/search_suggestion_item.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Reply/app/src/main/res/layout/search_suggestion_title.xml b/Reply/app/src/main/res/layout/search_suggestion_title.xml
new file mode 100644
index 00000000..58710ecb
--- /dev/null
+++ b/Reply/app/src/main/res/layout/search_suggestion_title.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Reply/app/src/main/res/navigation/navigation_graph.xml b/Reply/app/src/main/res/navigation/navigation_graph.xml
index 2e23a438..099a4db3 100644
--- a/Reply/app/src/main/res/navigation/navigation_graph.xml
+++ b/Reply/app/src/main/res/navigation/navigation_graph.xml
@@ -13,13 +13,18 @@
the License.
-->
+ android:label="HomeFragment">
+
@@ -41,7 +46,20 @@
app:argType="long"
android:defaultValue="-1L"/>
+
+
-
\ No newline at end of file
+
+
diff --git a/Reply/app/src/main/res/values/attrs.xml b/Reply/app/src/main/res/values/attrs.xml
index e040652e..417ec515 100644
--- a/Reply/app/src/main/res/values/attrs.xml
+++ b/Reply/app/src/main/res/values/attrs.xml
@@ -31,6 +31,4 @@
-
-
\ No newline at end of file
diff --git a/Reply/app/src/main/res/values/dimens.xml b/Reply/app/src/main/res/values/dimens.xml
index 457450bd..28727fe5 100644
--- a/Reply/app/src/main/res/values/dimens.xml
+++ b/Reply/app/src/main/res/values/dimens.xml
@@ -30,6 +30,8 @@
42dp
+ 6dp
+ 3dp
32dp
diff --git a/Reply/app/src/main/res/values/motion.xml b/Reply/app/src/main/res/values/motion.xml
index cf24acb0..81e5b031 100644
--- a/Reply/app/src/main/res/values/motion.xml
+++ b/Reply/app/src/main/res/values/motion.xml
@@ -16,7 +16,7 @@
- 300
+ 300
225
175
diff --git a/Reply/app/src/main/res/values/shape.xml b/Reply/app/src/main/res/values/shape.xml
index 1da8de85..7403dd95 100644
--- a/Reply/app/src/main/res/values/shape.xml
+++ b/Reply/app/src/main/res/values/shape.xml
@@ -31,12 +31,6 @@
- @dimen/reply_large_component_corner_radius
-
-
-
-
24dp
0dp
12dp
diff --git a/Reply/app/src/main/res/values/strings.xml b/Reply/app/src/main/res/values/strings.xml
index 6b5934bf..abf47617 100644
--- a/Reply/app/src/main/res/values/strings.xml
+++ b/Reply/app/src/main/res/values/strings.xml
@@ -41,7 +41,7 @@
Inbox
Starred
- Send
+ Sent
Trash
Spam
Drafts
@@ -53,6 +53,7 @@
To %1$s
email_card_%1$s
+ email_card_detail
Close editing email
@@ -61,4 +62,9 @@
Subject
New message…
Recipients
+
+
+ Search email
+ YESTERDAY
+ THIS WEEK
diff --git a/Reply/build.gradle b/Reply/build.gradle
index 9a441109..1ef24fc2 100644
--- a/Reply/build.gradle
+++ b/Reply/build.gradle
@@ -14,17 +14,15 @@
buildscript {
ext {
- kotlin_version = '1.3.50'
- navigation_version = '2.2.0-beta01'
+ kotlin_version = '1.4.21'
+ navigation_version = '2.3.2'
}
repositories {
google()
jcenter()
}
dependencies{
- // Wait to move to 3.6 until ConstraintLayout related attribute bugs are resolved. See
- // https://issuetracker.google.com/issues/138601946 for more details.
- classpath 'com.android.tools.build:gradle:3.5.0'
+ classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
@@ -39,7 +37,3 @@ allprojects {
jcenter()
}
}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
diff --git a/Reply/gradle.properties b/Reply/gradle.properties
index dbf9c126..df995589 100644
--- a/Reply/gradle.properties
+++ b/Reply/gradle.properties
@@ -6,7 +6,6 @@
# 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.
-android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
diff --git a/Reply/gradle/wrapper/gradle-wrapper.properties b/Reply/gradle/wrapper/gradle-wrapper.properties
index 1bd6748e..58947f54 100644
--- a/Reply/gradle/wrapper/gradle-wrapper.properties
+++ b/Reply/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Mon Oct 21 09:40:35 PDT 2019
+#Mon Nov 02 15:31:58 GMT 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
diff --git a/screenshots/mdc_samples.gif b/screenshots/mdc_samples.gif
new file mode 100644
index 00000000..bac3a3b0
Binary files /dev/null and b/screenshots/mdc_samples.gif differ