I have a bottom sheet dialog fragment that has an information (i) icon. On clicking that, We navigate to another scene(layout) which shows the details. I'm using the transition manager class to change the scenes. There is an Ok button on the second scene on clicking that we return to the first scene. After returning, I see the state in the first scene is erased. Not sure how I should restore it.
Here "scene layout" is the original layout and "scene info" is the info layout.
//initialising scene
private fun initScene() {
sceneLayout =
Scene.getSceneForLayout(
binding.root,
R.layout.fragment_polling,
requireContext().applicationContext
)
sceneInfo =
Scene.getSceneForLayout(
binding.root,
R.layout.fragment_polling_info,
requireContext().applicationContext
)
}
private fun fadeLayout(scene: Scene) {
TransitionManager.go(scene, fadeTransition)
if (scene == sceneInfo) {
setInfoClickListeners()
} else {
initView()
initScene()
}
}
//click listener for second scene button ok got it.
private fun setInfoClickListeners() {
sceneInfo.sceneRoot.findViewById<Button>(R.id.btn_ok_got_it).setOnClickListener {
fadeLayout(sceneLayout)
}
}
//launch info scene
private fun initInfoIcon(){
binding.includeOpinions.imgPotentialWin.setOnClickListener {
fadeLayout(sceneInfo)
}
}
i've a problem in my Android app.
I have a fragment that is generated as a list of editable textview. Every textview has a setOnFocusChangeListener that calls an API on server.
FocusChangedListener works correctly on last textview, and after that my textview remain focused as you can see in the figure below.
Problem
Now i have to change menu (fragment) by clicking on the right menu button.
When button is clicked and the new menu is about to be loaded, my textview loses focus and calls the API, but i don't want my app do that. If I click menu button is because i've to change it and i expect that my old fragment disappear or basically don't execute the FocusChangedListener.
Any ideas?
I made a little trick to solve the problem.
I just added this code inside OnCreate of my DrawerMenu and then i set a Global Variable "drawerOpened" to check every time if my Menu is open or not.
// Initialize the action bar drawer toggle instance
val drawerToggle:ActionBarDrawerToggle = object : ActionBarDrawerToggle(
this,
drawer_layout,
toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close
){
override fun onDrawerClosed(view:View){
super.onDrawerClosed(view)
GlobalVar.drawerOpened = false
}
override fun onDrawerOpened(drawerView: View){
super.onDrawerOpened(drawerView)
GlobalVar.drawerOpened = true
currentFocus?.clearFocus() //clear focus - any view having focus
}
}
Then in my HomeFragment i did this:
// if GlobalVar.drawerOpened is false then setOnFocus
if (field.validatefield || field.nextpage != 0)
{
textView.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus && !GlobalVar.drawerOpened)
{
if ((field.mandatory && !textView.text.toString().isNullOrEmpty()) || (!field.mandatory)) {
if (field.validatefield) {
validateField(field.fieldcode, textView.text.toString(), field.nextpage)
} else {
_goToNextPageNo = field.nextpage
goToNextPageIfExist()
}
}
}
}
}
This is not a complete solution, but it works fine.
I assume that after a click, the application switches to touch mode and your EditText loses focus.
It happens at the system level.
A focus can have only one view at a time.
https://android-developers.googleblog.com/2008/12/touch-mode.html
i recently faced a pretty strange issue with activity transitions. I'm trying to make a slide in transition with exclusions, which are native status bar, native navigation bar and my custom bottom navigation bar. So i created piece of code based on documentation (link here) which in most cases works pretty well, but sometimes all views in the activity have visibility set as View.INVISIBLE (i know that fact from Layout Inspector).
So, i have activity A and activity B. Then i'm navigating from activity A to B via startActivity(). When i click back on activity B sometimes transition works fine, but in some cases all views on activity A (except bottom navigation bar, which was excluded from transition) are set to View.INVISIBLE
My code:
Activity A :
Intent i = new Intent(this, ActivityB.class);
Transition slide = new Slide(Gravity.RIGHT);
slide.excludeTarget(findViewById(R.id.bottom_navigation_bar), true);
slide.excludeTarget(findViewById(android.R.id.navigationBarBackground), true);
slide.excludeTarget(findViewById(android.R.id.statusBarBackground), true);
getWindow().setExitTransition(slide);
startActivity(i,
ActivityOptionsCompat.makeSceneTransitionAnimation(this, createPairs()).toBundle());
private Pair[] createPairs() {
Pair[] pairs = new Pair[2];
pairs[0] = Pair.create(findViewById(android.R.id.statusBarBackground), Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME);
pairs[1] = Pair.create(findViewById(android.R.id.navigationBarBackground), Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME);
return pairs;
}
Activity B:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
setContentView(R.layout.activity);
final View decor = getWindow().getDecorView();
decor.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
#Override
public boolean onPreDraw() {
decor.getViewTreeObserver().removeOnPreDrawListener(this);
Transition fade = new Fade();
fade.excludeTarget(findViewById(R.id.bottom_navigation_bar), true);
fade.excludeTarget(findViewById(android.R.id.navigationBarBackground), true);
fade.excludeTarget(findViewById(android.R.id.statusBarBackground), true);
getWindow().setEnterTransition(fade);
return true;
}
});
I'm trying to change the style attribute "colorControlNormal" of my app programmatically and during runtime, but I didn't have any results.
This property is the color that will tint the hamburger & back icons of the new Toolbar viewGroup. Beside, I'm using the v7 compatibility library.
I heard that we cannot change app theme during runtime, but I'm looking for an answer, even if it's not so clean way.
Edit:
I just figured that gmail is doing what i want, when you click on the search icon, the white hamburger icon turn into grey back.
Waiting for more.
I spent one day, played with different implementation. So my opinion, the best way todo that it copy paste DrawerArrowDrawable from AppCompat v7 library.
https://gist.github.com/IstiN/5d542355935fd7f0f357 - take a look on the code with some optimization
than you can use it in your main activity with code below
DrawerArrowDrawable drawable = new DrawerArrowDrawable(this, this);
ImageView menuButton = (ImageView) findViewById(R.id.arrow);
menuButton.setImageDrawable(drawable);
menuButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
((DrawerLayout)findViewById(R.id.drawer)).openDrawer(Gravity.START);
}
});
when you start new fragment, you need to create one more view on the same place and add second code to your fragment
private DrawerArrowDrawable mArrowDrawable;
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mArrowDrawable = new DrawerArrowDrawable(getActivity(), getActivity());
ImageView topButton = (ImageView) view.findViewById(R.id.arrow);
topButton.setImageDrawable(mArrowDrawable);
topButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
closeSearch();
}
});
//run animation from hamburger to arrow
animate(0, 1, null);
....
private void animate(int startValue, int endvalue, Animator.AnimatorListener listener) {
ValueAnimator anim = ValueAnimator.ofFloat(startValue, endvalue);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float slideOffset = (Float) valueAnimator.getAnimatedValue();
mArrowDrawable.setProgress(slideOffset);
}
});
anim.setInterpolator(new DecelerateInterpolator());
anim.setDuration(300);
if (listener != null) {
anim.addListener(listener);
}
anim.start();
}
to make animation from arrow to hamburger handle back button and execute code
animate(1, 0, null);
you also need to wait in your fragment while animation will not finish, but it another questions.
If you have any questions ask in comments.
I've followed Google's official developer tutorials here to create a navigation drawer.
At the moment, everything works fine, except for when the user uses the native back button Android provides at the bottom of the screen (along with the home and recent app buttons). If the user navigates back using this native back button, the navigation drawer will still be open. If the user instead navigates back using the ActionBar, the navigation drawer will be closed like I want it to be.
My code is nearly identical to the official tutorials, except for how I handle the user selecting an item on the drawer:
mDrawerList.setOnItemClickListener(new ListView.OnItemClickListener()
{
#Override
public void onItemClick(AdapterView parent, View view, int position, long id)
{
switch(position)
{
case 0:
{
Intent intent = new Intent(MainActivity.this, NextActivity.class);
startActivity(intent);
}
}
}
});
How can I have the navigation drawer be closed when the user navigates back using the native back button? Any advice appreciated. Thanks!
You have to override onBackPressed(). From the docs :
Called when the activity has detected the user's press of the back
key. The default implementation simply finishes the current activity,
but you can override this to do whatever you want.
So you can have code like this :
#Override
public void onBackPressed() {
if (this.drawerLayout.isDrawerOpen(GravityCompat.START)) {
this.drawerLayout.closeDrawer(GravityCompat.START);
} else {
super.onBackPressed();
}
}
If is open this method closes it, else falls back to the default behavior.
You need to override onBackPressed() in your activity and check for the condition where the navigation drawer is open. If it is open, then close it, else do a normal back pressed method. Here is some code mixed with some pseudocode to help you:
#Override
public void onBackPressed(){
if(drawer.isDrawerOpen()){ //replace this with actual function which returns if the drawer is open
drawer.close(); // replace this with actual function which closes drawer
}
else{
super.onBackPressed();
}
}
To replace the pseudocode look in the documentation for the drawer. I know both those methods exist.
Here is an alternative solution to your problem.
#Override
public void onBackPressed(){
if(drawerLayout.isDrawerOpen(navigationView)){
drawerLayout.closeDrawer(navigationView);
}else {
finish();
}
}
UPDATE:
As of support library 24.0.0 this is possible without any workarounds. Two new openDrawer and closeDrawer methods have been added to DrawerLayout that allow the drawer to be opened or closed with no animation.
You can now use openDrawer(drawerView, false) and closeDrawer(drawerView, false) to open and close the drawer with no delay.
If you call startActivity() without calling closeDrawer(), the drawer will be left open in that instance of the activity when you navigate back to it using the back button. Calling closeDrawer() when you call startActivity() has several issues, ranging from choppy animation to a long perceptual delay, depending on which workaround you use. So I agree the best approach is to just call startActivity() and then close the drawer upon return.
To make this work nicely, you need a way to close the drawer without a close animation when navigating back to the activity with the back button. (A relatively wasteful workaround would be to just force the activity to recreate() when navigating back, but it's possible to solve this without doing that.)
You also need to make sure you only close the drawer if you're returning after navigating, and not after an orientation change, but that's easy.
Details
(You can skip past this explanation if you just want to see the code.)
Although calling closeDrawer() from onCreate() will make the drawer start out closed without any animation, the same is not true from onResume(). Calling closeDrawer() from onResume() will close the drawer with an animation that is momentarily visible to the user. DrawerLayout doesn't provide any method to close the drawer without that animation, but it's possible to extend it in order to add one.
Closing the drawer actually just slides it off the screen, so you can effectively skip the animation by moving the drawer directly to its "closed" position. The translation direction will vary according to the gravity (whether it's a left or right drawer), and the exact position depends on the size of the drawer once it's laid out with all its children.
However, simply moving it isn't quite enough, as DrawerLayout keeps some internal state in extended LayoutParams that it uses to know whether the drawer is open. If you just move the drawer off screen, it won't know that it's closed, and that will cause other problems. (For example, the drawer will reappear on the next orientation change.)
Since you're compiling the support library into your app, you can create a class in the android.support.v4.widget package to gain access to its default (package-private) parts, or extend DrawerLayout without copying over any of the other classes it needs. This will also reduce the burden of updating your code with future changes to the support library. (It's always best to insulate your code from implementation details as much as possible.) You can use moveDrawerToOffset() to move the drawer, and set the LayoutParams so it will know that the drawer is closed.
Code
This is the code that'll skip the animation:
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
Note: if you just call moveDrawerToOffset() without changing the LayoutParams, the drawer will move back to its open position on the next orientation change.
Option 1 (use existing DrawerLayout)
This approach adds a utility class to the support.v4 package to gain access to the package-private parts we need inside DrawerLayout.
Place this class into /src/android/support/v4/widget/:
package android.support.v4.widget;
import android.support.annotation.IntDef;
import android.support.v4.view.GravityCompat;
import android.view.Gravity;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class Support4Widget {
/** #hide */
#IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
#Retention(RetentionPolicy.SOURCE)
private #interface EdgeGravity {}
public static void setDrawerClosed(DrawerLayout drawerLayout, #EdgeGravity int gravity) {
final View drawerView = drawerLayout.findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
DrawerLayout.gravityToString(gravity));
}
// move drawer directly to the closed position
drawerLayout.moveDrawerToOffset(drawerView, 0.f);
// set internal state so DrawerLayout knows it's closed
final DrawerLayout.LayoutParams lp = (DrawerLayout.LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
drawerLayout.invalidate();
}
}
Set a boolean in your activity when you navigate away, indicating the drawer should be closed:
public static final String CLOSE_NAV_DRAWER = "CLOSE_NAV_DRAWER";
private boolean mCloseNavDrawer;
#Override
public void onCreate(Bundle savedInstanceState) {
// ...
if (savedInstanceState != null) {
mCloseNavDrawer = savedInstanceState.getBoolean(CLOSE_NAV_DRAWER);
}
}
#Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
// ...
startActivity(intent);
mCloseNavDrawer = true;
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putBoolean(CLOSE_NAV_DRAWER, mCloseNavDrawer);
super.onSaveInstanceState(savedInstanceState);
}
...and use the setDrawerClosed() method to shut the drawer in onResume() with no animation:
#Overrid6e
protected void onResume() {
super.onResume();
if(mCloseNavDrawer && mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
Support4Widget.setDrawerClosed(mDrawerLayout, GravityCompat.START);
mCloseNavDrawer = false;
}
}
Option 2 (extend from DrawerLayout)
This approach extends DrawerLayout to add a setDrawerClosed() method.
Place this class into /src/android/support/v4/widget/:
package android.support.v4.widget;
import android.content.Context;
import android.support.annotation.IntDef;
import android.support.v4.view.GravityCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class CustomDrawerLayout extends DrawerLayout {
/** #hide */
#IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
#Retention(RetentionPolicy.SOURCE)
private #interface EdgeGravity {}
public CustomDrawerLayout(Context context) {
super(context);
}
public CustomDrawerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomDrawerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setDrawerClosed(View drawerView) {
if (!isDrawerView(drawerView)) {
throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
}
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
}
public void setDrawerClosed(#EdgeGravity int gravity) {
final View drawerView = findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
gravityToString(gravity));
}
// move drawer directly to the closed position
moveDrawerToOffset(drawerView, 0.f);
// set internal state so DrawerLayout knows it's closed
final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
lp.onScreen = 0.f;
lp.knownOpen = false;
invalidate();
}
}
Use CustomDrawerLayout instead of DrawerLayout in your activity layouts:
<android.support.v4.widget.CustomDrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
>
...and set a boolean in your activity when you navigate away, indicating the drawer should be closed:
public static final String CLOSE_NAV_DRAWER = "CLOSE_NAV_DRAWER";
private boolean mCloseNavDrawer;
#Override
public void onCreate(Bundle savedInstanceState) {
// ...
if (savedInstanceState != null) {
mCloseNavDrawer = savedInstanceState.getBoolean(CLOSE_NAV_DRAWER);
}
}
#Override
public boolean onNavigationItemSelected(MenuItem menuItem) {
// ...
startActivity(intent);
mCloseNavDrawer = true;
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putBoolean(CLOSE_NAV_DRAWER, mCloseNavDrawer);
super.onSaveInstanceState(savedInstanceState);
}
...and use the setDrawerClosed() method to shut the drawer in onResume() with no animation:
#Overrid6e
protected void onResume() {
super.onResume();
if(mCloseNavDrawer && mDrawerLayout != null && mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
mDrawerLayout.setDrawerClosed(GravityCompat.START);
mCloseNavDrawer = false;
}
}
Using an implementation of the answer provided by #James Cross worked, but the animation to close the drawer was undesirable and unfixable without much hassle, example.
#Override
public void onResume()
{
super.onResume();
mDrawerLayout.closeDrawers();
}
A work-around is to restart the activity when the device back button is pressed. It does not seem ideal to me, but it works. Overriding onBackPressed(), as suggested by #mt0s and #Qazi Ahmed and passing an extra to determine the calling activity:
mDrawerList.setOnItemClickListener(new ListView.OnItemClickListener()
{
#Override
public void onItemClick(AdapterView parent, View view, int position, long id)
{
switch(position)
{
case 0:
{
Intent intent = new Intent(MainActivity.this, NextActivity.class);
//pass int extra to determine calling activity
intent.putExtra(EXTRA_CALLING_ACTIVITY, CallingActivityInterface.MAIN_ACTIVITY);
startActivity(intent);
}
}
}
});
In NextActivity.class, check for the calling activity:
#Override
public void onBackPressed()
{
int callingActivity = getIntent().getIntExtra(EXTRA_CALLING_ACTIVITY, CallingActivityInterface.MAIN_ACTIVITY);
switch(callingActivity)
{
case CallingActivityInterface.MAIN_ACTIVITY:
{
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
...
}
}
This way the drawer is closed with no animation when I return to MainActivity regardless of whether I use the up button or the back button. There are probably better ways to do this. My app is relatively simple at the moment and this works, but I await a more effective method if anyone has one.
Why the hassle? Simply close the Drawer when clicking a drawer item. That's how it's done in the official Google Play app.
private class DrawerItemClickListener implements ListView.OnItemClickListener {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
drawerLayout.closeDrawer(GravityCompat.START, false);
selectItem(position);
}
}
You will probably want to make sure the navigation draw is always closed when the activity is opened. Use this to do that:
#Override
public void onResume(){
mDrawerList.closeDrawer(Gravity.LEFT);
}
simple sample:
Drawer resultDrawer;
public void onBackPressed(){
if (this.resultDrawer.isDrawerOpen()) {
this.resultDrawer.closeDrawer();
} else {
super.onBackPressed();
}
}
With androidx.drawerlayout:drawerlayout:1.1.0 or higher, you can keep it simple using isOpen and close().
// YourActivity.kt
override fun onBackPressed() {
if (drawerLayout.isOpen) {
drawerLayout.close()
} else {
super.onBackPressed()
}
}
This how i did it:
#Override
public void onBackPressed() {
if(drawerLayout.isDrawerOpen(navigationView)){
drawerLayout.closeDrawer(Gravity.LEFT);
}else{
super.onBackPressed();
}
}
JETPACK COMPOSE
For someone that using jetpack compose.
use this code in your scaffold:
BackHandler(enabled = drawerState.isOpen) {
scope.launch { drawerState.close() }
}
complete version:
val scope = rememberCoroutineScope()
val drawerState = rememberDrawerState(DrawerValue.Closed)
Scaffold(
topBar = {},
bottomBar = {},
snackbarHost = {},
content = {
...
BackHandler(enabled = drawerState.isOpen) {
scope.launch { drawerState.close() }
}
},
...
)