diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..526b4c2 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..fa0d9d1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 93d31ce..0eebe9d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,16 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'androidx.navigation.safeargs.kotlin' } android { compileSdk 31 + viewBinding { + enabled = true + } + defaultConfig { applicationId "com.github.krottv.tmstemp" minSdk 24 @@ -32,6 +37,17 @@ android { } dependencies { + implementation "androidx.navigation:navigation-fragment-ktx:2.4.2" + implementation "androidx.navigation:navigation-ui-ktx:2.4.2" + implementation "androidx.fragment:fragment-ktx:1.4.1" + implementation "androidx.activity:activity-ktx:1.4.0" + + implementation("io.coil-kt:coil:2.0.0-rc03") + + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a551aff..e24bb3d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + diff --git a/app/src/main/java/com/github/krottv/tmstemp/MainActivity.kt b/app/src/main/java/com/github/krottv/tmstemp/MainActivity.kt deleted file mode 100644 index 8952f64..0000000 --- a/app/src/main/java/com/github/krottv/tmstemp/MainActivity.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.krottv.tmstemp - -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/binder/MainActivityDataBinder.kt b/app/src/main/java/com/github/krottv/tmstemp/binder/MainActivityDataBinder.kt new file mode 100644 index 0000000..8b81aeb --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/binder/MainActivityDataBinder.kt @@ -0,0 +1,5 @@ +package com.github.krottv.tmstemp.binder + +interface MainActivityDataBinder { + fun bind() +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/binder/MainActivityRecyclerScrollDataBinder.kt b/app/src/main/java/com/github/krottv/tmstemp/binder/MainActivityRecyclerScrollDataBinder.kt new file mode 100644 index 0000000..8b9d9da --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/binder/MainActivityRecyclerScrollDataBinder.kt @@ -0,0 +1,15 @@ +package com.github.krottv.tmstemp.binder + +import android.app.Activity +import android.view.LayoutInflater +import com.github.krottv.tmstemp.databinding.ActivityMainBinding + + +class MainActivityRecyclerScrollDataBinder(private val activity: Activity): MainActivityDataBinder { + + override fun bind() { + val container = ActivityMainBinding.inflate(LayoutInflater.from(activity)) + activity.setContentView(container.root) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/binder/RecyclerFragmentBinder.kt b/app/src/main/java/com/github/krottv/tmstemp/binder/RecyclerFragmentBinder.kt new file mode 100644 index 0000000..ecabf04 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/binder/RecyclerFragmentBinder.kt @@ -0,0 +1,67 @@ +package com.github.krottv.tmstemp.binder + +import android.os.Bundle +import android.transition.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.github.krottv.tmstemp.R +import com.github.krottv.tmstemp.databinding.SceneContainerBinding +import com.github.krottv.tmstemp.domain.MessageModel +import com.github.krottv.tmstemp.view.MessageAdapter + +class RecyclerFragmentBinder(private val fragment: Fragment, private val onItemClick: (View, MessageModel) -> Unit) { + + private lateinit var binding: SceneContainerBinding + + fun bindView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SceneContainerBinding.inflate(inflater, container, false) + + return binding.root + } + + + private val customTransition = TransitionSet().apply { + addTransition(ChangeBounds()) + addTransition(Fade(Fade.IN)) + } + + fun onDataLoaded(data: List) { + + val scene = Scene.getSceneForLayout(binding.sceneRoot, R.layout.messages_fragment, fragment.requireContext()) + TransitionManager.go(scene, customTransition) + + val recyclerView = scene.sceneRoot.findViewById(R.id.container) + recyclerView.layoutManager = LinearLayoutManager(fragment.requireContext()) + + if (recyclerView.adapter == null) { + recyclerView.adapter = MessageAdapter(data, onItemClick) + } else { + (recyclerView.adapter as MessageAdapter).data = data + } + } + + fun showError(exception: Throwable) { + + val scene = Scene.getSceneForLayout(binding.sceneRoot, R.layout.show_error, fragment.requireContext()) + TransitionManager.go(scene, customTransition) + + scene.sceneRoot.findViewById(R.id.errorText).apply { + text = exception.message + } + } + + fun showProgress() { + + val scene = Scene.getSceneForLayout(binding.sceneRoot, R.layout.loading, fragment.requireContext()) + TransitionManager.go(scene, customTransition) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSource.kt b/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSource.kt new file mode 100644 index 0000000..5d674f6 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSource.kt @@ -0,0 +1,7 @@ +package com.github.krottv.tmstemp.data + +import com.github.krottv.tmstemp.domain.MessageModel + +interface MessagesRemoteDataSource { + suspend fun getMessages(): List +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSourceError.kt b/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSourceError.kt new file mode 100644 index 0000000..206edf1 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSourceError.kt @@ -0,0 +1,12 @@ +package com.github.krottv.tmstemp.data + +import com.github.krottv.tmstemp.domain.MessageModel +import kotlinx.coroutines.delay +import java.lang.IllegalStateException + +class MessagesRemoteDataSourceError: MessagesRemoteDataSource { + override suspend fun getMessages(): List { + delay(2000) + throw IllegalStateException("IllegalStateException") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSourceFake.kt b/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSourceFake.kt new file mode 100644 index 0000000..47966d7 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/data/MessagesRemoteDataSourceFake.kt @@ -0,0 +1,20 @@ +package com.github.krottv.tmstemp.data + +import com.github.krottv.tmstemp.domain.MessageModel +import kotlinx.coroutines.delay + +class MessagesRemoteDataSourceFake: MessagesRemoteDataSource { + override suspend fun getMessages(): List { + delay(2000) + val message = MessageModel( + "https://images.unsplash.com/photo-1505062351414-586330b076f2?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1080&q=80", + "Пункт такойто", + "Какой-то текст Какой-то текст Какой-то текст Какой-то текст" + ) + val result = ArrayList(20) + for (i in 0..20) { + result.add(message.copy(mainText = "Пункт $i")) + } + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/domain/MessageModel.kt b/app/src/main/java/com/github/krottv/tmstemp/domain/MessageModel.kt new file mode 100644 index 0000000..5bd1276 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/domain/MessageModel.kt @@ -0,0 +1,36 @@ +package com.github.krottv.tmstemp.domain + +import android.os.Parcel +import android.os.Parcelable +import androidx.annotation.Keep + +@Keep +data class MessageModel(val mainImage: String, val mainText: String, val littleText: String): Parcelable { + constructor(parcel: Parcel) : this( + parcel.readString()!!, + parcel.readString()!!, + parcel.readString()!! + ) + + fun transitionId() = mainText + "_" + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(mainImage) + parcel.writeString(mainText) + parcel.writeString(littleText) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): MessageModel { + return MessageModel(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/presentation/MessageViewModel.kt b/app/src/main/java/com/github/krottv/tmstemp/presentation/MessageViewModel.kt new file mode 100644 index 0000000..2d18502 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/presentation/MessageViewModel.kt @@ -0,0 +1,31 @@ +package com.github.krottv.tmstemp.presentation + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.krottv.tmstemp.data.MessagesRemoteDataSource +import com.github.krottv.tmstemp.data.MessagesRemoteDataSourceError +import com.github.krottv.tmstemp.data.MessagesRemoteDataSourceFake +import com.github.krottv.tmstemp.domain.MessageModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class MessageViewModel: ViewModel() { + private val fakeMessages: MessagesRemoteDataSource = MessagesRemoteDataSourceFake() + + private val _state = MutableStateFlow>?>(null) + val state: StateFlow>?> = _state + + fun loadData() { + viewModelScope.launch(Dispatchers.IO) { + val result = try { + Result.success(fakeMessages.getMessages()) + } catch (exception: Throwable) { + Result.failure(exception) + } + + _state.emit(result) + } + } +} diff --git a/app/src/main/java/com/github/krottv/tmstemp/view/FullscreenFragment.kt b/app/src/main/java/com/github/krottv/tmstemp/view/FullscreenFragment.kt new file mode 100644 index 0000000..4b99d96 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/view/FullscreenFragment.kt @@ -0,0 +1,62 @@ +package com.github.krottv.tmstemp.view + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.doOnPreDraw +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController +import androidx.transition.AutoTransition +import coil.load +import com.github.krottv.tmstemp.databinding.FullscreenMessageBinding +import com.github.krottv.tmstemp.domain.MessageModel +import java.util.concurrent.TimeUnit + +class FullscreenFragment: Fragment() { + + companion object { + const val TRANSITION_ID = "Fullscreen" + } + + private lateinit var binding: FullscreenMessageBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + sharedElementEnterTransition = AutoTransition() + sharedElementReturnTransition = AutoTransition() + + binding = FullscreenMessageBinding.inflate(inflater, container, false) + + binding.root.transitionName = TRANSITION_ID + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val item: MessageModel = requireArguments().getParcelable("currentMessage")!! + + postponeEnterTransition(3000, TimeUnit.MILLISECONDS) + + binding.anyText.text = item.littleText + binding.imageView.load(item.mainImage) { + listener { request, result -> + (view.parent as? View)?.doOnPreDraw { + startPostponedEnterTransition() + } + } + } + binding.mainText.apply { + text = item.mainText + setOnClickListener { + findNavController().navigateUp() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/view/MainActivity.kt b/app/src/main/java/com/github/krottv/tmstemp/view/MainActivity.kt new file mode 100644 index 0000000..e528fe2 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/view/MainActivity.kt @@ -0,0 +1,15 @@ +package com.github.krottv.tmstemp.view + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import com.github.krottv.tmstemp.binder.MainActivityDataBinder +import com.github.krottv.tmstemp.binder.MainActivityRecyclerScrollDataBinder + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val mainActivityDataBinder: MainActivityDataBinder = MainActivityRecyclerScrollDataBinder(this) + mainActivityDataBinder.bind() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/view/MessageAdapter.kt b/app/src/main/java/com/github/krottv/tmstemp/view/MessageAdapter.kt new file mode 100644 index 0000000..656ea99 --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/view/MessageAdapter.kt @@ -0,0 +1,39 @@ +package com.github.krottv.tmstemp.view + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import coil.load +import com.github.krottv.tmstemp.R +import com.github.krottv.tmstemp.domain.MessageModel + +class MessageAdapter(var data: List, private val onItemClick: (View, MessageModel) -> Unit): RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.recycler_cell, parent, false) + return MessageViewHolder(view) + } + + override fun onBindViewHolder(holder: MessageViewHolder, position: Int) { + val cell = data[position] + holder.mainImage.apply { + load(cell.mainImage) + scaleType = ImageView.ScaleType.CENTER_CROP + } + + holder.mainText.text = cell.mainText + holder.littleText.text = cell.littleText + + holder.itemView.transitionName = cell.transitionId() + + holder.itemView.setOnClickListener { + onItemClick(it, cell) + } + } + + override fun getItemCount(): Int { + return data.size + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/view/MessageViewHolder.kt b/app/src/main/java/com/github/krottv/tmstemp/view/MessageViewHolder.kt new file mode 100644 index 0000000..158f20d --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/view/MessageViewHolder.kt @@ -0,0 +1,13 @@ +package com.github.krottv.tmstemp.view + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.github.krottv.tmstemp.R + +class MessageViewHolder(view: View): RecyclerView.ViewHolder(view) { + val mainImage: ImageView = view.findViewById(R.id.mainImage) + val mainText: TextView = view.findViewById(R.id.mainText) + val littleText: TextView = view.findViewById(R.id.littleText) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/krottv/tmstemp/view/RecyclerFragment.kt b/app/src/main/java/com/github/krottv/tmstemp/view/RecyclerFragment.kt new file mode 100644 index 0000000..7b6973f --- /dev/null +++ b/app/src/main/java/com/github/krottv/tmstemp/view/RecyclerFragment.kt @@ -0,0 +1,72 @@ +package com.github.krottv.tmstemp.view + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.doOnPreDraw +import androidx.fragment.app.Fragment +import androidx.lifecycle.* +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController +import androidx.transition.AutoTransition +import com.github.krottv.tmstemp.binder.RecyclerFragmentBinder +import com.github.krottv.tmstemp.presentation.MessageViewModel +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit + +class RecyclerFragment: Fragment() { + + private lateinit var fragment: RecyclerFragmentBinder + private lateinit var viewModel: MessageViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + viewModel = ViewModelProvider(this)[MessageViewModel::class.java] + + sharedElementEnterTransition = AutoTransition() + sharedElementReturnTransition = AutoTransition() + + fragment = RecyclerFragmentBinder(this) { view, item -> + val navController = findNavController() + + val action = RecyclerFragmentDirections.actionRecyclerFragmentToFullscreenFragment(item) + val extras = FragmentNavigatorExtras(view to FullscreenFragment.TRANSITION_ID) + + navController.navigate(action, extras) + } + + return fragment.bindView(inflater, container, savedInstanceState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + postponeEnterTransition(1000, TimeUnit.MILLISECONDS) + + viewModel.loadData() + + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.state.collect { + if (it != null) { + if (it.isSuccess) { + fragment.onDataLoaded(it.getOrThrow()) + + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + } + } else { + fragment.showError(it.exceptionOrNull() as Throwable) + } + } else { + fragment.showProgress() + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4fc2444..bf18798 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,18 +1,13 @@ - - - - - - \ No newline at end of file + android:layout_height="match_parent"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fullscreen_message.xml b/app/src/main/res/layout/fullscreen_message.xml new file mode 100644 index 0000000..af2697c --- /dev/null +++ b/app/src/main/res/layout/fullscreen_message.xml @@ -0,0 +1,48 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/loading.xml b/app/src/main/res/layout/loading.xml new file mode 100644 index 0000000..3e7fa63 --- /dev/null +++ b/app/src/main/res/layout/loading.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/messages_fragment.xml b/app/src/main/res/layout/messages_fragment.xml new file mode 100644 index 0000000..167ad72 --- /dev/null +++ b/app/src/main/res/layout/messages_fragment.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/layout/recycler_cell.xml b/app/src/main/res/layout/recycler_cell.xml new file mode 100644 index 0000000..6a08f67 --- /dev/null +++ b/app/src/main/res/layout/recycler_cell.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/scene_container.xml b/app/src/main/res/layout/scene_container.xml new file mode 100644 index 0000000..f540014 --- /dev/null +++ b/app/src/main/res/layout/scene_container.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/show_error.xml b/app/src/main/res/layout/show_error.xml new file mode 100644 index 0000000..f612ab7 --- /dev/null +++ b/app/src/main/res/layout/show_error.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..135a69d --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0ccd671..277b80e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + } + dependencies { + def nav_version = "2.4.2" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + } +} + plugins { id 'com.android.application' version '7.1.2' apply false id 'com.android.library' version '7.1.2' apply false