Ces jours-ci, je travaille sur la simulation du dialogue modal sous Android. J'ai beaucoup cherché sur Google, il y a beaucoup de discussions, mais malheureusement, il n'y a pas beaucoup d'options pour le faire modal. Voici un fond,
Dialogues, dialogues modaux et Blockin
Dialogs/AlertDialogs: Comment "bloquer l'exécution" lorsque la boîte de dialogue est active (style .NET)
Il n’existe aucun moyen d’obtenir un comportement modal, alors j’ai proposé trois solutions possibles,
1. Utilisez une activité basée sur le dialogue, comme ceci thread , mais je ne peux toujours pas faire en sorte que l'activité principale attende réellement le retour de l'activité de dialogue. L'activité principale s'est transformée pour arrêter le statut et a ensuite été redémarrée.
2. Construisez un thread de travail et utilisez la synchronisation de thread. Cependant, c'est un travail de refactoring énorme pour mon application. Maintenant, j'ai une seule activité principale et un service à la fois dans le fil principal de l'interface utilisateur.
3. Prendre en charge la gestion des événements dans une boucle lorsqu'il existe une boîte de dialogue modale et quitter la boucle lorsque la boîte de dialogue est fermée. En fait, c’est le moyen de construire un vrai dialogue modal comme dans Windows. Je n'ai toujours pas prototypé de cette façon.
Je voudrais toujours simuler avec une activité sur le thème du dialogue,
1. démarrer une activité de dialogue par startActivityForResult ()
2. obtenir le résultat de onActivityResult ()
Voici une source
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MyView v = new MyView(this);
setContentView(v);
}
private final int RESULT_CODE_ALERT = 1;
private boolean mAlertResult = false;
public boolean startAlertDialog() {
Intent it = new Intent(this, DialogActivity.class);
it.putExtra("AlertInfo", "This is an alert");
startActivityForResult(it, RESULT_CODE_ALERT);
// I want to wait right here
return mAlertResult;
}
@Override
protected void onActivityResult (int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case RESULT_CODE_ALERT:
Bundle ret = data.getExtras();
mAlertResult = ret.getBoolean("AlertResult");
break;
}
}
}
L'appelant de startAlertDialog bloque l'exécution et attend le résultat renvoyé. Mais startAlertDialog est revenu immédiatement, bien sûr, et l'activité principale est passée au statut STOP pendant que DialogActivity était actif.
La question est donc de savoir comment faire en sorte que l'activité principale attende vraiment le résultat.
Merci.
J'ai eu un dialogue modal en utilisant:
setCancelable(false);
sur le DialogFragment (pas sur le DialogBuilder).
Ce n'est pas possible comme vous l'aviez prévu. Tout d'abord, vous n'êtes pas autorisé à bloquer le thread d'interface utilisateur. Votre application sera terminée. Deuxièmement, vous devez gérer les méthodes de cycle de vie appelées lors du lancement d'une autre activité avec startActivity
(votre activité d'origine sera suspendue pendant l'exécution de l'autre activité). Troisièmement, vous pourriez probablement le pirater en utilisant startAlertDialog()
et non à partir du thread d'interface utilisateur, avec une synchronisation de thread (comme Object.wait()
) et un certain AlertDialog
. Cependant, je fortement vous encourage à ne pas le faire. C'est moche, ça va certainement casser et ce n'est tout simplement pas comme cela que les choses sont censées fonctionner.
Modifiez votre approche pour capturer la nature asynchrone de ces événements. Si vous souhaitez par exemple une boîte de dialogue qui demande à l'utilisateur une décision (comme accepter le type de service ou non) et effectuer des actions spéciales en fonction de cette décision, créez une boîte de dialogue comme celle-ci:
AlertDialog dialog = new AlertDialog.Builder(context).setMessage(R.string.someText)
.setPositiveButton(Android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
// Do stuff if user accepts
}
}).setNegativeButton(Android.R.string.cancel, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
// Do stuff when user neglects.
}
}).setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
// Do stuff when cancelled
}
}).create();
dialog.show();
Ensuite, utilisez deux méthodes pour gérer la rétroaction positive ou négative en conséquence (c’est-à-dire procéder à une opération quelconque ou terminer l’activité ou autre chose qui a du sens).
Les développeurs d'Android et iOS ont décidé qu'ils étaient assez puissants et intelligents pour rejeter la conception de Modal Dialog (qui existait déjà sur le marché depuis de nombreuses années et qui ne dérangeait personne auparavant), malheureusement pour nous.
Voici ma solution, ça marche très bien:
int pressedButtonID;
private final Semaphore dialogSemaphore = new Semaphore(0, true);
final Runnable mMyDialog = new Runnable()
{
public void run()
{
AlertDialog errorDialog = new AlertDialog.Builder( [your activity object here] ).create();
errorDialog.setMessage("My dialog!");
errorDialog.setButton("My Button1", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
pressedButtonID = MY_BUTTON_ID1;
dialogSemaphore.release();
}
});
errorDialog.setButton2("My Button2", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
pressedButtonID = MY_BUTTON_ID2;
dialogSemaphore.release();
}
});
errorDialog.setCancelable(false);
errorDialog.show();
}
};
public int ShowMyModalDialog() //should be called from non-UI thread
{
pressedButtonID = MY_BUTTON_INVALID_ID;
runOnUiThread(mMyDialog);
try
{
dialogSemaphore.acquire();
}
catch (InterruptedException e)
{
}
return pressedButtonID;
}
Cela fonctionne pour moi: créez une activité comme dialogue. Ensuite,
Ajoutez ceci à votre manifeste pour l'activité:
Android: theme = "@ Android: style/Theme.Dialog"
Ajoutez ceci à onCreate de votre activité
setFinishOnTouchOutside (false);
Remplacer onBackPressed dans votre activité:
@Override Public void onBackPressed () { // empêche le "retour" de quitter cette activité }
La première donne à l'activité l'aspect du dialogue. Les deux derniers le font se comporter comme un dialogue modal.
Finalement, je me suis retrouvé avec une solution vraiment simple et directe.
Les personnes familiarisées avec la programmation Win32 savent peut-être comment implémenter un dialogue modal. Généralement, il exécute une boucle de message imbriquée (par GetMessage/PostMessage) lorsqu'il existe une boîte de dialogue modale. J'ai donc essayé de mettre en œuvre mon propre dialogue modal de cette manière traditionnelle.
Au début, Android ne fournissait pas d'interfaces à injecter dans la boucle de messages du thread ui, ou je n'en trouvais pas. Quand j'ai examiné le source, Looper.loop (), j'ai trouvé que c'était exactement ce que je voulais. Néanmoins, MessageQueue/Message n’a pas fourni d’interfaces publiques. Heureusement, nous avons des réflexions dans Java . En gros, je viens de copier exactement ce que Looper.loop () a fait, il a bloqué le flux de travail et a toujours géré correctement les événements. Je n'ai pas testé le dialogue modal imbriqué, mais théoriquement, cela fonctionnerait.
Voici mon code source,
public class ModalDialog {
private boolean mChoice = false;
private boolean mQuitModal = false;
private Method mMsgQueueNextMethod = null;
private Field mMsgTargetFiled = null;
public ModalDialog() {
}
public void showAlertDialog(Context context, String info) {
if (!prepareModal()) {
return;
}
// build alert dialog
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(info);
builder.setCancelable(false);
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
ModalDialog.this.mQuitModal = true;
dialog.dismiss();
}
});
AlertDialog alert = builder.create();
alert.show();
// run in modal mode
doModal();
}
public boolean showConfirmDialog(Context context, String info) {
if (!prepareModal()) {
return false;
}
// reset choice
mChoice = false;
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(info);
builder.setCancelable(false);
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
ModalDialog.this.mQuitModal = true;
ModalDialog.this.mChoice = true;
dialog.dismiss();
}
});
builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
ModalDialog.this.mQuitModal = true;
ModalDialog.this.mChoice = false;
dialog.cancel();
}
});
AlertDialog alert = builder.create();
alert.show();
doModal();
return mChoice;
}
private boolean prepareModal() {
Class<?> clsMsgQueue = null;
Class<?> clsMessage = null;
try {
clsMsgQueue = Class.forName("Android.os.MessageQueue");
} catch (ClassNotFoundException e) {
e.printStackTrace();
return false;
}
try {
clsMessage = Class.forName("Android.os.Message");
} catch (ClassNotFoundException e) {
e.printStackTrace();
return false;
}
try {
mMsgQueueNextMethod = clsMsgQueue.getDeclaredMethod("next", new Class[]{});
} catch (SecurityException e) {
e.printStackTrace();
return false;
} catch (NoSuchMethodException e) {
e.printStackTrace();
return false;
}
mMsgQueueNextMethod.setAccessible(true);
try {
mMsgTargetFiled = clsMessage.getDeclaredField("target");
} catch (SecurityException e) {
e.printStackTrace();
return false;
} catch (NoSuchFieldException e) {
e.printStackTrace();
return false;
}
mMsgTargetFiled.setAccessible(true);
return true;
}
private void doModal() {
mQuitModal = false;
// get message queue associated with main UI thread
MessageQueue queue = Looper.myQueue();
while (!mQuitModal) {
// call queue.next(), might block
Message msg = null;
try {
msg = (Message)mMsgQueueNextMethod.invoke(queue, new Object[]{});
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
if (null != msg) {
Handler target = null;
try {
target = (Handler)mMsgTargetFiled.get(msg);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
if (target == null) {
// No target is a magic identifier for the quit message.
mQuitModal = true;
}
target.dispatchMessage(msg);
msg.recycle();
}
}
}
}
J'espère que cela aiderait.
Une solution est:
alert.show();
doit être la dernière ligne de code de la fonction appelant l'alerte. Tout code après cette ligne n'attendra pas de fermer l'alerte, mais s'exécutera immédiatement.Espérons l'aide!
Je ne suis pas sûr que ce soit à 100% modal, car vous pouvez cliquer sur un autre composant pour fermer la boîte de dialogue, mais je me suis trompé avec les constructions en boucle. Cela a bien fonctionné pour moi, alors j'aimerais partager l'idée. Vous pouvez créer et ouvrir la boîte de dialogue selon une méthode, puis la fermer dans la méthode de rappel et le programme attendra la réponse de la boîte de dialogue avant d'exécuter la méthode de rappel. Si vous exécutez ensuite le reste de la méthode de rappel dans un nouveau thread, la boîte de dialogue se ferme également avant que le reste du code ne soit exécuté. La seule chose que vous devez faire est d’avoir une variable de boîte de dialogue globale, afin que différentes méthodes puissent y accéder. Donc, quelque chose comme ce qui suit peut fonctionner:
public class MyActivity extends ...
{
/** Global dialog reference */
private AlertDialog okDialog;
/** Show the dialog box */
public void showDialog(View view)
{
// prepare the alert box
AlertDialog.Builder alertBox = new AlertDialog.Builder(...);
...
// set a negative/no button and create a listener
alertBox.setNegativeButton("No", new DialogInterface.OnClickListener() {
// do something when the button is clicked
public void onClick(DialogInterface arg0, int arg1) {
//no reply or do nothing;
}
});
// set a positive/yes button and create a listener
alertBox.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
// do something when the button is clicked
public void onClick(DialogInterface arg0, int arg1) {
callbackMethod(params);
}
});
//show the dialog
okDialog = alertBox.create();
okDialog.show();
}
/** The yes reply method */
private void callbackMethod(params)
{
//first statement closes the dialog box
okDialog.dismiss();
//the other statements run in a new thread
new Thread() {
public void run() {
try {
//statements or even a runOnUiThread
}
catch (Exception ex) {
...
}
}
}.start();
}
}
Comme hackbod et d'autres l'ont fait remarquer, Android ne fournit délibérément aucune méthode pour créer des boucles d'événements imbriquées. Je comprends les raisons, mais certaines situations les exigent. Dans notre cas, nous avons notre propre machine virtuelle fonctionnant sur diverses plates-formes et nous voulions la porter sur Android. En interne, il y a beaucoup d'endroits où cela nécessite une boucle d'événements imbriquée, et il n'est pas vraiment faisable de réécrire le tout pour Android. Quoi qu'il en soit, voici une solution (prise à partir de Comment puis-je traiter des événements non bloquants sous Android? , mais j'ai ajouté un délai d'attente):
private class IdleHandler implements MessageQueue.IdleHandler
{
private Looper _looper;
private int _timeout;
protected IdleHandler(Looper looper, int timeout)
{
_looper = looper;
_timeout = timeout;
}
public boolean queueIdle()
{
_uiEventsHandler = new Handler(_looper);
if (_timeout > 0)
{
_uiEventsHandler.postDelayed(_uiEventsTask, _timeout);
}
else
{
_uiEventsHandler.post(_uiEventsTask);
}
return(false);
}
};
private boolean _processingEventsf = false;
private Handler _uiEventsHandler = null;
private Runnable _uiEventsTask = new Runnable()
{
public void run() {
Looper looper = Looper.myLooper();
looper.quit();
_uiEventsHandler.removeCallbacks(this);
_uiEventsHandler = null;
}
};
public void processEvents(int timeout)
{
if (!_processingEventsf)
{
Looper looper = Looper.myLooper();
looper.myQueue().addIdleHandler(new IdleHandler(looper, timeout));
_processingEventsf = true;
try
{
looper.loop();
} catch (RuntimeException re)
{
// We get an exception when we try to quit the loop.
}
_processingEventsf = false;
}
}
Ce n'est pas difficile.
Supposons que vous ayez un drapeau sur votre activité de propriétaire (nommée waiting_for_result
) à chaque reprise de votre activité:
public void onResume(){
if (waiting_for_result) {
// Start the dialog Activity
}
}
Cela garantissait l'activité du propriétaire, sauf si la boîte de dialogue modale est fermée, chaque fois qu'il tente d'obtenir le focus passera à l'activité de la boîte de dialogue modale.
J'ai une solution similaire à celle de cinquième , mais c'est un peu plus simple et n'a pas besoin de réflexion. Je pensais pourquoi ne pas utiliser une exception pour sortir du looper. Donc, mon looper personnalisé Se lit comme suit:
1) L'exception qui est levée:
final class KillException extends RuntimeException {
}
2) Le boucleur personnalisé:
public final class KillLooper implements Runnable {
private final static KillLooper DEFAULT = new KillLooper();
private KillLooper() {
}
public static void loop() {
try {
Looper.loop();
} catch (KillException x) {
/* */
}
}
public static void quit(View v) {
v.post(KillLooper.DEFAULT);
}
public void run() {
throw new KillException();
}
}
L'utilisation du boucleur personnalisé est assez simple. Supposons que. Vous ayez un dialogue foo, puis procédez simplement comme suit Où vous voulez appeler le dialogue foo de manière modale:
a) Lorsque vous appelez foo:
foo.show();
KillLooper.loop();
Dans la boîte de dialogue foo, lorsque vous souhaitez quitter, il vous suffit.appeler la méthode quit du boucleur personnalisé. Ce looksas suit:
b) En sortant de foo:
dismiss();
KillLooper.quit(getContentView());
J'ai récemment vu quelques problèmes avec Android 5.1.1., N'appelez pas de dialogue modal à partir du menu principal, mais signalez plutôt un événement Qui appelle le dialogue modal. Sans poster, le menu principal .__ va caler et j'ai vu Looper :: pollInner () SIGSEGV dans mon application.
Utilisez un BroadcastReceiver qui appelle la méthode suivante requise dans l’activité.
Termine le code d'activité dans dialogFragment.show (fragmentTransaction, TAG); et continuez-le dans onReceive () - Je ne suis pas 100% positif, mais je déposerais de l'argent pour démarrerActivityForResult (); est basé sur exactement ce concept.
Jusqu'à ce que cette méthode soit invoquée par le destinataire, le code attendra l'interaction de l'utilisateur sans ANR.
Méthode onCreateView de DialogFragment
private static final String ACTION_CONTINUE = "com.package.name.action_continue";
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_dialog, container, false);
Button ok_button = v.findViewById(R.id.dialog_ok_button);
ok_button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent();
i.setAction(ACTION_CONTINUE);
getActivity().getApplicationContext().sendBroadcast(i);
dismiss();
}
});
return v;
}
Cette méthode dépend de la construction d'une classe d'extension DialogFrament et de l'appel d'une instance de cette classe via l'activité.
Toutefois...
Simple, clair, facile et vraiment modal.