Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: add material container transform transition between list and detail #1

Merged
merged 7 commits into from May 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Expand Up @@ -68,6 +68,7 @@ dependencies {

implementation 'net.mm2d:mmupnp:3.1.1'
implementation 'com.jakewharton.threetenabp:threetenabp:1.2.3'
implementation "com.google.android.material:material:$versions.google_material"

implementation 'com.google.firebase:firebase-analytics:17.3.0'
// Recommended: Add the Firebase SDK for Google Analytics.
Expand Down
44 changes: 42 additions & 2 deletions app/src/main/java/com/freshdigitable/upnpsample/DetailFragment.kt
@@ -1,24 +1,56 @@
package com.freshdigitable.upnpsample

import android.content.Context
import android.graphics.Color
import android.graphics.Path
import android.os.Bundle
import android.transition.PathMotion
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.observe
import androidx.navigation.fragment.navArgs
import com.freshdigitable.upnpsample.di.ViewModelKey
import com.google.android.material.transition.MaterialArcMotion
import com.google.android.material.transition.MaterialContainerTransform
import dagger.Binds
import dagger.Module
import dagger.android.support.AndroidSupportInjection
import dagger.multibindings.IntoMap
import kotlinx.android.synthetic.main.fragment_detail.view.detail_date
import kotlinx.android.synthetic.main.fragment_detail.view.detail_title
import javax.inject.Inject

class DetailFragment : Fragment() {
@Inject
lateinit var viewModelProvider: ViewModelProvider
lateinit var viewModelProviderFactory: ViewModelProvider.Factory
private val viewModel: DetailFragmentViewModel by viewModels { viewModelProviderFactory }
private val args: DetailFragmentArgs by navArgs()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = MaterialContainerTransform().apply {
fitMode = MaterialContainerTransform.FIT_MODE_WIDTH
fadeMode = MaterialContainerTransform.FADE_MODE_THROUGH
containerColor = Color.WHITE
// WORKAROUND: path motion is not worked when start point is equal to end point.
pathMotion = object : PathMotion() {
private val arcMotion = MaterialArcMotion()
override fun getPath(startX: Float, startY: Float, endX: Float, endY: Float): Path {
return if (startX != endX || startY != endY) {
arcMotion.getPath(startX, startY, endX, endY)
} else {
arcMotion.getPath(startX, startY - 1, endX, endY)
}
}
}
}
}

override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
Expand All @@ -34,11 +66,19 @@ class DetailFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = viewModelProvider[MainViewModel::class.java]
view.transitionName = args.sharedElementTransName
val scheduleItem = viewModel.findScheduleItemByTitle(args.title)
scheduleItem.observe(viewLifecycleOwner) {
view.detail_title.text = it?.title
view.detail_date.setListItemDatetime(it?.scheduledStartDateTime)
}
}
}

@Module
interface DetailFragmentModule {
@Binds
@IntoMap
@ViewModelKey(DetailFragmentViewModel::class)
fun bindDetailFragmentViewModel(viewModel: DetailFragmentViewModel): ViewModel
}
30 changes: 28 additions & 2 deletions app/src/main/java/com/freshdigitable/upnpsample/ListFragment.kt
Expand Up @@ -5,17 +5,32 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.app.SharedElementCallback
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.observe
import androidx.recyclerview.widget.LinearLayoutManager
import com.freshdigitable.upnpsample.di.ViewModelKey
import com.google.android.material.transition.Hold
import dagger.Binds
import dagger.Module
import dagger.android.support.AndroidSupportInjection
import dagger.multibindings.IntoMap
import kotlinx.android.synthetic.main.fragment_list.view.main_list
import javax.inject.Inject

class ListFragment : Fragment() {
@Inject
lateinit var viewModelProvider: ViewModelProvider
lateinit var viewModelProviderFactory: ViewModelProvider.Factory
private val viewModel: ListFragmentViewModel by viewModels { viewModelProviderFactory }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
exitTransition = Hold()
}

override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
Expand All @@ -32,7 +47,10 @@ class ListFragment : Fragment() {

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val viewModel = viewModelProvider[MainViewModel::class.java]
// WORKAROUND: for sharedElementReturnTransition with Navigation component
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }

val mainListAdapter = MainListAdapter()

viewModel.allRecordScheduleItems.observe(viewLifecycleOwner) {
Expand All @@ -43,3 +61,11 @@ class ListFragment : Fragment() {
view.main_list.layoutManager = LinearLayoutManager(requireContext())
}
}

@Module
interface ListFragmentModule {
@Binds
@IntoMap
@ViewModelKey(ListFragmentViewModel::class)
fun bindListFragment(viewModel: ListFragmentViewModel): ViewModel
}
37 changes: 22 additions & 15 deletions app/src/main/java/com/freshdigitable/upnpsample/MainActivity.kt
Expand Up @@ -2,25 +2,38 @@ package com.freshdigitable.upnpsample

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelStoreOwner
import com.freshdigitable.upnpsample.di.ViewModelKey
import dagger.Binds
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import com.freshdigitable.upnpsample.di.FragmentScope
import dagger.Module
import dagger.android.AndroidInjection
import dagger.android.AndroidInjector
import dagger.android.ContributesAndroidInjector
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import dagger.multibindings.IntoMap
import javax.inject.Inject

class MainActivity : AppCompatActivity(), HasAndroidInjector {

private val navController: NavController by lazy {
findNavController(R.id.nav_host_fragment_container)
}
private val appBarConfiguration: AppBarConfiguration by lazy {
AppBarConfiguration(navController.graph)
}

override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupActionBarWithNavController(navController, appBarConfiguration)
}

override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}

@Inject
Expand All @@ -31,17 +44,11 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {

@Module
interface MainActivityModule {
@ContributesAndroidInjector
@FragmentScope
@ContributesAndroidInjector(modules = [ListFragmentModule::class])
fun contributeListFragment(): ListFragment

@ContributesAndroidInjector
@FragmentScope
@ContributesAndroidInjector(modules = [DetailFragmentModule::class])
fun contributeDetailFragment(): DetailFragment

@Binds
fun bindViewModelStoreOwner(mainActivity: MainActivity): ViewModelStoreOwner

@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
fun bindMainViewModel(viewModel: MainViewModel): ViewModel
}
17 changes: 14 additions & 3 deletions app/src/main/java/com/freshdigitable/upnpsample/MainListAdapter.kt
Expand Up @@ -5,7 +5,8 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.navigation.Navigation
import androidx.navigation.findNavController
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.recyclerview.widget.RecyclerView
import com.freshdigitable.upnpsample.model.RecordScheduleItem
import kotlinx.android.synthetic.main.view_record_schedule_item.view.record_schedule_date
Expand Down Expand Up @@ -51,13 +52,23 @@ class MainListAdapter : RecyclerView.Adapter<ViewHolder>() {
)
holder.title.text = item.title
holder.date.setListItemDatetime(item.scheduledStartDateTime)
}

val action = ListFragmentDirections.actionMainListToMainDetail(item.title)
holder.itemView.setOnClickListener(Navigation.createNavigateOnClickListener(action))
override fun onViewAttachedToWindow(holder: ViewHolder) {
super.onViewAttachedToWindow(holder)
val item = items[holder.adapterPosition]
val transitionName = "sec_${item.title}"
val action = ListFragmentDirections.actionMainListToMainDetail(item.title, transitionName)
holder.itemView.transitionName = transitionName
holder.itemView.setOnClickListener { v ->
v.findNavController()
.navigate(action, FragmentNavigatorExtras(v to transitionName))
}
}

override fun onViewDetachedFromWindow(holder: ViewHolder) {
super.onViewDetachedFromWindow(holder)
holder.itemView.transitionName = ""
holder.itemView.setOnClickListener(null)
}
}
Expand Down
Expand Up @@ -5,13 +5,17 @@ import androidx.lifecycle.ViewModel
import com.freshdigitable.upnpsample.model.RecordScheduleItem
import javax.inject.Inject

class MainViewModel @Inject constructor(
class ListFragmentViewModel @Inject constructor(
private val repository: RecordScheduleRepository
) : ViewModel() {
val allRecordScheduleItems: LiveData<List<RecordScheduleItem>> by lazy {
repository.getAllRecordScheduleSource()
}
}

class DetailFragmentViewModel @Inject constructor(
private val repository: RecordScheduleRepository
) : ViewModel() {
fun findScheduleItemByTitle(title: String): LiveData<RecordScheduleItem?> {
return repository.findScheduleItemByTitle(title)
}
Expand Down
18 changes: 5 additions & 13 deletions app/src/main/java/com/freshdigitable/upnpsample/di/AppBuilder.kt
@@ -1,11 +1,8 @@
package com.freshdigitable.upnpsample.di

import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import com.freshdigitable.upnpsample.MainActivity
import com.freshdigitable.upnpsample.MainActivityModule
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import javax.inject.Scope

Expand All @@ -14,19 +11,14 @@ import javax.inject.Scope
@Retention
annotation class ActivityScope

@Scope
@MustBeDocumented
@Retention
annotation class FragmentScope

@Module
interface AppBuilder {
@ActivityScope
@ContributesAndroidInjector(modules = [MainActivityModule::class])
fun contributeMainActivity(): MainActivity

companion object {
@Provides
fun provideViewModelProvider(
viewModelStoreOwner: ViewModelStoreOwner,
viewModelProviderFactory: ViewModelProvider.Factory
): ViewModelProvider {
return ViewModelProvider(viewModelStoreOwner, viewModelProviderFactory)
}
}
}
Expand Up @@ -5,10 +5,12 @@ import com.freshdigitable.upnpsample.db.RecordScheduleDao
import com.freshdigitable.upnpsample.device.NasneDeviceProvider
import dagger.Module
import dagger.Provides
import javax.inject.Singleton

@Module
interface RepositoryModule {
companion object {
@Singleton
@Provides
fun provideRecordScheduleRepository(
dao: RecordScheduleDao,
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/layout/fragment_detail.xml
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/layout/fragment_list.xml
Expand Up @@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
Expand All @@ -13,5 +14,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/view_record_schedule_item"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
7 changes: 7 additions & 0 deletions app/src/main/res/navigation/nav_main.xml
Expand Up @@ -7,6 +7,7 @@
<fragment
android:id="@+id/main_list"
android:name="com.freshdigitable.upnpsample.ListFragment"
android:label="@string/title_schedule_list"
tools:layout="@layout/fragment_list"
>
<action
Expand All @@ -17,12 +18,18 @@
<fragment
android:id="@+id/main_detail"
android:name="com.freshdigitable.upnpsample.DetailFragment"
android:label="@string/title_schedule_detail"
tools:layout="@layout/fragment_detail"
>
<argument
android:name="title"
app:argType="string"
app:nullable="false"
/>
<argument
android:name="sharedElementTransName"
app:argType="string"
app:nullable="false"
/>
</fragment>
</navigation>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Expand Up @@ -5,4 +5,6 @@
<string name="notif_schedule_checker_description">scheduleChecker description</string>

<string name="listitem_datetime_format">%1$d/%2$d (%3$s) %4$d:%5$02d</string>
<string name="title_schedule_list">Recording Schedule</string>
<string name="title_schedule_detail">Schedule Detail</string>
</resources>
2 changes: 1 addition & 1 deletion app/src/main/res/values/styles.xml
@@ -1,7 +1,7 @@
<resources>

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Expand Up @@ -9,6 +9,7 @@ buildscript {
"androidx_worker" : "2.3.1",
"androidx_room" : "2.2.3",
"androidx_navigation" : "2.3.0-alpha06",
"google_material" : "1.2.0-alpha06",
"dagger" : "2.27",

"truth" : "1.0",
Expand Down