Activity shared element transition using MVVM architecture - android

I'm learning MVVM pattern and have an issue right now. I have an activity A with imageview on it, and an activity B with same imageview but in another place and bigger. When in activity A i click on imageview, i want to start activity B and imageview should be shared element to achieve what i want with nice animation.
Using databinding i handle click on imageview in my view model:
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:onClick="#{(v) -> user.onAvatarClick(v)}"
android:scaleType="centerCrop"
android:src="#{user.photoUrl}"
android:transitionName="#string/avatar_transition" />
And in my viewmodel i should wrote something like this:
public void onAvatarClick(View view) {
Intent intent = new Intent(context, AvatarActivity.class);
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, view, view.getTransitionName());
context.startActivity(intent, options.toBundle());
}
But, the problem is that my viewmodel knows nothing about activity. And i have no idea how to do what i want.
One possible solution is create interface with method like startActivityB(View view) and implement it in activity and set it to field for example viewModelListener. And then i can wrote something like:
public void onAvatarClick(View view) {
viewModelListener.startActivityB(view);
}
But in this case my viewmodel will have reference to view, and it broke MVVM main idea, right?
So, what the right way to start new activity with shared element using MVVM architecture?

This can be done through Live Data in Android. As you can observe the changes in live data and navigate to other activity accordingly.
Example here
In your xml file where you clicked the button
android:onClick="#{()->homeActivityViewModel.openNewActivity()}"
In your viewModel class
private final MutableLiveData<Boolean> openNewScreen = new MutableLiveData<>();
//function that is binded to xml
public void openNewActivity() {
openNewScreen.setValue(true);
}
public MutableLiveData<Boolean> getNewScreen() {
return openNewScreen;
}
In your activity
homeActivityViewModel.getNewScreen().observe(this,
start -> {
if (start) {
Intent intent = new Intent(this, NewActivity.class);
startActivity(intent);
} });

Related

Fragment not attached to host

Let me explain the whole thing, just in case. I'm using BottomNavigationView with Jetpack's Mobile Navigation graphs and so. I reimplemented the method:
NavigationUI.setupWithNavController(...)
during navigation setup, instead of using this method I made a very similar:
customSetupWithNavController(...)
the only changes I made was to change de default fading transition between fragments, and started to using more natural (in my opinion) side slide animations, just like this in onNavDestinationSelected:
if(origin == 0){
builder.setEnterAnim(R.anim.slide_in_from_right)
.setExitAnim(R.anim.slide_out_to_left)
.setPopEnterAnim(R.anim.slide_in_from_left)
.setPopExitAnim(R.anim.slide_out_to_right);
}else if(origin == 1){
builder.setEnterAnim(R.anim.slide_in_from_left)
.setExitAnim(R.anim.slide_out_to_right)
.setPopEnterAnim(R.anim.slide_in_from_right)
.setPopExitAnim(R.anim.slide_out_to_left);
}
Where origin stands for the direction where the incoming Fragment comes from.
It came with a problem: 2 of my fragments need a FAB to add elements to a recycler, and the side slide transitioning suddenly became ugly. So I added a single FAB in MainActivity's Layout, and a logic shows the FAB only when these 2 fragments are called.
I couldn't find a nice way to pass the click event from Activity to Fragments, because I wasn't able to instantiate the Fragments, since the Navigation handles the whole process.
So, what I did was to create a ViewHolder, since I know it can survive trough lifecycle changes. This ViewHolder holds an int, and a MutableLiveData, in the MainActivity logic I pass the current selected id of the element selected by the BottomNavigationView to the int, and only if the MainActivity's FAB is clicked the live Boolean is set to true. So, in Fragments onViewCreated() I added and observer to this Boolean, when the value is set to true, and the id passed to the ViewHolder matches with the id of the current fragment, the Boolean is set back to false, and something can be done, it's something like this:
eventsNotificationHandler.getClickEvent().observe(requireActivity(), new Observer<Boolean>() {
#Override
public void onChanged(Boolean aBoolean) {
if(aBoolean && eventsNotificationHandler.getPositionId() == R.id.nav_contacts){
eventsNotificationHandler.setClickEvent(false);
//do something here
}
}
});
This notificationHandler is the ViewHolder.
So far so good, at this point I can:
1- Navigate between BottomNavigationView' Fragments freely, and the FAB shows only for needed fragments.
2- Use Log.d(...) inside the observer any time I want, and see that the debug message just fine.
3- Toast a message, any time I want, ONLY if the context parameter was initialized outside the Observer, something like this:
Toast.makeText(previouslyDefinedContext, "SOME TEXT", Toast.LENGTH_SHORT).show();
What I can't:
1- Launch an Activity whenever I want, from inside the observer by using same idea than before, ONLY initializing the context before, and outside the Observer I was able to start the intent, just like this:
eventsNotificationHandler.getClickEvent().observe(requireActivity(), new Observer<Boolean>() {
#Override
public void onChanged(Boolean aBoolean) {
if(aBoolean && eventsNotificationHandler.getPositionId() == R.id.nav_contacts){
eventsNotificationHandler.setClickEvent(false);
Intent newContact = new Intent(previouslyDefinedContext, NewContactActivity.class);
startActivity(newContact);
requireActivity().overridePendingTransition(R.anim.slide_in_from_right,R.anim.slide_out_to_left);
}
}
});
But in this particular case I can launch the new Activity as many times as I want, BUT ONLY if I navigate directly to this particular fragment where the observer is defined after the app opens, if I decide to navigate first trough some other fragments instead, and then I go to this fragment to try to launch the Activity, the app crashes
I've noticed that this exact behavior happens when I call requireContext() from inside the Observer, it works but then stops working.
The app crashes with:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: cu.arrowtech.bpawreckproject, PID: 18019
java.lang.IllegalStateException: Fragment FragmentContacts{883259b} (9f127bdb-127d-4366-b90b-c8900a5a771e)} not attached to Activity
What I want:
1- A right way to launch a new Activity from inside a Fragment, by pressing a FAB in MainActivity, if it's possible.
2- A nice way to switch fragments if a possible solution implies to change the logic I have already.
3- Keep using Jetpack and Navigation Graphs.
I'm able to do what I want by using 2 separate FABs in each Fragment, but the transitioning is not nice and beautiful.
I'm open to suggestions, even if that implies to change the logic. I'm almost certain it must be a better way to do what I'm doing, instead of using ViewHolder for this purpose.
I would like to get something similar to the Google Pay, it seems to be that the Buttons for adding payment method, passes, and transfers is the same button, but it adapts to each situation.
After some reasearch I did found a way to keep fluid transitions between fragments AND Mobile Navigation components AND a single FAB in the MainActivity layout.
What I did was to use an Interface instead of ViewModels (I always knew that approach was wrong):
public interface SharedViewsInterface {
void onFabClicked();
}
So in my MainActivity I defined that interface as null, and created a method to define it according to the situation. My MainActivity looks like:
public class MainActivity extends AppCompatActivity {
//UI elements
private FloatingActionButton main_fab;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Same dode to get the custom animations
//.......
//Main and only Fab configuration
main_fab = findViewById(R.id.main_fab);
main_fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if(sharedViewsInterface !=null){
sharedViewsInterface.onFabClicked();
}
}
});
}
//Other auxiliary methods
public void setInterface(SharedViewsInterface sharedViewsInterface){
this.sharedViewsInterface = sharedViewsInterface;
}
}
By doing this, I can, from each fragment to implement the Interface in onCreate by doing this:
((MainActivity) requireActivity()).setInterface(new SharedViewsInterface() {
#Override
public void onFabClicked() {
Intent newContact = new Intent(requireContext(), NewContactActivity.class);
startActivity(newContact);
requireActivity().overridePendingTransition(R.anim.slide_in_from_right_short,R.anim.slide_out_to_left_medium);
}
});
This works well becouse the FAB is shown only when a fragment with Interface implementation is visible, see my example gif

How to start a fragment from a RecyclerView Adapter which is inside another fragment?

I have a tab view with two fragments. Those two fragments contain a recycler view with cards.
Each card in both fragments had a button.
Clicking on fragment 1's button should open the fragment 2 as a separate page and vice-versa.
I am struggling to find a method to implement this without making every too complex and tightly coupled.
This is fragment one with its own Adapter.
And this is fragment two:
Clicking on that SELECT DONOR button in Donees page should open donor fragment in a new page where the user will be able to assign a donor for the selected donee.
So I have two needs here
1) To start a fragment from a fragment
2) To Keep track from which Donee the new donor page was opened so that I can assign a donor for that specific donee.
I hope this is understandable.
so far I have tried LocalBroadcast and FragmentManager but its hard to keep track of what I'm doing with the code.
Can you guys suggest a better technique to achieve this ?
the easiest solution would probably be, starting a new activity, passing something like an ID, name or something to the intent on an Button click.
Context.startActivity(new Intent(Context, YourAssigneeActivity.class)
.putExtra("ID",id));
So I assume that you do not switch to the other tab when you click a button on one tab. Therefore the fragment should fill the whole screen.
With this assumption in mind you most likely have to switch the Activity. This can be dones easily with an Intent:
Intent intent = new Intent(getActivity(), ActivityB.class)
intent.putExtra("KEY", <your required data to transfer>);
getActivity().startActivityForResult(intent);
Note that when you use putExtra() don't forget that you need to implement Parcelable in those objects (explained here)
To get to know which item was clicked you can use the following pattern (pseudocode - I personally think it's really clean):
FragmentA implements YourAdapter.callback {
onItemClicked(<YourObject> item) {
<starting new activity as described above>
}
}
class YourAdapter extends RecyclerView.Adapter {
Callback mCallback;
YourAdapter(Context context, otherStuff) {
mCallback = (Callback) context;
}
interface Callback {
onItemClicked(<YourObject> item)
}
YourViewHolder implements View.OnClickListener {
onClick(View v) {
mCallback.onItemClicked(<YourObject> item)
}
}
}
Once you are in your Activity, you can set the Fragment in onCreate() of your Activity. In the Activity retrieve the data with getIntent() in the onCreate before creating the Fragment. Then you can put your data in the Fragment with setArguments(<Bundle>). In the Fragment in the onCreateview() retrieve it with getArguments().
I know this is kind of conmplicated. A better solution would be to just switch to an Activity and forget about the Fragment. This would remove one layer of complexity.
If you directly go from Fragment to Fragment you can ignore the Activity part but the rest should stay the same.
Hope this was the answer you were looking for!
Edit: Note that mCallback = (Callback) context is null if the Activity is not implementing Callback

Shared element transition : activity into fragment nested in another activity

i am trying to add shared element transition into my app.
Scenario is that user clicks on image thumbnail which than opens another activity with full screen image view.
This works fine if shared view is hosted directly within layout of target activity. Works smoothy for enter/exit animation.
But when i'am trying to achieve similar effect within fragment which is nested in target activity this approach doesn't work. Funny thing is that enter animation is not showed, but exit animation is working fine .
Another even more complicated view hierarchy is that if target view (ImageView) is hosted within view pager which is hosted in frame layout of target activity.
Does someone had same issue ?
Edit:
My click listener code
public class OnClickPicture extends OnClickBase {
private ObjectPicture object;
public OnClickPicture(Activity_Parent activity, ObjectPicture object) {
super(activity);
this.object = object;
}
public void onClick(View v) {
picasso.load(object.getFullUrl()).fetch();
Intent intent = new Intent(activity, ActivityPicture.class);
intent.putExtra("picture_object", helper.gson.toJson(object));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && v != null) {
Pair<View, String> p1 = Pair.create(v, "image");
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, p1);
activity.startActivity(intent, options.toBundle());
} else {
activity.startActivity(intent);
}
}
}
The way that transitions work require the new Activity to be created, measured and laid out before any animations can happen. That's so that it can find the view that you want to animate and create the appropriate animation.
In your case this isn't happening because, as stated in the docs, all FragmentTransaction.commit() does is schedule work to be done. It doesn't happen immediately. Therefore when the framework creates your Activity it cant find the view that you want to animate. That's why you don't see an entry animation but you do see an exit animation. The View is there when you leave the activity.
The solution is simple enough. First of all you can try FragmentManager.executePendingTransactions(). That still might not be enough. The transitions framework has another solution:
In the onCreate of Activity postponeEnterTransition(). This tells the framework to wait until you tell it that its safe to create the animation. That does mean that you need to tell it that its safe (via calling startPostponedEnterTransition()) at some point. In your case that would probably be in the Fragments onCreateView.
Here's an example of how that might look like:
Activity B
#Override
protected void onCreate(Bundle savedInstanceState) {
// etc
postponeEnterTransition();
}
Fragment B
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View sharedView = root.findViewById(R.id.shared_view);
sharedview.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
sharedview.getViewTreeObserver().removeOnPreDrawListener(this);
getActivity().startPostponedEnterTransition();
return true;
}
});
}
Thanks to Alex Lockwood for his detailed blog posts about the Transitions framework.

Animate search bar to toolbar android

I'm trying to build a search bar similar to Flipboard. This search bar animates from below the toolbar to cover the toolbar. This GIF shows it better than I can explain:
Does anyone know if this is standard Material Design? And if so, are there any libraries or standard widgets I can use to do this? Soundcloud also does this so just wanted to ask if there was anything already out there. If not I'll just have to implement it myself.
Thanks!
To solve this I'd recommend looking into android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAnimation().
Here are the snippets from the code I remember:
I have ActivityA which contains an EditTextA.
The EditTextA has a View.OnClickListener:
View.OnClickListener() {
#Override
public void onClick(View view) {
ActivityB.launch(getActivity(), view);
}
}
which when invoked calls a static method: ActivityB.launch(Activity a, View transitionView):
public static void launch(Activity activity, View transitionView) {
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(
activity, transitionView, EXTRA_TRANSITION_VIEW);
Intent intent = new Intent(activity, ActivityB.class);
ActivityCompat.startActivity(activity, intent, options.toBundle());
}
The code above is basically creating an Intent that will launch ActivityB. That Intent also includes a Bundle (options.toBundle()) which has a bunch of stuff, including the view that is transitioning from ActivityA to ActivityB. In my case this is the EditTextA.
In ActivityB.onCreate() all we need to do is "connect" that view to the new view it's transitioning to. So in my case, EditTextA is transitioning to another EditTextB that lives in ActivityB.
ActivityB.onCreate() {
....
mEditTextB = (EditText) findByViewId(...);
ViewCompat.setTransitionName(mEditTextB, EXTRA_TRANSITION_VIEW);
}
If I remember everything correctly that should be it :).

Subactivity onClick Firing in Parent Class

I am having some trouble with the onClick handler in a sub activity of an ActivityGroup.
I am launching a StoreActivity using:
Intent storeIntent = new Intent(this, StoreActivity.class);
storeIntent.putExtra(StoreActivity.INTENT_STORE_ID, storeId);
View newVeiw = getLocalActivityManager().startActivity("StoreActivity", storeIntent).getDecorView();
setContentView(newVeiw);
Log.e("DEBUG", "current activity: " + getLocalActivityManager().getCurrentActivity().toString());
In the StoreActivity layout I have a button which defines an onClick method. For some reason however, it is trying to call this in the parent class that launched StoreActivity. Am I doing something wrong when launching the activity? The output of Log.e above says that StoreActivity is the current activity so I am a bit lost as to why this is happening. I can get around this by defining an onClickListener for the button in code in StoreActvity but I would like to avoid that if possible.
I think this is because you are calling setContentView from the parent activity instead of the subactivity. Why don't you just start the activity in the intent instead and set the content view in the new activity? It would be much simpler.
Try this:
Intent storeIntent = new Intent(this, StoreActivity.class);
storeIntent.putExtra(StoreActivity.INTENT_STORE_ID, storeId);
startActivity(storeIntent);
and then in StoreActivity.java do:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
View newVeiw = getLocalActivityManager().startActivity("StoreActivity", storeIntent).getDecorView();
setContentView(newView); //not sure if this would work, would probably be easier to put your xml layout file in here.
}
Ok I have solved this. The problem was unrelated to any of this code. I had a common base class to my activities and in that I had accidentally made the inflater a singleton. This meant that all inflated layouts belonged to the first class that created that singleton instance which happened to be the class that was incorrectly receiving the onClick event. Removing that singleton resolved this.

Categories

Resources