Although Snackbar is beautiful, it doesn't persist when changing activities. This is a bummer in scenarios where I would like to confirm that a message was sent using a Snackbar, before finishing the activity. I've considered pausing the code before exiting the activity, but have found that to be a bad practice.
If what I describe isn't possible, is there any type of material design toast message? Or a way to make a rectangular toast message; one with rounded edges of a smaller radius?
To create a Snackbar with the application context which is visible across multiple activities:
Get the WindowManager as system service
Create and add a FrameLayout (rootView) with type WindowManager.LayoutParams.TYPE_TOAST and WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL to the WindowManager
Wait until on FrameLayout.onAttachedToWindow() is called in the FrameLayout (rootView)
Get the window token of the FrameLayout (rootView) with View.getWindowToken()
Create a ContextThemeWrapper with the application context and a derived #style/Theme.AppCompat
Use the new context to create an additional FrameLayout (snackbarContainer)
Add this FrameLayout (snackbarContainer) with type WindowManager.LayoutParams.TYPE_APPLICATION_PANEL and flag WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
Wait until on View.onAttachedToWindow() is called in the FrameLayout (snackbarContainer)
Create the Snackbar like normal with the FrameLayout (snackbarContainer)
Set View.onDismissed() callback to the Snackbar and remove the FrameLayouts (rootView and snackbarContainer)
Show the snackbar Snackbar.show()
Here a working wrapper (NOTE: Swipe to dismiss is not working. Maybe some one else find the correct WindowManager.LayoutParams flags to receive touch events Fixed by CoordinatorLayout):
public class SnackbarWrapper
{
private final CharSequence text;
private final int duration;
private final WindowManager windowManager;
private final Context appplicationContext;
#Nullable
private Snackbar.Callback externalCallback;
#Nullable
private Action action;
#NonNull
public static SnackbarWrapper make(#NonNull Context applicationContext, #NonNull CharSequence text, #Snackbar.Duration int duration)
{
return new SnackbarWrapper(applicationContext, text, duration);
}
private SnackbarWrapper(#NonNull final Context appplicationContext, #NonNull CharSequence text, #Snackbar.Duration int duration)
{
this.appplicationContext = appplicationContext;
this.windowManager = (WindowManager) appplicationContext.getSystemService(Context.WINDOW_SERVICE);
this.text = text;
this.duration = duration;
}
public void show()
{
WindowManager.LayoutParams layoutParams = createDefaultLayoutParams(WindowManager.LayoutParams.TYPE_TOAST, null);
windowManager.addView(new FrameLayout(appplicationContext)
{
#Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
onRootViewAvailable(this);
}
}, layoutParams);
}
private void onRootViewAvailable(final FrameLayout rootView)
{
final CoordinatorLayout snackbarContainer = new CoordinatorLayout(new ContextThemeWrapper(appplicationContext, R.style.FOL_Theme_SnackbarWrapper))
{
#Override
public void onAttachedToWindow()
{
super.onAttachedToWindow();
onSnackbarContainerAttached(rootView, this);
}
};
windowManager.addView(snackbarContainer, createDefaultLayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, rootView.getWindowToken()));
}
private void onSnackbarContainerAttached(final View rootView, final CoordinatorLayout snackbarContainer)
{
Snackbar snackbar = Snackbar.make(snackbarContainer, text, duration);
snackbar.setCallback(new Snackbar.Callback()
{
#Override
public void onDismissed(Snackbar snackbar, int event)
{
super.onDismissed(snackbar, event);
// Clean up (NOTE! This callback can be called multiple times)
if (snackbarContainer.getParent() != null && rootView.getParent() != null)
{
windowManager.removeView(snackbarContainer);
windowManager.removeView(rootView);
}
if (externalCallback != null)
{
externalCallback.onDismissed(snackbar, event);
}
}
#Override
public void onShown(Snackbar snackbar)
{
super.onShown(snackbar);
if (externalCallback != null)
{
externalCallback.onShown(snackbar);
}
}
});
if (action != null)
{
snackbar.setAction(action.text, action.listener);
}
snackbar.show();
}
private WindowManager.LayoutParams createDefaultLayoutParams(int type, #Nullable IBinder windowToken)
{
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.format = PixelFormat.TRANSLUCENT;
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
layoutParams.gravity = GravityCompat.getAbsoluteGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, ViewCompat.LAYOUT_DIRECTION_LTR);
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
layoutParams.type = type;
layoutParams.token = windowToken;
return layoutParams;
}
#NonNull
public SnackbarWrapper setCallback(#Nullable Snackbar.Callback callback)
{
this.externalCallback = callback;
return this;
}
#NonNull
public SnackbarWrapper setAction(CharSequence text, final View.OnClickListener listener)
{
action = new Action(text, listener);
return this;
}
private static class Action
{
private final CharSequence text;
private final View.OnClickListener listener;
public Action(CharSequence text, View.OnClickListener listener)
{
this.text = text;
this.listener = listener;
}
}
}
EDIT
Once SnackbarWrapper is defined you can use it like this:
final SnackbarWrapper snackbarWrapper = SnackbarWrapper.make(getApplicationContext(),
"Test snackbarWrapper", Snackbar.LENGTH_LONG);
snackbarWrapper.setAction(R.string.snackbar_text,
new View.OnClickListener() {
#Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), "Action",
Toast.LENGTH_SHORT).show();
}
});
snackbarWrapper.show();
If you don't have a theme, you can quickly define one in styles.xml:
<style name="FOL_Theme_SnackbarWrapper" parent="#style/Theme.AppCompat">
<!--Insert customization here-->
</style>
EDIT
For those on Android Oreo getting Bad Token Exception, change TYPE_TOAST to TYPE_APPLICATION_OVERLAY. This is due to Android Oreo implementing special permissions to draw over applications. You can ask for this permissions using:
if(!Settings.canDrawOverlays(Activity.this){
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, URI.parse("package:" + getPackageName()));
startActivityForResult(intent, REQ_CODE);
}
If I understand correctly, you do this:
Activity A launch Activity B to send a message
Once message is send, you display a confirmation message
You go back to Activity A
You can use SnackBar to do that by using an ActivityResult (here is a StackOverflow post with how to use it)
Here are the steps:
Activity A launch Activity B with startActivityForResult
Do your stuff on Activity B
Set your result (check the link above to understand)
Finish Activity
In Activity A, get that code in OnActivityResult and display your
SnackBar with the proper message
This allow you do display a Snackar in Activity A corresponding to result of Activity B.
Hopes it can helps your problem
Just in case somebody needs to do this in Xamarin I have adapted the accepted answer which I found really helpful.
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Support.Design.Widget;
using Android.Views;
using Android.Widget;
using System;
public class SnackbarWrapper
{
private readonly string text;
private readonly int duration;
private readonly IWindowManager windowManager;
private readonly Context appplicationContext;
private Snackbar.Callback externalCallback;
private SnackbarAction action { get; set; }
public static SnackbarWrapper make(Context applicationContext, string text, int duration)
{
return new SnackbarWrapper(applicationContext, text, duration);
}
private SnackbarWrapper(Context appplicationContext, string text, int duration)
{
this.appplicationContext = appplicationContext;
var wm = appplicationContext.GetSystemService(Context.WindowService);
// We have to use JavaCast instead of a normal cast
this.windowManager = wm.JavaCast<IWindowManager>();
this.text = text;
this.duration = duration;
}
public void Show()
{
WindowManagerLayoutParams layoutParams = createDefaultLayoutParams(WindowManagerTypes.Toast, null);
var frameLayout = new FrameLayout(appplicationContext);
frameLayout.ViewAttachedToWindow += delegate
{
//this.onAttachedToWindow();
onRootViewAvailable(frameLayout);
};
windowManager.AddView(frameLayout, layoutParams);
}
private void onRootViewAvailable(FrameLayout rootView)
{
var ctw = new ContextThemeWrapper(appplicationContext, Resource.Style.Base_Theme_AppCompat);
CoordinatorLayout snackbarContainer = new CoordinatorLayout(ctw);
snackbarContainer.ViewAttachedToWindow += delegate
{
onSnackbarContainerAttached(rootView, snackbarContainer);
};
windowManager.AddView(snackbarContainer, createDefaultLayoutParams(WindowManagerTypes.ApplicationPanel, rootView.WindowToken));
}
private void onSnackbarContainerAttached(View rootView, CoordinatorLayout snackbarContainer)
{
Snackbar snackbar = Snackbar.Make(snackbarContainer, text, duration);
snackbar.SetCallback(new SnackbarCallbackImpl(rootView, snackbarContainer, windowManager));
if (action != null)
{
snackbar.SetAction(action.Text, action.Listener);
}
snackbar.Show();
}
private WindowManagerLayoutParams createDefaultLayoutParams(WindowManagerTypes type, IBinder windowToken)
{
WindowManagerLayoutParams layoutParams = new WindowManagerLayoutParams();
layoutParams.Format = Format.Translucent;
layoutParams.Width = ViewGroup.LayoutParams.MatchParent;
/* Si ponemos aqui WrapContent en alguna ocasion en la que haya un action largo y el texto tambien, el snackbar puede volverse como loco
* asi que usamos MatchParent. Aun asi sucede que a veces se puede mostrar en una linea o en dos el mismo texto, pero al menos no hace el temblor loco que de la otra forma*/
layoutParams.Height = ViewGroup.LayoutParams.MatchParent;
layoutParams.Gravity = GravityFlags.CenterHorizontal | GravityFlags.Bottom;
layoutParams.Flags = WindowManagerFlags.NotTouchModal;
layoutParams.Type = type;
layoutParams.Token = windowToken;
return layoutParams;
}
public SnackbarWrapper SetCallback(Snackbar.Callback callback)
{
this.externalCallback = callback;
return this;
}
public SnackbarWrapper SetAction(string text, Action<View> listener)
{
action = new SnackbarAction(text, listener);
return this;
}
}//class
internal class SnackbarAction
{
public string Text { get; set; }
public Action<View> Listener { get; set; }
public SnackbarAction(string text, Action<View> listener)
{
Text = text;
Listener = listener;
}
}
internal class SnackbarCallbackImpl : Snackbar.Callback
{
public Snackbar.Callback externalCallback { get; set; }
View rootView;
CoordinatorLayout snackbarContainer;
IWindowManager windowManager;
public SnackbarCallbackImpl(View rootView, CoordinatorLayout snackbarContainer, IWindowManager windowManager)
{
this.rootView = rootView;
this.snackbarContainer = snackbarContainer;
this.windowManager = windowManager;
}
public override void OnShown(Snackbar snackbar)
{
base.OnShown(snackbar);
externalCallback?.OnShown(snackbar);
}
public override void OnDismissed(Snackbar snackbar, int evt)
{
base.OnDismissed(snackbar, evt);
// Clean up (NOTE! This callback can be called multiple times)
if (snackbarContainer.Parent != null && rootView.Parent != null)
{
windowManager.RemoveView(snackbarContainer);
windowManager.RemoveView(rootView);
}
externalCallback?.OnDismissed(snackbar, evt);
}
}
To have a rectangular Toast, set a rectangular background for the Toast or just set a different background color for the Toast.
Refer this post where it was posted as a problem. But it your case it is a possible solution.
UPDATE: See selected answer.
The best solution to my question is using a Timer after the presenting the Snackbar and then in the run() method of the timer, starting the activity.
Snackbar.show(); // Excluded make for brevity.
Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run() {
Intent chooseVideoIntent = new Intent(Intent.ACTION_GET_CONTENT); // Any type of content/file. Song, doc, video...
chooseVideoIntent.setType("video/*");
startActivityForResult(chooseVideoIntent, CHOOSE_VIDEO_REQUEST);
}
}, 2 * 1000);
UPDATE: I found that by using findViewById(android.R.id.content) as the view in Snackbar.make() the Snackbar persists among fragment changes.
Actually I just need to display a message and I don't need a onClickListener on the message.
If you just need to show a message look at "Myke Dev" answer in this thread which is the one I needed:
https://stackoverflow.com/a/34640942/9993413
(Don't give upvote to me, give upvote to "Myke Dev" which wrote the answer)
In user1185087 answer you must request permissions from user by opening settings and this, for me, isn't a good thing for user interactions, but I thinks is the only way if you want to show a snackbar with an onClickListener.
(Maybe you can use an activity with no background which launch a snackbar-like dialog, but it won't act just as a window_alert snackbar)
Related
I'm currently having trouble setting up my custom listener. I just want to pass a string from my dialog to my fragment (where I set up the dialog). I was trying to follow this tutorial: https://www.youtube.com/watch?v=ARezg1D9Zd0.
At minute 10:38, he sets up the listener.
This only problem is that in this, he uses DialogFragment, but I'm extending dialog and I don't know how to attach the context to the listener.
I've tried to set it up in onAttachedToWindow() and in the dialog constructor but it crashes.
What should I actually do?
I'd also appreciate it if someone could explain what the difference is between:
onAttachedToWindow() vs. onAttach(Context context).
Thanks!
MY CUSTOM DIALOG BOX:
public class NewListDialog extends Dialog implements View.OnClickListener {
private Activity c;
private TextInputLayout textInputLayout;
private TextInputEditText editText;
private LinearLayout dialog_root_view;
private Animation fade_out;
private String list_name;
private NewListDialogListener listener;
NewListDialog(Activity a) {
super(a);
this.c = a;
//ANOTHER ATTEMPT TO ATTACH CONTEXT TO LISTENER
//listener = (NewListDialogListener) a.getApplicationContext();
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.new_list_dialog);
MaterialButton cancel = findViewById(R.id.dialog_new_list_cancel_button);
MaterialButton create = findViewById(R.id.dialog_new_list_create_button);
textInputLayout = findViewById(R.id.dialog_text_input_layout);
editText = findViewById(R.id.dialog_edit_text);
dialog_root_view = findViewById(R.id.dialog_root);
fade_out = AnimationUtils.loadAnimation(c, R.anim.fade_out_dialog);
editText.setOnKeyListener(new View.OnKeyListener() {
#Override
public boolean onKey(View view, int i, KeyEvent keyEvent) {
if (isTextValid(editText.getText())) {
textInputLayout.setError(null);
return true;
}
return false;
}
});
cancel.setOnClickListener(this);
create.setOnClickListener(this);
}
#Override
public void onClick(View view) {
switch (view.getId()) {
//Cancel Button
case R.id.dialog_new_list_cancel_button:
dialog_root_view.startAnimation(fade_out);
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
dismiss();
}
}, 200);
break;
//Create Button
case R.id.dialog_new_list_create_button:
if (!isTextValid(editText.getText())) {
textInputLayout.setError(c.getString(R.string.dialog_error));
} else {
textInputLayout.setError(null);
//record input string
list_name = editText.getText().toString();
//send information to parent activity
//What to put here?
listener.createListName(list_name);
dismiss();
}
break;
default:
break;
}
}
private boolean isTextValid(#Nullable Editable text) {
return text != null && text.length() > 0;
}
//ATTEMPT TO ATTACH CONTEXT TO LISTENER
#Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
try {
listener = (NewListDialogListener) c.getBaseContext();
} catch (ClassCastException e) {
throw new ClassCastException(c.getBaseContext().toString() + "must implement ExampleDialogListener");
}
}
public interface NewListDialogListener {
void createListName(String listname);
}
}
In case you define a custom dialog then you can declare a method to allow other components call it or listen events on this dialog. Add this method to you custom dialog.
public void setNewListDialogListener(NewListDialogListener listener){
this.listener = listener;
}
NewListDialog.java
public class NewListDialog extends Dialog implements View.OnClickListener {
private Activity c;
private TextInputLayout textInputLayout;
private TextInputEditText editText;
private LinearLayout dialog_root_view;
private Animation fade_out;
private String list_name;
private NewListDialogListener listener;
NewListDialog(Activity a) {
super(a);
this.c = a;
}
public void setNewListDialogListener(NewListDialogListener listener) {
this.listener = listener;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.new_list_dialog);
MaterialButton cancel = findViewById(R.id.dialog_new_list_cancel_button);
MaterialButton create = findViewById(R.id.dialog_new_list_create_button);
textInputLayout = findViewById(R.id.dialog_text_input_layout);
editText = findViewById(R.id.dialog_edit_text);
dialog_root_view = findViewById(R.id.dialog_root);
fade_out = AnimationUtils.loadAnimation(c, R.anim.fade_out_dialog);
editText.setOnKeyListener(new View.OnKeyListener() {
#Override
public boolean onKey(View view, int i, KeyEvent keyEvent) {
if (isTextValid(editText.getText())) {
textInputLayout.setError(null);
return true;
}
return false;
}
});
cancel.setOnClickListener(this);
create.setOnClickListener(this);
}
#Override
public void onClick(View view) {
switch (view.getId()) {
//Cancel Button
case R.id.dialog_new_list_cancel_button:
dialog_root_view.startAnimation(fade_out);
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
#Override
public void run() {
dismiss();
}
}, 200);
break;
//Create Button
case R.id.dialog_new_list_create_button:
if (!isTextValid(editText.getText())) {
textInputLayout.setError(c.getString(R.string.dialog_error));
} else {
textInputLayout.setError(null);
//record input string
list_name = editText.getText().toString();
//send information to parent activity
//What to put here?
if (listener != null) {
listener.createListName(list_name);
}
dismiss();
}
break;
default:
break;
}
}
private boolean isTextValid(#Nullable Editable text) {
return text != null && text.length() > 0;
}
public interface NewListDialogListener {
void createListName(String listname);
}
}
In other components such as an activity which must implements NewListDialogListener.
NewListDialog dialog = new NewListDialog(this);
dialog.setNewListDialogListener(this);
If you don't want the activity implements NewListDialogListener then you can pass a listener instead.
NewListDialog dialog = new NewListDialog(this);
dialog.setNewListDialogListener(new NewListDialog.NewListDialogListener() {
#Override
public void createListName(String listname) {
// TODO: Your code here
}
});
In android Fragments and Activity has lifecycles. Fragments are hosted inside Activity and get the context of host activity via onattach method.
On the other hand Dialog is extended from Object (God class) without any lifecycle and should be treaded as an object.
If your activity is implementing NewListDialogListener then you can do
listener = (NewListDialogListener) a;
onAttachedToWindow : mean the dialog will be drawn on screen soon
and
getApplicationContext() will give you the context object of the application (one per app) which is surely not related with your listener and hence won't work
Reference :
Android DialogFragment vs Dialog
Difference between getContext() , getApplicationContext() , getBaseContext() and “this”
You can use RxAndroid instead of using listener, in this situation I use RxAndroid to get data from dialogs to activities or fragments.
Just need to create a PublishSubject and get the observed data. on activity or fragment :
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PublishSubject<String > objectPublishSubject = PublishSubject.create();
objectPublishSubject.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.newThread())
.subscribe(this::onNext);
CustomDialog customDialog = new CustomDialog(this, objectPublishSubject);
customDialog.show();
}
private void onNext(String data) {
Log.i("DIALOG_DATA", data);
}
and you can create dialog like this :
public class CustomDialog extends Dialog implements View.OnClickListener {
private PublishSubject<String> subject;
public CustomDialog(#NonNull Context context, PublishSubject<String> subject) {
super(context);
this.subject = subject;
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.custom_dialog);
findViewById(R.id.button).setOnClickListener(this);
}
#Override
public void onClick(View v) {
subject.onNext("Data");
dismiss();
}
I'm adding a small TextView at the bottom of my app when the app goes offline. So I have a BroadcastReceiver that monitors network connectivity changes and in the onReceive, I show the banner. Here is the banner class which adds the TextView on top of the existing view:
public static void show() {
if (!isShowing && !isAppBackgrounded()) {
MyApplication app = MyApplication.getInstance();
WindowManager windowManager = (WindowManager) app.getSystemService(Context.WINDOW_SERVICE);
Resources res = app.getResources();
TextView offlineTv = app.getOfflineTv();
if (offlineTv.getWindowToken() != null) {
return;
}
offlineTv.setText("Offline");
offlineTv.setTextColor(ContextCompat.getColor(app, R.color.yellow));
offlineTv.setGravity(Gravity.CENTER);
offlineTv.setBackgroundColor(ContextCompat.getColor(app, R.color.dark_grey));
offlineTv.setTextSize(TypedValue.COMPLEX_UNIT_SP, app.getResources().getInteger(R.integer.offline_banner_text_size));
WindowManager.LayoutParams params = createLayoutParams(WindowManager.LayoutParams.TYPE_TOAST, null);
windowManager.addView(offlineTv, params);
isShowing = true;
}
}
Here is the createLayoutParams method
private static WindowManager.LayoutParams createLayoutParams(int type, #Nullable IBinder windowToken) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.format = PixelFormat.TRANSLUCENT;
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.height = 25;
layoutParams.gravity = GravityCompat.getAbsoluteGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, ViewCompat.LAYOUT_DIRECTION_LTR);
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
layoutParams.type = type;
layoutParams.token = windowToken;
layoutParams.windowAnimations = android.R.style.Animation_Toast;
return layoutParams;
}
This code works fine on all the devices but 7.1.1 devices. In 7.1.1 device, the TextView shows for a while and then disappears. There is just a blank white space instead of the TextView on 7.1.1 devices. Any idea why is it happening?
EDIT: as asked in the comment, here is how I get the TextView: This is MyApplication class extending Application:
TextView offlineTv = null;
/** Get the TextView to show the offline message */
public TextView getOfflineTv() {
if (offlineTv == null) {
offlineTv = new TextView(this);
}
return offlineTv;
}
/** Clear the offline TextView once we are done showing it */
public void clearOfflineTv() {
if (offlineTv != null) {
offlineTv = null;
}
}
And this is my BroadcastReceiver, where I show / hide it:
public class DSConnectionChangeReceiver extends BroadcastReceiver {
/**
* Connection-changed callback
* #param context Context
* #param intent Intent
*/
#Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo();
boolean connected = false;
boolean isCellularData = false;
if (activeNetworkInfo != null) {
connected = activeNetworkInfo.isAvailable() && activeNetworkInfo.isConnected();
int type = activeNetworkInfo.getType();
isCellularData = (type == ConnectivityManager.TYPE_MOBILE) || (type == ConnectivityManager.TYPE_MOBILE_DUN);
}
if (connected) {
if (OfflineBanner.isShowing()) {
OfflineBanner.dismiss();
}
} else {
OfflineBanner.show();
}
}
}
The problem is caused by you adding the android.R.style.Animation_Toast windowAnimation. When the animation finishes on an actual Toast the whole toast would disappear. In this case your view is in the hierarchy so instead of disappearing, it becomes blank.
What you should do is leave layoutParams.windowAnimations off of the params and instead create and attach the view with the visibility set to View.GONE then animate the view onto the screen manually
Manually animating the view can be achieved with the following utility:
Animation animIn = AnimationUtils.makeInAnimation(context, true);
textView.setAnimation(animIn);
textView.setVisibility(View.VISIBLE);
textView.animate();
Snackbar Alternative:
public final class ConnectionBar {
private static boolean mIsConnected = true; //static to preserve state
private static ConnectionReceiver mReceiver; //static to detect leaks
private static SnackBar mSnack;
private ConnectionBar() { /* required */ )
public static void prepare(Context ctx) {
if (mReceiver != null) {
Log.e(TAG, "WARNING previous ConnectionBar was leaked");
}
mReceiver = new ConnectionReceiver();
ctx.registerBroadcastReceiver(mReceiver);
if (!mIsConnected) { //static so will remember from last screen
showBar(ctx);
}
}
private static void showBar(Context ctx) {
if (mSnack == null) {
mSnack = Snackbar.make(view, message, SnackBar.LENGTH_INDEFINITE);
mSnack.show();
}
}
public static void release(Context ctx) {
if (mReceiver != null) {
ctx.unregisterBroadcastReceiver(mReceiver);
mReceiver = null;
}
if (mSnack != null) {
mSnack.dismiss();
}
}
private static class ConnectionReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetworkInfo = cm.getActiveNetworkInfo();
boolean isCellularData = false; //migrate this how you want
if (activeNetworkInfo != null) {
ConnectionBar.mIsConnected = activeNetworkInfo.isAvailable() && activeNetworkInfo.isConnected();
int type = activeNetworkInfo.getType();
isCellularData = (type == ConnectivityManager.TYPE_MOBILE) || (type == ConnectivityManager.TYPE_MOBILE_DUN);
}
}
if (connected && ConnectionBar.mSnack != null) {
ConnectionBar.mSnack.dismiss(); //check this, might need to wrap in runOnUiThread
} else {
ConnectionBar.showBar(context);
}
}
}
Then in your activity:
public void onResume() {
ConnectionBar.prepare(this); //takes care of setting br too
}
public void onPause() {
ConnectionBar.release(this);
}
If you want View in WindowManager remains more than BroadcastReciever Lifecycle you need to do it inside a class extends of service. check out this tutorial
I think still there is problems with Lifecycle. How you use it and how the system handles it.
If you want to force system not to kill your service (and not to remove WindowManager) you have 3 options.
return OnStartCommand with the proper flag.
return Service.START_REDELIVER_INTENT;
add foreground notification
startForeground(123, NotificationFunction());
and If you have lots of processes to do add Accessibility Service. check out this
This is an intented behaviour since Android 7.1 to prevent apps from using a toast view to overlay other apps indefinitely. Whenever you use a TYPE_TOAST view, the system will impose a maximum of 3.5 seconds (i.e. that of a LONG toast) for the display of your view (and also change the view animation to the internal Toast style), after which your toast view will be hidden, EXCEPT where your app is the currently focused one.
To avoid crashing apps, your view still remains on the view hierarchy. In other words, you can still call removeView on it after it is hidden by the system, without causing an illegal state exception.
(Reference: See the commit message to the Android source:
https://github.com/aosp-mirror/platform_frameworks_base/commit/aa07653d2eea38a7a5bda5944c8a353586916ae9 )
To display a view over other apps on Android 7.1 or above you may need to request the SYSTEM_ALERT_WINDOW permission, prompt the user to get the Draw Over Apps permission, and use another view type, such as TYPE_SYSTEM_OVERLAY.
I created an AlertDialog. And I need to put a timer there somehow.
Timer must show time from 90 seconds to 0 seconds.
Does someone know how to make that inscription in textView("90 sseconds to acceptance...") change every second with different text? ("90 sseconds to acceptance..." -> "89 sseconds to acceptance..." -> etc..)
Does this work for you?
/*
new CountDownUpdate((TextView)findViewById(R.id.accept_text), 90,
new CountDownUpdate.Callback(){
#Override
public void onCountDownComplete(TextView textView)
{
Toast.makeText(getApplicationContext(), "BOOM!", Toast.LENGTH_LONG).show();
}
});
*/
private static class CountDownUpdate implements Runnable
{
private Callback mCallback;
private int mFrom;
private TextView mView;
public CountDownUpdate(TextView view, int from, Callback callback)
{
mCallback = callback;
mFrom = from;
mView = view;
mView.post(this);
}
#Override
public void run()
{
mView.setText(mFrom + " seconds to acceptance...");
if(mFrom-- == 0){
if(mCallback != null){
mCallback.onCountDownComplete(mView);
}
}
else{
mView.postDelayed(this, 1000);
}
}
public static interface Callback
{
public void onCountDownComplete(TextView textView);
}
}
When the screen rotates my seekbar's colored bar goes back to its initial value, while the thumb remains at the correct position.
Basically from this:
It becomes like this:
Note that the TextView showing 15 is connected to the seekbar and correctly shows the same value, which is updated in onCreateView retrieving the value with getProgress on the seekbar, so the seekbar has the correct progress internally but "forgets" to update its bar. Note also that if moved slightly, the bar will be updated correctly.
The strange thing is that I have an identical seekbar, on which I do exactly the same actions(method calls etc) but this one never has this problem.
They are defined in the same way in the XML layout file(except for the id).
These seekbars are inside a Fragment shown into a ViewPager, here's more or less the code for the fragment:
public class NewCharacterNameFragment extends Fragment
implements NumericSeekBar.OnValueChangedListener {
private static final String LEVEL = "org.my.package.LEVEL";
private static final String STATS = "org.my.package.STATS";
private NumericSeekBar levelBar; // Causing problems
private NumericSeekBar statsBar; // Well behaved
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
if (savedInstanceState == null) { // defaults values to avoid multiple checks later
savedInstanceState = new Bundle();
savedInstanceState.putInt(LEVEL, 1);
savedInstanceState.putInt(STATS, 30);
}
View view = inflater.inflate(R.layout.new_character_name_fragment, container,
false);
levelBar = (NumericSeekBar) view.findViewById(R.id.levelSeekBar);
statsBar = (NumericSeekBar) view.findViewById(R.id.statPointsSeekBar);
levelBar.setValue(savedInstanceState.getInt(LEVEL));
levelBar.setMax(20);
levelBar.setValueChangedListener(this);
statsBar.setValue(savedInstanceState.getInt(STATS));
statsBar.setMax(100);
statsBar.setValueChangedListener(this);
// Initialize the text-views with the progress values:
TextView tView = (TextView) view.findViewById(R.id.statPointsNumTextView);
tView.setText(Integer.valueOf(statsBar.getValue()).toString());
tView = (TextView) view.findViewById(R.id.levelNumTextView);
tView.setText(Integer.valueOf(levelBar.getValue()).toString());
return view;
}
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
savedInstanceState.putInt(LEVEL, levelBar.getValue());
savedInstanceState.putInt(STATS, statsBar.getValue());
}
#Override
public void onNumericSeekBarValueChanged(NumericSeekBar bar, int value,
boolean fromUser) {
// Called whenever the seekbar value changes
if (bar == statsBar) {
TextView view = (TextView) getView().findViewById(R.id.statPointsNumTextView);
view.setText(Integer.valueOf(value).toString());
} else if (bar == levelBar) {
TextView view = (TextView) getView().findViewById(R.id.levelNumTextView);
view.setText(Integer.valueOf(value).toString());
}
}
}
Where NumericSeekBar is a widget I created and is basically a LinearLayout with the two increment and decrement buttons and the seekbar:
public class NumericSeekBar extends LinearLayout
implements SeekBar.OnSeekBarChangeListener, View.OnClickListener {
public interface OnValueChangedListener {
public void onNumericSeekBarValueChanged(NumericSeekBar bar, int value,
boolean fromUser);
}
private Button incButton;
private Button decButton;
private SeekBar seekBar;
private OnValueChangedListener listener = null;
private int maxValue = 100;
private int value = 0;
public NumericSeekBar(Context ctx) {
super(ctx);
setOpts();
setWidgets();
}
public NumericSeekBar(Context ctx, AttributeSet attributes) {
super(ctx, attributes);
setOpts();
setWidgets();
}
public void setValueChangedListener(OnValueChangedListener listener) {
this.listener = listener;
}
public void setMax(int maxValue) {
this.maxValue = maxValue;
seekBar.setMax(maxValue);
}
public int getValue() {
return this.value; // using seekBar.getProgress() obtains same results
}
public boolean setValue(int value) {
if (value < 0 || value > maxValue) {
return false;
}
this.value = value;
seekBar.setProgress(value);
return true;
}
#Override
public void onStopTrackingTouch(SeekBar bar) {
bar.setSecondaryProgress(bar.getProgress());
}
#Override
public void onStartTrackingTouch(SeekBar bar) {
}
#Override
public void onProgressChanged(SeekBar bar, int value, boolean fromUser) {
this.value = value;
if (listener != null ){
listener.onNumericSeekBarValueChanged(this, value, fromUser);
}
if (!fromUser) {
bar.setSecondaryProgress(0);
}
}
#Override
public void onClick(View v) {
// Handle increment/decrement button clicks
if (v.equals(incButton)) {
this.setValue(this.getValue() + 1);
} else if(v.equals(decButton)) {
this.setValue(this.getValue() - 1);
}
}
private void setOpts() {
setOrientation(HORIZONTAL);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setShowDividers(SHOW_DIVIDER_NONE);
}
}
private void setWidgets() {
incButton = new Button(getContext());
decButton = new Button(getContext());
seekBar = new SeekBar(getContext());
incButton.setText("+");
incButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
incButton.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
(float) 0.4));
incButton.setOnClickListener(this);
decButton.setText("-");
decButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
decButton.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
(float) 0.4));
decButton.setOnClickListener(this);
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
(float) 0.2);
layoutParams.gravity = Gravity.CENTER;
seekBar.setLayoutParams(layoutParams);
setMax(this.maxValue);
setValue(this.value);
addView(incButton);
addView(seekBar);
addView(decButton);
seekBar.setOnSeekBarChangeListener(this);
}
}
This happens both on the emulator and on a physical device.
EDIT: I just tested my app on an android 4 emulator and this does not happen, so it seems to be something 2.x related.
EDIT2: I've tried to invalidate() the bars in onCreateView, onStart and onResume but the problems still occurs. I've also tried to put a levelBar.setValue(levelBar.getValue()) in onResume but nothing changed.
I really don't understand what's happening.
EDIT3: I've added an other fragment which contains six of these bars and only the levelBar in the code above behaves strangely. I wonder how is this possible. Either there is some really strange bug, or I'm doing something not properly, even though I can't see where(and in android 4.x all works well).
EDIT4: My third edit is incorrect: now almost all the bars have this behaviour. The statsBar above seems to be the only one that is never affected.
I finally understood what's wrong with the code.
It seems like changing the maximum value of a SeekBar does not trigger a repaint of the color bar while it does trigger a repaint of the thumb. This is probably a bug in android 2.x(since it does not happen in android 4.x).
To solve this problem you simply have to set the maximum value before setting the progress on the seek-bar.
In my case only some bars were affected because I set the default maximum for the NumericSeekBar to 100, and only the bars with a different maximum where affected.
It's still not clear why invalidating the view in onResume does not produce the correct re-drawn of the widget.
I find the recurrence method:
final SeekBar seekBar = (SeekBar) findViewById(R.id.test_seekbar);
seekBar.setMax(4000);
seekBar.setProgress(1500);
new Thread(new Runnable() {
public void run() {
seekBar.setProgress(500);
}
}).start();
for (int i = 0; i < 5000; i++) {
seekBar.setProgress(2000);
}
above code, called ProgressBar#setProgress in work thread, it causes the bug.
Android official documentation advice called it at main thread, like this:
public class MyActivity extends Activity {
private static final int PROGRESS = 0x1;
private ProgressBar mProgress;
private int mProgressStatus = 0;
private Handler mHandler = new Handler();
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.progressbar_activity);
mProgress = (ProgressBar) findViewById(R.id.progress_bar);
// Start lengthy operation in a background thread
new Thread(new Runnable() {
public void run() {
while (mProgressStatus < 100) {
mProgressStatus = doWork();
// Update the progress bar
mHandler.post(new Runnable() {
public void run() {
mProgress.setProgress(mProgressStatus);
}
});
}
}
}).start();
}
}
I have searched this issue for hours...
Is it possible to launch google maps navigation within my app and display a textview with some information on it? I have to create an app which passes the destination address to Maps Navigation and while Navigation is working show a textview with the cars model name on the bottom of the app. Is this doable?
Is it possible to launch google maps navigation within my app and display a textview with some information on it?
You cannot embed other applications in yours, and you cannot add your own widgets to some other application's UI.
Try this.
public class FloatingOverNewBooking extends Service {
private WindowManager windowManager;
private FrameLayout frameLayout;
private String str_ride_id;
public static final String BROADCAST_ACTION = "com.yourpackage.YourActivity";
#Nullable
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onCreate() {
super.onCreate();
timerLocation = new Timer();
createFloatingBackButton();
}
Timer timerLocation;
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
// to receive any data from activity
str_ride_id = intent.getStringExtra("RIDE_ID");
return START_STICKY;
}
#Override
public void onDestroy() {
super.onDestroy();
if (frameLayout != null) {
//((WindowManager) getSystemService(WINDOW_SERVICE)).removeView(frameLayout);
windowManager.removeView(frameLayout);
frameLayout = null;
}
timerLocation.cancel();
}
private void createFloatingBackButton() {
ClientLocatedActivity.isFloatingIconServiceAlive = true;
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
WindowManager.LayoutParams windowManagerParams = new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY ,
WindowManager.LayoutParams. FLAG_DIM_BEHIND, PixelFormat.TRANSLUCENT);
params.gravity = Gravity.LEFT | Gravity.CENTER_VERTICAL;
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
frameLayout = new FrameLayout(this);
LayoutInflater layoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
// Here is the place where you can inject whatever layout you want in the frame layout
layoutInflater.inflate(R.layout.share_new_booking_alert, frameLayout);
final TextView txtName = (TextView) frameLayout.findViewById(R.id.txtName);
Button backOnMap = (Button) frameLayout.findViewById(R.id.dialog_button);
if(!ObjectUtility.isNullOrEmpty(Config.Share.newPassenger)){
txtName.setText(Config.Share.newPassenger.getUsername());
}
backOnMap.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
try {
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
am.killBackgroundProcesses("com.google.android.apps.maps");
//MainActivity.getInstance().getShareRideDataById("go");
FloatingOverNewBooking.this.stopSelf();
ClientLocatedActivity.isFloatingIconServiceAlive = false;
} catch (Exception e) {
e.printStackTrace();
}
}
});
windowManager.addView(frameLayout, windowManagerParams);
}
}