Convert XML ShimmerFrameLayout into Composable function - android

i'm new in android jetpack compose
i would like to implement Shimmer effect for Android. as per given in this documentation
it's working fine with xml approach, but i want to do same with compose function (in short embed XML into composable function).
Here is XML Code: shimmer_view.xml
<com.facebook.shimmer.ShimmerFrameLayout 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"
tools:context=".presentation.ui.recipe_list.UserListFragment"
android:id="#+id/shimmerFrameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:shimmer_auto_start="true"
>
<include layout="#layout/shimmer_placeholder_card" />
</com.facebook.shimmer.ShimmerFrameLayout>
Fragment where i want to use above xml file
class UserListFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Traditional Approach Working Fine.
// val view = inflater.inflate(R.layout.shimmer_view, container, false)
// return view
// ComposeView inside fragment
val composeView = ComposeView(requireContext()).apply {
setContent {
Text(text = "Welcome in Compose-World ")
// Here i want To use xml file as a Compose View
}
}
return composeView
}
}
is it possible to inflate or Convert shimmer_view.xml into composable function?
OR
somehow emmbed this xml into compose function.
for reference please share sample code if any. it will help us.
Thank you

I created this function that help me to inflate an XML layout as a composable instance:
#Composable
fun createAndroidViewForXMLLayout(#LayoutRes resId: Int) {
val context = LocalContext.current
val your_xml_Layout = remember(resId, context) {
LayoutInflater.from(context).inflate(resId,null)
}
AndroidView({ your_xml_Layout })
}
In your case, you just have to call this function like here:
class UserListFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val composeView = ComposeView(requireContext()).apply {
setContent {
Text(text = "Welcome in Compose-World ")
// Here you add the function with the id of your layout **"R.layout.shimmer_view"**
createAndroidViewForXMLLayout(R.layout.shimmer_view)
}
}
return composeView
}
}

Here's a composable that gives you the shimmer effect inside another composable:
#Composable
fun Shimmer(modifier: Modifier = Modifier, content: #Composable () -> Unit) {
val context = LocalContext.current
val shimmer = remember {
ShimmerFrameLayout(context).apply {
addView(ComposeView(context).apply {
setContent(content)
})
}
}
AndroidView(
modifier = modifier,
factory = { shimmer }
) { it.startShimmer() }
}
And use it as:
Shimmer {
// Your placeholder
Box(modifier = Modifier.size(100.dp).background(Color.LightGray))
}

Related

How to use signature pad library with jetpack compose using Android and Kotlin?

I was using this library to get signature https://github.com/gcacace/android-signaturepad with Android app written in Kotlin, now I moved to jetpack compose and I want to use it with the new UI library
my old code was:
XML
<com.github.gcacace.signaturepad.views.SignaturePad
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/signature_pad"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:penColor="#android:color/black"
/>
Fragment
class SignatureFragment : Fragment() {
private lateinit var signatureBinding: FragmentSignatureBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setupSignatureBinding(inflater, container)
handleSignatureClicks()
return signatureBinding.root
}
private fun setupSignatureBinding(inflater: LayoutInflater, container: ViewGroup?) {
signatureBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_signature,
container,
false
)
}
private fun handleSignatureClicks() {
signatureBinding.signaturePad.setOnSignedListener(object : SignaturePad.OnSignedListener {
override fun onStartSigning() {}
override fun onSigned() {
signatureBinding.saveButton.isEnabled = true
signatureBinding.clearButton.isEnabled = true
}
override fun onClear() {
signatureBinding.saveButton.isEnabled = false
signatureBinding.clearButton.isEnabled = false
}
})
signatureBinding.clearButton.setOnClickListener{ signatureBinding.signaturePad.clear() }
signatureBinding.saveButton.setOnClickListener {
val signatureBitmap: Bitmap = signatureBinding.signaturePad.signatureBitmap
}
}
}
I was thinking about solving this problem and I found that I could go from the compose screen to activity with intent, and I did that at the code below, but the problem is I can't back to the form to complete the rest of the questions but instead when clicking on the back button it goes to the main activity of the app
#Composable
fun SignatureQuestion(question: QuestionModel, formViewModel: FormViewModel) {
val context = LocalContext.current
CustomInputFieldContainer(
isRequired = question.is_required,
label = question.question_title
) {
Button(
onClick = {
val intent = Intent(context, SignatureActivity::class.java)
intent.putExtra("question", question)
context.startActivity(intent)
},
contentPadding = PaddingValues(),
modifier = Modifier.background(Color.Yellow)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.BottomCenter)
.padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(imageVector = Icons.Filled.Edit, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Add Signature")
}
}
}
}
Could anyone help me solve this problem?
I have been working on a updated version of this library since the original author don't seems to have no time to do it. Find updated version with Jetpack Compose support here: https://github.com/warting/android-signaturepad
If you want to access xml layout then follow below code as i did with your mentioned library:
1st create xml layout with your library tag.
and then write below lines in your #composable function:
val parser: XmlPullParser = context.resources.getLayout(R.layout.test_layout)
try {
parser.next()
parser.nextTag()
} catch (e: Exception) {
e.printStackTrace()
}
val attr: AttributeSet = Xml.asAttributeSet(parser)
val signature= remember {
SignaturePad(context,attr).apply {
id=R.id.signature_pad
}
}
AndroidView({signature}) { signaturepad->
// here you can play with signature pad
}

Jetpack Compose - LazyColumnFor not recomposing when item added

I am trying to create a list of items in Jetpack Compose that automatically updates whenever the underlying data changes, but it is not updating and I cannot add an item to the LazyColumnFor after it has initially composed.
private var latestMessages = mutableListOf<ChatMessage>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(context = requireContext()).apply {
setContent {
LatestMessages(latestMessages)
}
}
}
#Composable
fun LatestMessages(messages: MutableList<ChatMessage>) {
Column {
LazyColumnFor(items = messages) { chatMessage ->
LatestMessageItem(chatMessage) {
// onclick
}
}
}
Box(alignment = Alignment.Center) {
Button(onClick = {
latestMessages.add(
ChatMessage(
"testId",
"testText2",
"testFromId2",
"testToId",
"timestamp",
0
)
)
}) {
}
}
}
The Button in the Box is just to test adding an item. From what I understand LazyColumnFor should update whenever the underlying data is changed, similar to ForEach in SwiftUI. What am I doing wrong?
Edit March 2022
Jetpack compose no longer uses LazyColumnFor so I've updated the answer to reflect the current state of compose.
Jetpack compose always needs a state object to be modified before recomposition can occur. You need to use a MutableStateList<T>. You can pass a reference to it around and update in any method which accepts a SnapShotStateList<T>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(context = requireContext()).apply {
setContent {
val latestMessages = mutableStateListOf<ChatMessage>()
LatestMessages(latestMessages)
}
}
}
#Composable
fun LatestMessages(messages: SnapshotStateList<ChatMessage>) {
Column {
LazyColumn {
items(messages){ chatMessage ->
LatestMessageItem(chatMessage) {
// onclick
}
}
}
Box(alignment = Alignment.Center) {
Button(onClick = {
latestMessages.add(
ChatMessage(
"testId",
"testText2",
"testFromId2",
"testToId",
"timestamp",
0
)
)
}) {
}
}
}

How to use Compose inside Fragment?

The documentation describes how to create UI Jetpack Compose inside Activity.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello world!")
}
}
}
But how can I use it inside fragment?
setContent on ViewGroup is now deprecated.
The below is accurate as of Compose v1.0.0-alpha01.
For pure compose UI Fragment:
class ComposeUIFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
Text(text = "Hello world.")
}
}
}
}
For hybrid compose UI Fragment - add ComposeView to xml layout, then:
class ComposeUIFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_compose_ui, container, false).apply {
findViewById<ComposeView>(R.id.composeView).setContent {
Text(text = "Hello world.")
}
}
}
}
You don't need Fragments with Compose. You can navigate to another screen without needing a Fragment or an Activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(navController, startDestination = "welcome") {
composable("welcome") { WelcomeScreen(navController) }
composable("secondScreen") { SecondScreen() }
}
}
}
}
#Composable
fun WelcomeScreen(navController: NavController) {
Column {
Text(text = "Welcome!")
Button(onClick = { navController.navigate("secondScreen") }) {
Text(text = "Continue")
}
}
}
#Composable
fun SecondScreen() {
Text(text = "Second screen!")
}
With 1.0.x you can :
- Define a ComposeView in the xml-layout.
add a androidx.compose.ui.platform.ComposeView in your layout-xml files:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
...>
<TextView ../>
<androidx.compose.ui.platform.ComposeView
android:id="#+id/compose_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Then get the ComposeView using the XML ID, set a Composition strategy and call setContent():
class ExampleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentExampleBinding.inflate(inflater, container, false)
val view = binding.root
view.composeView.apply {
// Dispose the Composition when viewLifecycleOwner is destroyed
setViewCompositionStrategy(
DisposeOnLifecycleDestroyed(viewLifecycleOwner)
)
setContent {
// In Compose world
MaterialTheme {
Text("Hello Compose!")
}
}
}
return view
}
/** ... */
}
- Include a ComposeView directly in a fragment.
class ExampleFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
// Dispose the Composition when viewLifecycleOwner is destroyed
setViewCompositionStrategy(
DisposeOnLifecycleDestroyed(viewLifecycleOwner)
)
setContent {
MaterialTheme {
// In Compose world
Text("Hello Compose!")
}
}
}
}
}
Found it:
class LoginFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val fragmentView = inflater.inflate(R.layout.fragment_login, container, false)
(fragmentView as ViewGroup).setContent {
Hello("Jetpack Compose")
}
return fragmentView
}
#Composable
fun Hello(name: String) = MaterialTheme {
FlexColumn {
inflexible {
// Item height will be equal content height
TopAppBar( // App Bar with title
title = { Text("Jetpack Compose Sample") }
)
}
expanded(1F) {
// occupy whole empty space in the Column
Center {
// Center content
Text("Hello $name!") // Text label
}
}
}
}
}
On my mind if you want to use Jetpack Compose with fragments in a pretty way like this
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = contentView {
Text("Hello world")
}
you can create you own extension functions for Fragments
fun Fragment.requireContentView(
compositionStrategy: ViewCompositionStrategy = DisposeOnDetachedFromWindow,
context: Context = requireContext(),
content: #Composable () -> Unit
): ComposeView {
val view = ComposeView(context)
view.setViewCompositionStrategy(compositionStrategy)
view.setContent(content)
return view
}
fun Fragment.contentView(
compositionStrategy: ViewCompositionStrategy = DisposeOnDetachedFromWindow,
context: Context? = getContext(),
content: #Composable () -> Unit
): ComposeView? {
context ?: return null
val view = ComposeView(context)
view.setViewCompositionStrategy(compositionStrategy)
view.setContent(content)
return view
}
I like this approach because it looks similar to Activity's setContent { } extension
Also you can define another CompositionStrategy
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = contentView(DisposeOnLifecycleDestroyed(viewLifecycleOwner)) {
Text("Hello world")
}

How to speedup when change fragment in Android

WHY NOT ANY PEOPLE HELP TO ME???
In my application I used BottomNavBar and NavigationGraph for show some fragments!
In one of my fragments I have many views (fragment layout has 1069 lines xml codes) and when select this fragment from BottomNavBar, after some second show me this fragment.
In the other words show me this fragment with delay!
Fragment codes:
class HomeDashboardFragment : Fragment(), HomeDashboardContracts.View {
#NonNull
private var pageTitle: TextView? = null
#NonNull
private var menuIcon: TextView? = null
private lateinit var token: String
private lateinit var presenter: HomeDashboardPresenterImpl
private var giftExpireSplit: List<String> = emptyList()
private var giftExpireDate: List<String> = emptyList()
private var timeUtils: TimeUtils? = null
#NonNull
private var disposable: Disposable? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_home_dashboard, container, false)
}
#SuppressLint("WrongConstant")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Initialize
presenter = HomeDashboardPresenterImpl(requireContext(), this)
//Initialize views from activity
activity?.let { itActivity ->
pageTitle = itActivity.findViewById(R.id.toolbarMain_title)
menuIcon = itActivity.findViewById(R.id.toolbarMain_menuIcon)
//Set title
pageTitle?.let { itTitle ->
itTitle.text = getString(R.string.menuHomeDashboard)
}
//Open menu
menuIcon?.let { itMenu ->
itMenu.setOnClickListener {
itActivity.findViewById<AwesomeDrawerLayout>(R.id.homePage_drawerLayout).openDrawer(Gravity.END)
}
}
//Get token
token = GoodPrefs.getInstance().getString(PrefsKey.USER_JWT_TOKEN.name, "")
//User registered
if (GoodPrefs.getInstance().isKeyExists(PrefsKey.USER_JWT_TOKEN.name)) {
menuIcon?.visibility = View.VISIBLE
}
//Set layout
presenter.checkRegisterUser(token)
//Load profile data
if (!isEmptyString(token)) {
presenter.getProfile(token, USER_NOTIF_ID)
}
}
}
MainActivity codes for set fragments into BottomNavBar with NavigationGraph :
private fun setupNavigation() {
val navController = Navigation.findNavController(this, R.id.homePage_fragmentNavHost)
NavigationUI.setupWithNavController(homePage_bottomNavBar, navController)
}
override fun onSupportNavigateUp() = Navigation.findNavController(this, R.id.homePage_fragmentNavHost).navigateUp()
How can i fix this issue?
You can add a layout dummy consisting of a single viewGroup. Then in the onViewCreated method inflate the real layout. For example:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
inflater.inflate(R.layout.layout_dummy, container, false);
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Handler().post {
val layout = flContainer
val child = layoutInflater.inflate(R.layout.fragment_real, null)
layout.removeAllViews()
layout.addView(child)
setupToolbar()
setupWebView()
}
}
layout_dummy.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
android:id="#+id/flContainer"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
You issue is almost assuredly caused by your massive XML file. Loading all of those views takes time. You have A LOT of nesting which slows things down.
You have to optimize your layout. Remove things you don't need. Replace as many nested LinearLayouts as you can with ConstraintLayout. Maybe use a RecyclerView if this is intended to be a long, scrolling view.
Hope that helps.

How to implement shared transition element from RecyclerView item to Fragment with Android Navigation Component?

I have a pretty straightforward case. I want to implement shared element transition between an item in recyclerView and fragment. I'm using android navigation component in my app.
There is an article about shared transition on developer.android and topic on stackoverflow but this solution works only for view that located in fragment layout that starts transition and doesn't work for items from RecyclerView. Also there is a lib on github but i don't want to rely on 3rd party libs and do it by myself.
Is there some solution for this? Maybe it should work and this is just a bug? But I haven't found any information about it.
code sample:
transition start
class TransitionStartFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_transition_start, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val testData = listOf("one", "two", "three")
val adapter = TestAdapter(testData, View.OnClickListener { transitionWithTextViewInRecyclerViewItem(it) })
val recyclerView = view.findViewById<RecyclerView>(R.id.test_list)
recyclerView.adapter = adapter
val button = view.findViewById<Button>(R.id.open_transition_end_fragment)
button.setOnClickListener { transitionWithTextViewInFragment() }
}
private fun transitionWithTextViewInFragment(){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(transition_start_text to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
private fun transitionWithTextViewInRecyclerViewItem(view: View){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(view to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/transition_start_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/open_transition_end_fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="#id/transition_start_text"
android:text="open transition end fragment" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/test_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/open_transition_end_fragment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
adapter for recyclerView
class TestAdapter(
private val items: List<String>,
private val onItemClickListener: View.OnClickListener
) : RecyclerView.Adapter<TestAdapter.ViewHodler>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHodler {
return ViewHodler(LayoutInflater.from(parent.context).inflate(R.layout.item_test, parent, false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHodler, position: Int) {
val item = items[position]
holder.transitionText.text = item
holder.itemView.setOnClickListener { onItemClickListener.onClick(holder.transitionText) }
}
class ViewHodler(itemView: View) : RecyclerView.ViewHolder(itemView) {
val transitionText = itemView.findViewById<TextView>(R.id.item_test_text)
}
}
in onItemClick I pass the textView form item in recyclerView for transition
transition end
class TransitionEndFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setUpTransition()
return inflater.inflate(R.layout.fragment_transition_end, container, false)
}
private fun setUpTransition(){
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
layout
<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"
android:orientation="vertical">
<TextView
android:id="#+id/transition_end_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
fun transitionWithTextViewInFragment() - has transition.
fun transitionWithTextViewInRecyclerViewItem(view: View) - no transition.
To solve the return transition problem you need to add this lines on the Source Fragment (the fragment with the recycler view) where you initialize your recycler view
// your recyclerView
recyclerView.apply {
...
adapter = myAdapter
postponeEnterTransition()
viewTreeObserver
.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
Here is my example with RecyclerView that have fragment shared transition.
In my adapter i am setting different transition name for each item based on position(In my example it is ImageView).
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.itemView.txtView.text=item
ViewCompat.setTransitionName(holder.itemView.imgViewIcon, "Test_$position")
holder.setClickListener(object : ViewHolder.ClickListener {
override fun onClick(v: View, position: Int) {
when (v.id) {
R.id.linearLayout -> listener.onClick(item, holder.itemView.imgViewIcon, position)
}
}
})
}
And when clicking on item, my interface that implemented in source fragment:
override fun onClick(text: String, img: ImageView, position: Int) {
val action = MainFragmentDirections.actionMainFragmentToSecondFragment(text, position)
val extras = FragmentNavigator.Extras.Builder()
.addSharedElement(img, ViewCompat.getTransitionName(img)!!)
.build()
NavHostFragment.findNavController(this#MainFragment).navigate(action, extras)
}
And in my destination fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
info("onCreate")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
info("onCreateView")
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
info("onViewCreated")
val name=SecondFragmentArgs.fromBundle(arguments).name
val position=SecondFragmentArgs.fromBundle(arguments).position
txtViewName.text=name
ViewCompat.setTransitionName(imgViewSecond, "Test_$position")
}
Faced the same issue as many on SO with the return transition but for me the root cause of the problem was that Navigation currently only uses replace for fragment transactions and it caused my recycler in the start fragment to reload every time you hit back which was a problem by itself.
So by solving the second (root) problem the return transition started to work without delayed animations. For those of you who are looking to keep the initial state when hitting back here is what I did :
just adding a simple check in onCreateView as so
private lateinit var binding: FragmentSearchResultsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return if (::binding.isInitialized) {
binding.root
} else {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
with(binding) {
//doing some stuff here
root
}
}
So triple win here: recycler is not redrawn, no refetching from server and also return transitions are working as expected.
I have managed return transitions to work.
Actually this is not a bug in Android and not a problem with setReorderingAllowed = true. What happens here is the original fragment (to which we return) trying to start transition before its views/data are settled up.
To fix this we have to use postponeEnterTransition() and startPostponedEnterTransition().
For example:
Original fragment:
class FragmentOne : Fragment(R.layout.f1) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
val items = listOf("one", "two", "three", "four", "five")
.zip(listOf(Color.RED, Color.GRAY, Color.GREEN, Color.BLUE, Color.YELLOW))
.map { Item(it.first, it.second) }
val rv = view.findViewById<RecyclerView>(R.id.rvItems)
rv.adapter = ItemsAdapter(items) { item, view -> navigateOn(item, view) }
view.doOnPreDraw { startPostponedEnterTransition() }
}
private fun navigateOn(item: Item, view: View) {
val extras = FragmentNavigatorExtras(view to "yura")
findNavController().navigate(FragmentOneDirections.toTwo(item), extras)
}
}
Next fragment:
class FragmentTwo : Fragment(R.layout.f2) {
val item: Item by lazy { arguments?.getSerializable("item") as Item }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
val tv = view.findViewById<TextView>(R.id.tvItemId)
with(tv) {
text = item.id
transitionName = "yura"
setBackgroundColor(item.color)
}
}
}
For more details and deeper explanation see:
https://issuetracker.google.com/issues/118475573
and
https://chris.banes.dev/2018/02/18/fragmented-transitions/
Android material design library contains MaterialContainerTransform class which allows to easily implement container transitions including transitions on recycler-view items. See container transform section for more details.
Here's an example of such a transition:
// FooListFragment.kt
class FooListFragment : Fragment() {
...
private val itemListener = object : FooListener {
override fun onClick(item: Foo, itemView: View) {
...
val transitionName = getString(R.string.foo_details_transition_name)
val extras = FragmentNavigatorExtras(itemView to transitionName)
navController.navigate(directions, extras)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Postpone enter transitions to allow shared element transitions to run.
// https://github.com/googlesamples/android-architecture-components/issues/495
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }
...
}
// FooDetailsFragment.kt
class FooDetailsFragment : Fragment() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = MaterialContainerTransform().apply {
duration = 1000
}
}
}
And don't forget to add unique transition names to the views:
<!-- foo_list_item.xml -->
<LinearLayout ...
android:transitionName="#{#string/foo_item_transition_name(foo.id)}">...</LinearLayout>
<!-- fragment_foo_details.xml -->
<LinearLayout ...
android:transitionName="#string/foo_details_transition_name">...</LinearLayout>
<!-- strings.xml -->
<resources>
...
<string name="foo_item_transition_name" translatable="false">foo_item_transition_%1$s</string>
<string name="foo_details_transition_name" translatable="false">foo_details_transition</string>
</resources>
The full sample is available on GitHub.
You can also take a look at Reply - an official android material sample app where a similar transition is implemented, see HomeFragment.kt & EmailFragment.kt. There's a codelab describing the process of implementing transitions in the app, and a video tutorial.

Categories

Resources