How to prevent Android Snackbar from dismissing on setAction onclick, Thanks
Snackbar.make(rootlayout, "Hello SnackBar!", Snackbar.LENGTH_INDEFINITE)
.setAction("Undo", new View.OnClickListener() {
#Override
public void onClick(View v) {
// Snackbar should not dismiss
}
})
.show();
Here is a somewhat cleaner solution for achieving this, which doesn't require reflection. It's based on knowning the view ID of the button within the Snackbar. This is working with version 27.1.1 of the support library, but may no longer work in a future version if the view ID will be changed!
First, set your snackbar action using an empty OnClickListener:
snackbar.setAction("Save", new View.OnClickListener() {
#Override
public void onClick(View v) {}
});
Afterwards, add a callback to the snackbar (before showing it). Override the onShown function, find the button using R.id.snackbar_action and add your own OnClickListener to it. The snackbar will only be dismissed when manually calling snackbar.dismiss(), or by swiping if the snackbar is attached to a CoordinatorLayout (how to disable the swipe is a different SO question).
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
#Override
public void onShown(Snackbar transientBottomBar) {
super.onShown(transientBottomBar);
transientBottomBar.getView().findViewById(R.id.snackbar_action).setOnClickListener(new View.OnClickListener() {
// your code here
}
First, by design Snackbar shouldn't stay there after the action click, that's why it is non-configurable parameter.
Diving into code I could find enough seams in order to do that by reflection.
public static void doNotHideSnackbar(Snackbar snackbar) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException {
final Field sHandler = BaseTransientBottomBar.class.getDeclaredField("sHandler");
sHandler.setAccessible(true);
final Method handleMessage = Handler.class.getMethod("handleMessage", Message.class);
final Handler originalHandler = (Handler) sHandler.get(snackbar);
Handler decoratedHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
#Override
public boolean handleMessage(Message message) {
switch (message.what) {
case 0:
try {
handleMessage.invoke(originalHandler, Message.obtain(originalHandler, 0));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return true;
}
return false;
}
});
sHandler.set(snackbar, decoratedHandler);
}
This is tested and works with support library version 25.3.1.
Usage
final Snackbar snackbar = Snackbar.make(root, "Hello SnackBar!", Snackbar.LENGTH_INDEFINITE).setAction("Undo", new View.OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(v.getContext(), "clicked", Toast.LENGTH_SHORT).show();
}
});
snackbar.show();
try {
doNotHideSnackbar(snackbar);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Result
BEWARE, this is not the solution you should prefer to stick with, as long as API may change from version to version. You'd better consider implementing your custom Snackbaralike view. But as a fast workaround you can consider using this reflectioned version.
Better late than never - here's how i did it.
private fun showSnackbar() {
if(snackbar == null) {
//init snackbar
snackbar = Snackbar.make(mainCoordinator, R.string.snackbar_no_network, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.snackbar_no_network_action) {
checkConnection()
} // action text on the right side
.setActionTextColor(ContextCompat.getColor(context, R.color.snack_green))
//set background color
snackbar!!.view.setBackgroundColor(ContextCompat.getColor(context, R.color.main_dark_gray))
}
//show
snackbar!!.show()
}
private val handler = Handler()
private fun checkConnection() {
handler.postDelayed(checkConnectionRunnable, 500)
}
private val checkConnectionRunnable = Runnable {
if (!NetworkUtil.isOnline(context)){
showSnackbar()
}
}
Related
I am unable to change the view visibility inside other function rather than onCreate method. Its working only at time of onCreate is called.
public class CameraXActivity extends AppCompatActivity {
...
public Group fipGroup;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.camerax_layout); // Created xml using constraintLayout.
//intial setup
fipGroup = (Group)findViewById(R.id.fip_group);
startCamera();
//hideFipGroup(); <<--- This is working
}
private void hideFipGroup() {
Log.d(TAG, "=== hideFipGroup ===");
fipGroup.setVisibility(View.INVISIBLE);
}
private void startCamera() {
CameraX.unbindAll();
preview = setPreview();
imageAnalysis = setImageAnalysis();
//bind to lifecycle:
CameraX.bindToLifecycle(this, preview , imageAnalysis);
preview.enableTorch(true);
}
private ImageAnalysis setImageAnalysis() {
hideFipGroup() // This is working
imageAnalysis.setAnalyzer(
new ImageAnalysis.Analyzer() {
#Override
public void analyze(ImageProxy image, int rotationDegrees) {
hideFipGroup() // Exactly, Failed at this place.
}
}
)
}
}
Edit Update:
It's failing to update on the analyze method of imageAnalysis. Just to test, called toast message which is showing on the UI. But am not able to control the UI.
private void raiseToast(String msg) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() {
Toast toast = Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER, 0, 500);
toast.show();
}
});
}
The problem is with Group visibility on constraint Layout.
private void setGroupVisiblity(Group group, int visibility) {
group.setVisibility(visibility);
group.requestLayout();
}
or manually turn off each views in the group:
private void setGroupVisibility(ConstraintLayout layout, Group group, Integer visibility) {
int[] refIds = group.getReferencedIds();
for (int id : refIds) {
layout.findViewById(id).setVisibility(visibility);
}
}
or upgrade to ConstraintLayout version 2.0.0 beta 6
source: Can't set visibility on constraint group
Please try to view.post or runOnUIThread method.
so like this.
private ImageAnalysis setImageAnalysis() {
hideFipGroup() // This is working
imageAnalysis.setAnalyzer(
new ImageAnalysis.Analyzer() {
#Override
public void analyze(ImageProxy image, int rotationDegrees) {
fipGroup.post(new Runnable() {
#Override
public void run() {
hideFipGroup(); // Exactly, Failed at this place.
}
});
}
}
)
}
Or you can simply call view.post() method in hideFipGroup() method
private void hideFipGroup() {
Log.d(TAG, "=== hideFipGroup ===");
fipGroup.post(new Runnable() {
#Override
public void run() {
fipGroup.setVisibility(View.INVISIBLE);
}
});
}
you should check the function you called. Remember that you can only update the UI in the main thread
I am attempting to create a toggling animation for a custom menu. There are multiple menus that can be chosen by different buttons.
When no menu is open, tapping on a button should open that menu.
If another menu is open, the open one is closed and after that animation, the chosen one should be opened.
every closing/opening action is coupled with a ConstraintLayout transition.
As it didn't work properly, I created the following test procedure:
#Override
protected void onCreate(Bundle savedInstanceState) {
...
ConstraintLayout layout_main;
ConstraintSet l_mh0c = new ConstraintSet();
ConstraintSet l_mh1o = new ConstraintSet();
ConstraintSet l_mh1c = new ConstraintSet();
Button btn;
btn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
toggleOne();
}
});
layout_main = (ConstraintLayout) findViewById(R.id.constraintMain);
l_mh1o.clone(this, R.layout.main_menue_header1_open);
l_mh1c.clone(this, R.layout.main_menue_header1_closed);
l_mh0c.clone(this, R.layout.activity_main);
...
}
int openedMenue = -1;
long animationTime = 1000;
private void toggleOne() {
TransitionManager.endTransitions(layout_main); //<- makes no difference when commented
if(openedMenue==1) {
System.out.println("sync closing");
Runnable r = () -> toggleOne();
closeMenue(1, animationTime, r);
} else {
System.out.println("sync opening");
startMenue(1, animationTime);
}
}
public void startMenue(Integer index, final Long maxtime) {
Transition t;
switch (index) {
case 1:
t = new ChangeBounds();
t.setDuration(0).addListener(new TransitionEndListener() {
#Override
public void onTransitionEnd(Transition transition) {
Transition t = new ChangeBounds();
t.setDuration(maxtime / 2).addListener(new TransitionEndListener() {
#Override
public void onTransitionEnd(Transition transition) {
Transition t = new ChangeBounds();
t.setDuration(maxtime / 2);
TransitionManager.beginDelayedTransition(layout_main, t);
l_mh0o.applyTo(layout_main);
openedMenue = 1;
System.out.println("sync start finished");
}
});
TransitionManager.beginDelayedTransition(layout_main, t);
l_mh1o.applyTo(layout_main);
}
});
TransitionManager.beginDelayedTransition(layout_main, t);
l_mh1c.applyTo(layout_main);
// the following does not provoke any changes
Scene s = new Scene(layout_main);
TransitionManager.go(s);
break;
}
}
private void closeMenue(int index, final long maxtime, Runnable callback) {
System.out.println("sync closing menue " + openedMenue);
Transition t;
switch (index) {
case 1:
t = new ChangeBounds();
t.setDuration(maxtime/2).addListener(new TransitionEndListener() {
#Override
public void onTransitionEnd(Transition transition) {
Transition t = new ChangeBounds();
t.setDuration(maxtime/2).addListener(new TransitionEndListener() {
#Override
public void onTransitionEnd(Transition transition) {
Transition t = new ChangeBounds();
t.setDuration(0);
TransitionManager.beginDelayedTransition(layout_main, t);
l_mh0c.applyTo(layout_main);
openedMenue = -1;
try {
callback.run();
} catch (Exception e) { e.printStackTrace(); }
}
});
TransitionManager.beginDelayedTransition(layout_main, t);
l_mh1c.applyTo(layout_main);
openedMenue = 1;
}
});
TransitionManager.beginDelayedTransition(layout_main, t);
l_mh1o.applyTo(layout_main);
break;
}
}
When I run it, I get the following output:
[Button Click]
sync opening
sync start finished
[Button Click]
sync closing
sync closing menue 1
sync opening
sync start finished // problem
However, the last line is never printed; It seems as if (after the closing action starts the opening action again) the first transition in startMenue() never does a callback to the onTransitionEnd().
Here is the TransitionEndListener (just a simple wrapper for the interface)
public abstract class TransitionEndListener implements Transition.TransitionListener {
#Override
public final void onTransitionStart(Transition transition) {}
#Override
public final void onTransitionCancel(Transition transition) {}
#Override
public final void onTransitionPause(Transition transition) {}
#Override
public final void onTransitionResume(Transition transition) {}
}
I already checked if the second starting Transition is cancelled by putting a print statement in the onTransitionCancel() which doesn't seem to be the case.
Can you please explain why this is happening?
UPDATE
I found this post on TransitionManager callbacks;
The Transition from mh0c t0 mh1c is a ConstraintLayout transition, because the constraints actually change; however, the transition is not visible on the UI because the width of the element in transition is 0. (This transition is a jump from one menu point to the other that should not be visible.)
Could this be a reason that the Transition does not do the callback?
And if so; how can I get around this?
UPDATE 2
I was reading the documentation and may have found a solution using TransitionManager.go(scene, transition).
--> Unfortunately this did not work; see code of startMenue() for changes
After more and more struggling and reviewing these posts:
Animate visibility of a view from gone to visible with animation
TransitionListener callbacks not called when there is nothing to change
Why does my Android transition ignore the TransitionListener?
I have found out what caused the trouble:
In the most inner part (the most inner callback) of closeMenue()
Transition t = new ChangeBounds();
t.setDuration(0);
TransitionManager.beginDelayedTransition(layout_main, t);
l_mh0c.applyTo(layout_main);
openedMenue = -1;
try {
callback.run();
} catch (Exception e) { e.printStackTrace(); }
The sequence of commands is slightly wrong. It should look like this:
Transition t = new ChangeBounds();
t.setDuration(0).addListener(new TransitionEndListener() {
#Override
public void onTransitionEnd(Transition transition) {
try {
callback.run();
} catch (Exception e) { e.prtinStackTrace(); }
}
});
TransitionManager.beginDelayedTransition(layout_main, t);
l_mh0c.applyTo(layout_main);
openedMenue = -1;
This makes sure to first finish the closing transition and THEN starting the opening transition.
So imagine that at first we load 10 items in our RecyclerView. Adding or removing one element gives us a nice animation (the adapter has stable ids).
The problem is, I have a search bar, I can look for something and then the result should replace the current items of the RecyclerView. If some item was already there, there is a nice "moving" animation. But if all items are new, there is a quite ugly fade-in transition that it's too fast and looks like a glitch. Is it possible to override that animation? I'd like to have a fade-out-fade-in one but slower.
By the way, when the query returns with results, I do this in the adapter:
mItems.clear();
mItems.addAll(resultItems);
notifyDataSetChanged();
Also, it's worth to say that if I make a search with no results, then I see the RecyclerView empty and then if I get some results again, the transition from empty state to some results looks ok.
You can batch remove and insert items in a RecyclerView.
adapter.notifyItemRangeRemoved(0, mItems.size());
mItems.clear();
mItems.addAll(resultItems);
adapter.notifyItemRangeInserted(0, mItems.size());
EDIT: After looking at your problem some more you probably don't want to do what I suggested above. Instead you should not clear your list and instead remove some items and then notify the adapter of the change with notifyItemRemove(index)
If you do range methods like RangeRemoved/RangeAdded, you loose out in the animation side. So, do it one by one in a loop to preserve the animation effect of one by one, including a delay in the loop. Here's how I have implemented:
MainActivity.java
clearItemsView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final List<LineItem> lineItemsCopy = new ArrayList<>(lineItems);
new Thread(new Runnable() {
#Override
public void run() {
for (int i=0; i<lineItemsCopy.size(); i++) {
runOnUiThread(new Runnable() {
#Override
public void run() {
salesOrderItemListAdapter.removeItem(0);
}
});
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
Snackbar snackbar = Snackbar.make(coordinatorLayout, getString(R.string.items_cleared_message), Snackbar.LENGTH_LONG)
.setAction(getString(R.string.label_undo), new View.OnClickListener() {
#Override
public void onClick(View v) {
new Thread(new Runnable() {
#Override
public void run() {
for (int i=0; i<lineItemsCopy.size(); i++) {
final int finalI = i;
runOnUiThread(new Runnable() {
#Override
public void run() {
salesOrderItemListAdapter.restoreItem(lineItemsCopy.get(finalI), 0);
}
});
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}).setActionTextColor(Color.YELLOW);
snackbar.show();
}
});
RecyclerViewAdapter.java
//Only remove & restore functions are shown
public void removeItem(int position) {
lineItems.remove(position);
notifyItemRemoved(position);
}
public void restoreItem(LineItem item, int position) {
lineItems.add(position, item);
notifyItemInserted(position);
}
I have an Activity with ViewPager PagerSlidingTabStrip for each page of my ViewPager has a fragment, and in each fragment realize an http request (using Volley) to load the data from the page, but when the request ends in error, type timeout or lost connection, I need to display a dialog with the option to redo the call to the server, the problem to prevent multiple dialogs are open for each error is resolved with the snippet:
See this solution here: http://www.jorgecoca.com/android-quick-tip-avoid-opening-multiple-dialogs-when-tapping-an-element/
#Override
public void show(FragmentManager manager, String tag) {
if (manager.findFragmentByTag(tag) == null) {
super.show(manager, tag);
}
}
When the user clicks the dialog button to try again, and the dialog closed and taken to check if there is internet connection, if I'm not, the dialog should be opened again, but the dialog is not displayed again, I believe that the tag does not was released to FragmentManager.
Code in Activity:
final Button mButton = ( Button ) this.findViewById( R.id.btn_opendialog );
final DialogFragmentHelper mDialog = new DialogFragmentHelper();
mDialog.setCallbackListener( new OnCallback() {
#Override
public void onCancel() {
}
#Override
public void onConfirm() {
// verify if network available
mDialog.show( MainActivity.this.getSupportFragmentManager(), DialogFragmentHelper.DIALOG_TAG );
}
} );
mButton.setOnClickListener( new OnClickListener() {
#Override
public void onClick( final View v ) {
mDialog.show( MainActivity.this.getSupportFragmentManager(), DialogFragmentHelper.DIALOG_TAG );
}
} );
Would someone have a suggestion of a workaround?
In order to maintain the structure that is ready in my project, and also keep something closer to my goal, which is to use no flags, nor pass control of a third dialogfragment to manage, arrived at a solution that'll take an hour as a result.
DialogFragmentHelper mDialog = new DialogFragmentHelper();
mDialog.setCallbackListener( new OnCallback() {
#Override
public void onCancel() {}
#Override
public void onConfirm() {
mDialog.dismissAllowingStateLoss();
if(networkAvailable == false){
new Handler().post( new Runnable() {
#Override
public void run() {
mDialog.show( MainActivity.this.getSupportFragmentManager(), DialogFragmentHelper.DIALOG_TAG );
}
} );
}else {
//do something here
}
}
} );
this way I guarantee that while several requests are sent to open the dialogfragment, only the first is executed, and closing the dialogfragment, I can quickly open it again if needed, as can happen in the scenario I'm working.
You could approach it with a singleton controller. E.g.:
package com.example.stackoverflowsandbox;
public class MyDialogController {
private static MyDialogController instance;
public static MyDialogController getInstance() {
if ( MyDialogController.instance == null ) {
MyDialogController.instance = new MyDialogController();
}
return MyDialogController.instance;
}
private boolean dialogOpenned;
private MyDialogController() {}
public void closeDialog() {
if ( this.dialogOpenned ) {
this.dialogOpenned = false;
// you close code...
}
}
public void openDialog() {
if ( !this.dialogOpenned ) {
this.dialogOpenned = true;
// your open code...
}
}
}
I want to show the same dialog in different activities. I tried to make a BaseActivitiy. The Activities extends my BaseActivity. That worked so far, but now I want to update the Activity which shows the dialog when the dialog is closed. Update means in my case to fill a listview with data from a SQLite database.
I also tried to retrieve the classname to use the update method of those activities. But it is not possible to change the update method to static, because of the non-static SQL methods...
Do you have any idea?
Activity:
public class MyActivity extends Dialogs {
...
#Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
int idx = info.position;
switch (item.getItemId()) {
case CONTEXTMENU_ID:
showMyDialog(this,DIALOG_ID);
break;
}
return true;
}
public void update() {
...
}
}
DialogsClass
public class Dialogs extends Activity {
#Override
protected Dialog onCreateDialog(int id) {
...
}
...
//Called on Dialog-Butten press
private void ReloadActivity(){
if(DialogCalledByClass.contains("MyActivity")) {
MyActivity.update();// It doesn't worke because non-static....
}
else if(DialogCalledByClass.contains("MyActivity2")) {
}
}
public void showMyDialog(Context ctx,int id) {
showDialog(id);
DialogCalledByClass =ctx.getClass().toString();
}
}
That's what I have tried...
For example... Instead of create a BaseActivity you could create your own Dialog:
class myDialog extends AlertDialog {
Activity myActivity;
public myDialog(Activity myAct){
myActivity=myAct;
}
#Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.my_dialog);
...
...
}
#Override
public void dismiss(){
myActivity.update();
}
#Override
public void cancel(){
myActivity.update();
}
}
I don't know if I've understood your question, but it's an idea. I hope it help you.
I found a Solution. Thanks to you David!! Sry I couldn't vote up because to less reputation...
private void ReloadActivity(){
if(DialogCalledByClass.contains("MyActivity")){
try {
Method m = DialogActivity.getClass().getMethod("Update");
try {
m.invoke(DialogActivity);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
} catch (SecurityException e) {
error.d(TAG, "SecurityException"+ e);
e.printStackTrace();
} catch (NoSuchMethodException e) {
Log.d(TAG, "NoSuchMethodException"+ e);
}
}
else if(DialogCalledByClass.contains("MyActivity2")){
}
}
public void showMyDialog(Activity act,int id){
showDialog(id);
DialogCalledByClass = act.getClass().toString();
DialogActivity = act;
}