My activities extend from a generic base activity, where I declare and initialize public variables like context of type Context, activity of type Activity and mActionBar of type ActionBar.
So this avoids redundant initialization code in all my app's activities.
But with the advent of Toolbar, I am a little confused on how to do this. Toolbar is not like ActionBar and replaces it, but also extends it.
The ActionBar is a view object that is always available for retrieval, by ActionBar activity, and it sits above the views that are created. This is not declared in layout XML anywhere.
But Toolbar is declared only in layout XML, so I have to include it in each and every layout I create, or else I will not be able to access the Toolbar object.
I typically use setContentView(R.layout.mylayout) in the onCreate method of each individual activity. And then I have to initialize my Toolbar object after that using findViewById. Therefore I can't put this code in my BaseActivity's onCreate function because the setContentView wouldn't have been initialized yet.
Even if I created Toolbar programmatically with it's constructor, and attempted to add the view to the top of the hierarchy, I would still have to do this on a layout by layout, and activity by activity basis, because some layouts are RelativeLayout's as the root object, and others are different. So these will still have separate code considerations.
The reason I am curious about a good way for my activities to inherit Toolbar, is because it is a complete nightmare for Google to suddenly require Android 4.0-4.4 devices to use the v7 compatibility pack, replace the actionbar completely with the Toolbar object, use v4 compatibility pack fragments instead of native ones, all to use the latest design paradigms.
this is my implementation. Hope it helps someone.
public abstract class BaseActivity extends AppCompatActivity {
Toolbar toolbar;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
protected boolean useToolbar() {
return true;
}
#Override
public void setContentView(int layoutResID) {
View view = getLayoutInflater().inflate(layoutResID, null);
configureToolbar(view);
super.setContentView(view);
}
private void configureToolbar(View view) {
toolbar = (Toolbar) view.findViewById(R.id.toolbar);
if (toolbar != null) {
if (useToolbar()) {
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
} else {
toolbar.setVisibility(View.GONE);
}
}
}
}
From here on you just extend BaseActivity. If you don't want a toolbar you will have to override the useToolbar().
Don't forget to add in activity.xml at the top
<include layout="#layout/toolbar" />
toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<merge 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="wrap_content">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
</merge>
Related
I have few fragments and one activity. The app contains toolbar which is always visible. There is icon on my toolbar. And i need to hide this icon when user opens 2,4,5 fragments and show this icon when users open 1 and 3 fragment. I don't need all code for this logic, I need advice how can I implement it and where add some logic for this behavior
The following is true assuming you are using jetpack's navigation and single activity:
Add destination change listener to your main nav controller inside your activity (addOnDestinationChangedListener, the interface is NavController.OnDestinationChangedListener). Inside the listener, you could check for destination.id in onDestinationChanged implementation. Actually, you could create two sets like this
private val twoFourFiveDestinations =
setOf(R.id.two, R.id.four, R.id.five)
private val oneThreeDestinations =
setOf(R.id.one, R.id.three)
just to make check like this if(twoFourFiveDestinations.contains(destination.id) ... and manage your icon visibility accordingly, it will make life easier.
Alternative solution would be to hand over icon management to fragments. You could define some interface for comm with activity, and manage toolbar icon when respective fragment is up and running. But you'd need to do that in every fragment of your question.
Step 1 : create an interface FragmentListener with one method :
public interface MyFragmentListener {
void onChangeToolbarImage(boolean show);
}
Step 2 : implement in YourActivity :
ImageView toolarImage= findViewById(R.id.toolbarimage)()///
#Override
public void onChangeToolbarImage(boolean show) {
if()
{ //check your imageView is Visible or not
toolbarImage.setVisibility(show); //change your ImageView's visibility
}
}
Step 3 : in each fragment you need get instance from interface :
private MyFragmentListener fragmentListener;
Step 4 : override onAtach in your Fragment :
#Override
public void onAttach(#NonNull Context context) {
super.onAttach(context);
if (context instanceof MyFragmentListener)
fragmentListener = (MyFragmentListener) context;
fragmentListener.onChangeToolbarTitleImage(true or false);
}
Create a companion object or static variable if you are using java.
class Util {companion object { lateinit var toolbarImg: ImageView }}
Inside your Main Activity onCreate initialize your toolbar and imageview
toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
Util.toolbarImg = toolbar.findViewById(R.id.cartImg)
XML
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="#style/AppTheme.PopupOverlay">
<ImageView
android:id="#+id/cartImg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:visibility="visible"
/>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
Now all you need to do is control the visibility of this ImageView.
On Fragment transaction
To Hide
if(Util.toolbarImg.visibility == View.VISIBLE){
Util.toolbarImg.visibility = View.GONE }
According to the navigation priciples the first destination in your app should be the screen your users would normally see when they launch the app after signup/login or any other conditional navigation, I called that start destination 'homeFragment'.
Following this principle and after reading the post on conditional navigation by Maria Neumayer I am facing some issues with the Toolbar and the back navigation when going through the conditional navigation flows.
I am architecting the app using one single activity with a ConstraintLayout, a Toolbar and the NavHostFragment:
<android.support.constraint.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"
tools:context=".ui.activities.NavigationTestActivity">
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<fragment
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/nav_graph"
app:defaultNavHost="true"
/>
</android.support.constraint.ConstraintLayout>
The main graph looks like this with the home destination as start destination, connected with an action to a detail fragment (this action is triggered from a button) and a conditional navigation implemented using a nested graph:
Main Graph
I called this nested graph welcomeGraph and it includes the screens for login or signup, you can see it here:
Welcome Nested Graph
In the homeFragment onResume I check if the login/signup has been completed (determined by a dummy boolean stored in sharedPrefs) and if not I launch the welcome nested graph for signup/login.
In the login destination I have a 'Completed' button which sets the dummy boolean in sharedPrefs as true and triggers an action popToWelcomeGraph (inclusive) which should close the whole nested graph and take me back to the homeFragment (this works).
PROBLEM - Toolbar issue in nested graph:
As the Welcome graph is lauched inmediatelly after user lands in the app, the toolbar should not display a back/up arrow in the first destination of that nested graph, instead it should feel as if it was the first screen on the app, and tapping back should quit the app.
QUESTION: Is it possible to alter the Toolbar here to simulate the first screen in the nested graph is the first screen in the app until the login/signup is completed? Would this be a bad practice?
I have recently solved this issue by creating an AppBarConfiguration and setting up the toolbar with that configuration. The AppBarConfiguration takes in the top level destination ids as parameter. With that being said, you can do something like
private lateinit var appBarConfiguration: AppBarConfiguration
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
appBarConfiguration = AppBarConfiguration(setOf(R.id.homeFragment,R.id.welcomeFragment))
findViewById<Toolbar>(R.id.toolbar).setupWithNavController(navController, appBarConfiguration)
}
This way the back button won`t be shown on top level fragments.
You have to implement communicator interface like below
interface ActivityCommunicator {
void alterToolbar();
}
and implement it to your activity class like below
class HomeActivity extends AppCompatActivity implements ActivityCommunicator {
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Activity code
}
#Override
public void alterToolbar() {
ActionBar actionBar = getSupportActionBar();
// False to hide back button and true to show it
actionBar.setDisplayHomeAsUpEnabled(false);
}
}
and from your fragment you can call it like below
public class MainFragment extends Fragment {
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
ActivityCommunicator activityCommunicator = (ActivityCommunicator) getActivity();
activityCommunicator.alterToolbar();
// Fragment code
return super.onCreateView(inflater, container, savedInstanceState);
}
}
You can change alterToolbar() implementation as per your need
What I am trying to achieve is to add a Toolbar to my Fragment UI when my Activity also has a bar that is NOT shown inside the fragment. Basically, how can my Activity and Fragment(s) have their own toolbar?
This is my code in my activity
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mToolbar = (Toolbar) findViewById(R.id.my_toolbar);
setSupportActionBar(mToolbar);
}
And here is my code in my fragment
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mView = inflater.inflate(R.layout.fragment_layout, container, false);
mToolbar = (Toolbar) mView.findViewById(R.id.fragment_toolbar);
((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar);
return mView;
}
What I get is that when I create the toolbar in my Fragment, the toolbar is the toolbar from the activity. What I want to do is I want the fragment to have it's separate toolbar from it's parent Activity.
Here are a few things that I can think of which are definitely not ideal:
Make the fragment an activity.
Simulate as if I have created a toolbar but simply have the Fragment toolbar defined as a UI element that I have pieced together myself.
Somehow edit the parent Activity's toolbar to make it specific to the fragment, and then somehow redraw the Activity toolbar? This seems weird and I am not even sure if it's possible.
Having 2 toolbars in one activity is not advisable since it'll look really bad.
with ((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar);
you're replacing the activities toolbar with the one that you're defined in the fragment's layout file.
I would suggest you to use fragment specific actions for the toolbar when the fragment is visible on the screen and change it's behavior by accessing the content of the toolbar by
Toolbar mToolbar = ((AppCompatActivity) getActivity()).getSupportActionBar();
now use the mToolbar to change the actions
The structure of my application is just an Activity which shows Fragment one at a time. The layout of the activity is defined by an xml file where it's present a FloatingActionButton. Fragments should intercept click events on the button, but the onClick() is never called.
Activity xml layout:
...
<android.support.design.widget.FloatingActionButton
android:id="#+id/fab"
android:visibility="visible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
...
Fragment class:
...
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
adapterCity = new AdapterCity(getContext(), 0, Main.cities);
setListAdapter(adapterCity);
colorAccent = getContext().getResources().getColor(R.color.colorAccent);
fab = (FloatingActionButton) getActivity().findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
openMap();
}
});
fab.setImageDrawable(getResources().getDrawable(R.drawable.ic_bubble_chart_black_24dp));
}
...
It seems that UI events from the activity aren't propagated to the attacched fragments. What is happening here? How can I solve my problem?
This is not a good practice. Fragments should not know directly about hosting activity nor should they access the views from them. This can be done by having your activity implement a custom interface which has some methods in it, for example myMethod(), and then calling getActivity() and typecasting it to the interface and the calling the method, like this:
((MyInterface)getActivity()).myMethod();
Now for your particular case, I'd suggest that you pass some kind of a variable which will differ in each of the fragments and based on that in you Activity's implementation of myMethod() set different onClickListeners to your FloatingActionButton.
I'm starting a new project that uses the AppCompat/ActionBarCompat in v7 support library. I'm trying to figure out how to use the getSupportActionBar from within a fragment. My activity that hosts the fragment extends ActionBarActivity, but I don't see a similar support class for Fragments.
From within my fragment
public class CrimeFragment extends Fragment {
//...
getActivity().getSupportActionBar().setSubtitle(R.string.subtitle); // getSupportActionBar is not defined in the v4 version of Fragment
//...
}
The google page for using it (http://android-developers.blogspot.in/2013/08/actionbarcompat-and-io-2013-app-source.html) says there should be no changes for the v4 fragment. Do I need to cast all my getActivity() calls to an ActionBarActivity? That seems like poor design.
After Fragment.onActivityCreated(...) you'll have a valid activity accessible through getActivity().
You'll need to cast it to an ActionBarActivity then make the call to getSupportActionBar().
((AppCompatActivity)getActivity()).getSupportActionBar().setSubtitle(R.string.subtitle);
You do need the cast. It's not poor design, it's backwards compatibility.
While this question has an accepted answer already, I must point out that it isn't totally correct: calling getSupportActionBar() from Fragment.onAttach() will cause a NullPointerException when the activity is rotated.
Short answer:
Use ((ActionBarActivity)getActivity()).getSupportActionBar() in onActivityCreated() (or any point afterwards in its lifecycle) instead of onAttach().
Long answer:
The reason is that if an ActionBarActivity is recreated after a rotation, it will restore all Fragments before actually creating the ActionBar object.
Source code for ActionBarActivity in the support-v7 library:
#Override
protected void onCreate(Bundle savedInstanceState) {
mImpl = ActionBarActivityDelegate.createDelegate(this);
super.onCreate(savedInstanceState);
mImpl.onCreate(savedInstanceState);
}
ActionBarActivityDelegate.createDelegate() creates the mImpl object depending on the Android version.
super.onCreate() is FragmentActivity.onCreate(), which restores any previous fragments after a rotation (FragmentManagerImpl.dispatchCreate(), &c).
mImpl.onCreate(savedInstanceState) is ActionBarActivityDelegate.onCreate(), which reads the mHasActionBar variable from the window style.
Before mHasActionBar is true, getSupportActionBar() will always return null.
Source for ActionBarActivityDelegate.getSupportActionBar():
final ActionBar getSupportActionBar() {
// The Action Bar should be lazily created as mHasActionBar or mOverlayActionBar
// could change after onCreate
if (mHasActionBar || mOverlayActionBar) {
if (mActionBar == null) {
... creates the action bar ...
}
} else {
// If we're not set to have a Action Bar, null it just in case it's been set
mActionBar = null;
}
return mActionBar;
}
If someone uses com.android.support:appcompat-v7: and AppCompatActivity as activity then this will work
((AppCompatActivity)getActivity()).getSupportActionBar().setSubtitle(R.string.subtitle);
For those using kotlin,
(activity as AppCompatActivity).supportActionBar.setSubtitle(R.string.subtitle)
As an updated answer for Pierre-Antoine LaFayette's answer
ActionBarActivity is deprecated; use AppCompatActivity instead
((AppCompatActivity)getActivity()).getSupportActionBar();
in your fragment.xml add Toolbar Tag from support library
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="#style/ThemeOverlay.AppCompat.Light" />
Now how we can control it from MyFragment class? let's see
inside onCreateView function add the following
mToolbar = (Toolbar) view.findViewById(R.id.toolbar);
((AppCompatActivity)getActivity()).setSupportActionBar(mToolbar);
//add this line if you want to provide Up Navigation but don't forget to to
//identify parent activity in manifest file
((AppCompatActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
and if you want to add items to the toolbar within MyFragment
you must add this line inside onCreateView function
setHasOptionsMenu(true);
this line is important, if you forget it, android will not populate your menu Items.
assume we identify them in menu/fragment_menu.xml
after that override the following functions
#Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.fragment_menu, menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
switch (id) {
case R.id.action_1:
// do stuff
return true;
case R.id.action_2:
// do more stuff
return true;
}
return false;
}
hope this helps
As an addendum to GzDev's answer, if you already have the string, you can use kotlin's auto-setter:
(activity as AppCompatActivity).supportActionBar?.subtitle = my_string
And you can turn it off by simply using an empty string.
Note that this works for both the title and the subtitle.