Hei*_*nen 515 android android-dialog android-activity
我的程序在后台线程中执行一些网络活动.在开始之前,它会弹出一个进度对话框.该对话框在处理程序上被关闭.这一切都很好,除非在对话框启动时屏幕方向发生变化(后台线程正在运行).此时,应用程序崩溃或死锁,或进入一个奇怪的阶段,在应用程序完全无法工作之前,直到所有线程都被杀死.
如何优雅地处理屏幕方向变化?
下面的示例代码大致匹配我的真实程序:
public class MyAct extends Activity implements Runnable {
public ProgressDialog mProgress;
// UI has a button that when pressed calls send
public void send() {
mProgress = ProgressDialog.show(this, "Please wait",
"Please wait",
true, true);
Thread thread = new Thread(this);
thread.start();
}
public void run() {
Thread.sleep(10000);
Message msg = new Message();
mHandler.sendMessage(msg);
}
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
mProgress.dismiss();
}
};
}
Run Code Online (Sandbox Code Playgroud)
堆:
E/WindowManager( 244): Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager( 244): android.view.WindowLeaked: Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager( 244): at android.view.ViewRoot.<init>(ViewRoot.java:178)
E/WindowManager( 244): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:147)
E/WindowManager( 244): at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:90)
E/WindowManager( 244): at android.view.Window$LocalWindowManager.addView(Window.java:393)
E/WindowManager( 244): at android.app.Dialog.show(Dialog.java:212)
E/WindowManager( 244): at android.app.ProgressDialog.show(ProgressDialog.java:103)
E/WindowManager( 244): at android.app.ProgressDialog.show(ProgressDialog.java:91)
E/WindowManager( 244): at MyAct.send(MyAct.java:294)
E/WindowManager( 244): at MyAct$4.onClick(MyAct.java:174)
E/WindowManager( 244): at android.view.View.performClick(View.java:2129)
E/WindowManager( 244): at android.view.View.onTouchEvent(View.java:3543)
E/WindowManager( 244): at android.widget.TextView.onTouchEvent(TextView.java:4664)
E/WindowManager( 244): at android.view.View.dispatchTouchEvent(View.java:3198)
Run Code Online (Sandbox Code Playgroud)
我试图在onSaveInstanceState中关闭进度对话框,但这只是防止立即崩溃.后台线程仍在继续,UI处于部分绘制状态.需要在重新开始工作之前杀死整个应用程序.
小智 259
编辑:谷歌工程师不推荐这种方法,正如Dianne Hackborn(又名hackbod)在这篇StackOverflow帖子中所描述的那样.查看此博客文章了解更多信息.
您必须将此添加到清单中的活动声明:
android:configChanges="orientation|screenSize"
Run Code Online (Sandbox Code Playgroud)
所以它看起来像
<activity android:label="@string/app_name"
android:configChanges="orientation|screenSize|keyboardHidden"
android:name=".your.package">
Run Code Online (Sandbox Code Playgroud)
问题是当配置发生变化时,系统会破坏活动.请参阅ConfigurationChanges.
因此,将其放在配置文件中可以避免系统破坏您的活动.相反,它会调用该onConfigurationChanged(Configuration)方法.
has*_*man 152
当您切换方向时,Android将创建一个新的视图.您可能正在崩溃,因为您的后台线程正在尝试更改旧的状态.(它可能也有问题,因为你的后台线程不在UI线程上)
我建议使mHandler易变,并在方向改变时更新它.
jfe*_*ron 68
我想出了一个坚如磐石的解决方案来解决这些问题,这些解决方案符合"Android方式"的要求.我使用IntentService模式进行了所有长时间运行的操作.
也就是说,我的活动广播意图,IntentService完成工作,将数据保存在数据库中,然后广播粘性意图.粘性部分很重要,这样即使在用户启动工作期间暂停活动并且错过了IntentService的实时广播,我们仍然可以响应并从调用活动中获取数据.ProgressDialogs可以很好地使用这种模式onSaveInstanceState().
基本上,您需要保存一个标志,您在已保存的实例包中运行了一个进度对话框.不要保存进度对话框对象,因为这会泄漏整个Activity.要拥有进度对话框的持久句柄,我将其存储为应用程序对象中的弱引用.在方向更改或导致活动暂停的任何其他内容(电话呼叫,用户点击回家等)然后恢复时,我会关闭旧对话框并在新创建的活动中重新创建一个新对话框.
对于无限期的进度对话,这很容易.对于进度条样式,您必须在捆绑中放置最后的已知进度以及您在活动中本地使用的任何信息以跟踪进度.在恢复进度时,您将使用此信息以与以前相同的状态重新生成进度条,然后根据事物的当前状态进行更新.
总而言之,将长时间运行的任务放入IntentService并加上明智的使用,onSaveInstanceState()可以让您有效地跟踪对话并在整个Activity生命周期事件中恢复.活动代码的相关位如下.你还需要在BroadcastReceiver中使用逻辑来适当地处理Sticky意图,但这超出了这个范围.
public void doSignIn(View view) {
waiting=true;
AppClass app=(AppClass) getApplication();
String logingon=getString(R.string.signon);
app.Dialog=new WeakReference<ProgressDialog>(ProgressDialog.show(AddAccount.this, "", logingon, true));
...
}
@Override
protected void onSaveInstanceState(Bundle saveState) {
super.onSaveInstanceState(saveState);
saveState.putBoolean("waiting",waiting);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState!=null) {
restoreProgress(savedInstanceState);
}
...
}
private void restoreProgress(Bundle savedInstanceState) {
waiting=savedInstanceState.getBoolean("waiting");
if (waiting) {
AppClass app=(AppClass) getApplication();
ProgressDialog refresher=(ProgressDialog) app.Dialog.get();
refresher.dismiss();
String logingon=getString(R.string.signon);
app.Dialog=new WeakReference<ProgressDialog>(ProgressDialog.show(AddAccount.this, "", logingon, true));
}
}
Run Code Online (Sandbox Code Playgroud)
小智 28
我遇到了同样的问题.我的活动需要从URL解析一些数据并且速度很慢.所以我创建了一个线程来执行此操作,然后显示进度对话框.我让线程Handler在完成后将消息发回UI线程.在Handler.handleMessage,我从线程获取数据对象(现在准备好)并将其填充到UI.所以它与你的例子非常相似.
经过大量的反复试验后,我发现了一个解决方案.至少现在我可以在线程完成之前或之后的任何时刻旋转屏幕.在所有测试中,对话框都已正确关闭,所有行为都符合预期.
我做了什么如下所示.目标是填充我的数据模型(mDataObject),然后将其填充到UI.应该允许屏幕随时旋转而不会出现意外.
class MyActivity {
private MyDataObject mDataObject = null;
private static MyThread mParserThread = null; // static, or make it singleton
OnCreate() {
...
Object retained = this.getLastNonConfigurationInstance();
if(retained != null) {
// data is already completely obtained before config change
// by my previous self.
// no need to create thread or show dialog at all
mDataObject = (MyDataObject) retained;
populateUI();
} else if(mParserThread != null && mParserThread.isAlive()){
// note: mParserThread is a static member or singleton object.
// config changed during parsing in previous instance. swap handler
// then wait for it to finish.
mParserThread.setHandler(new MyHandler());
} else {
// no data and no thread. likely initial run
// create thread, show dialog
mParserThread = new MyThread(..., new MyHandler());
mParserThread.start();
showDialog(DIALOG_PROGRESS);
}
}
// http://android-developers.blogspot.com/2009/02/faster-screen-orientation-change.html
public Object onRetainNonConfigurationInstance() {
// my future self can get this without re-downloading
// if it's already ready.
return mDataObject;
}
// use Activity.showDialog instead of ProgressDialog.show
// so the dialog can be automatically managed across config change
@Override
protected Dialog onCreateDialog(int id) {
// show progress dialog here
}
// inner class of MyActivity
private class MyHandler extends Handler {
public void handleMessage(msg) {
mDataObject = mParserThread.getDataObject();
populateUI();
dismissDialog(DIALOG_PROGRESS);
}
}
}
class MyThread extends Thread {
Handler mHandler;
MyDataObject mDataObject;
// constructor with handler param
public MyHandler(..., Handler h) {
...
mHandler = h;
}
public void setHandler(Handler h) { mHandler = h; } // for handler swapping after config change
public MyDataObject getDataObject() { return mDataObject; } // return data object (completed) to caller
public void run() {
mDataObject = new MyDataObject();
// do the lengthy task to fill mDataObject with data
lengthyTask(mDataObject);
// done. notify activity
mHandler.sendEmptyMessage(0); // tell activity: i'm ready. come pick up the data.
}
}
Run Code Online (Sandbox Code Playgroud)
这对我有用.我不知道这是否是Android设计的"正确"方法 - 他们声称这种"在屏幕旋转期间破坏/重新创建活动"实际上使事情变得更容易,所以我想它不应该太棘手.
如果您在我的代码中发现问题,请告诉我.如上所述,我真的不知道是否有任何副作用.
gym*_*hoe 15
最初的感知问题是代码无法在屏幕方向更改中存活.显然,通过让程序自己处理屏幕方向更改而不是让UI框架执行它(通过调用onDestroy)来"解决"这个问题.
我想提出的是,如果根本问题是,该方案将无法生存的onDestroy(),然后接受的解决方案就是这样留下严重的其他问题和漏洞的程序的解决方法.请记住,Android框架明确指出,由于您无法控制的情况,您的活动几乎可以随时被销毁.因此,您的活动必须能够以任何理由存在onDestroy()和后续onCreate(),而不仅仅是屏幕方向更改.
如果您要自己接受处理屏幕方向更改以解决OP的问题,则需要验证onDestroy()的其他原因不会导致相同的错误.你能做到吗?如果没有,我会质疑"接受"的答案是否真的是一个非常好的答案.
小智 14
我的解决方案是扩展ProgressDialog课程以获得自己的课程MyProgressDialog.
我重新定义show()和dismiss()方法,以显示之前锁定方向Dialog和解锁回来时Dialog被驳回.因此,当Dialog显示并且设备的方向改变时,屏幕的方向保持不变直到dismiss()被调用,然后屏幕方向根据传感器值/设备方向而改变.
这是我的代码:
public class MyProgressDialog extends ProgressDialog {
private Context mContext;
public MyProgressDialog(Context context) {
super(context);
mContext = context;
}
public MyProgressDialog(Context context, int theme) {
super(context, theme);
mContext = context;
}
public void show() {
if (mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
else
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
super.show();
}
public void dismiss() {
super.dismiss();
((Activity) mContext).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
}
}
Run Code Online (Sandbox Code Playgroud)
我遇到了同样的问题,我想出了一个没有使用ProgressDialog进行访问的解决方案,我得到了更快的结果.
我所做的是创建一个包含ProgressBar的布局.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ProgressBar
android:id="@+id/progressImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
/>
</RelativeLayout>
Run Code Online (Sandbox Code Playgroud)
然后在onCreate方法中执行以下操作
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.progress);
}
Run Code Online (Sandbox Code Playgroud)
然后在一个线程中完成长任务,当完成后,Runnable将内容视图设置为您要用于此活动的实际布局.
例如:
mHandler.post(new Runnable(){
public void run() {
setContentView(R.layout.my_layout);
}
});
Run Code Online (Sandbox Code Playgroud)
这就是我所做的,而且我发现它比显示ProgressDialog运行得更快,并且它的侵入性更小,并且在我看来更好看.
但是,如果您想使用ProgressDialog,那么这个答案不适合您.
我将贡献我的方法来处理这个轮换问题.这可能与OP没有关系AsyncTask,因为他没有使用,但也许其他人会觉得它很有用.这很简单,但它似乎为我做的工作:
我有一个AsyncTask名为嵌套类的登录活动BackgroundLoginTask.
在我看来,BackgroundLoginTask我不做任何与众不同的事情,除非在调用ProgressDialog解雇时添加一个空检查:
@Override
protected void onPostExecute(Boolean result)
{
if (pleaseWaitDialog != null)
pleaseWaitDialog.dismiss();
[...]
}
Run Code Online (Sandbox Code Playgroud)
这是为了处理后台任务在Activity不可见时完成的情况,因此,该onPause()方法已经取消了进度对话框.
接下来,在我的父Activity类中,我为我的AsyncTask类创建全局静态句柄,而我ProgressDialog(AsyncTask嵌套,可以访问这些变量):
private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;
Run Code Online (Sandbox Code Playgroud)
这有两个目的:首先,它允许我Activity始终访问AsyncTask对象,即使是新的后旋转活动.其次,它允许我在旋转后BackgroundLoginTask访问和关闭ProgressDialog.
接下来,我将其添加到onPause(),导致进度对话框在我们Activity离开前景时消失(防止丑陋的"强制关闭"崩溃):
if (pleaseWaitDialog != null)
pleaseWaitDialog.dismiss();
Run Code Online (Sandbox Code Playgroud)
最后,我的方法中有以下内容onResume():
if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
{
if (pleaseWaitDialog != null)
pleaseWaitDialog.show();
}
Run Code Online (Sandbox Code Playgroud)
这允许在Dialog重新Activity创建之后重新出现.
这是整个班级:
public class NSFkioskLoginActivity extends NSFkioskBaseActivity {
private static BackgroundLoginTask backgroundLoginTask;
private static ProgressDialog pleaseWaitDialog;
private Controller cont;
// This is the app entry point.
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (CredentialsAvailableAndValidated())
{
//Go to main menu and don't run rest of onCreate method.
gotoMainMenu();
return;
}
setContentView(R.layout.login);
populateStoredCredentials();
}
//Save current progress to options when app is leaving foreground
@Override
public void onPause()
{
super.onPause();
saveCredentialsToPreferences(false);
//Get rid of progress dialog in the event of a screen rotation. Prevents a crash.
if (pleaseWaitDialog != null)
pleaseWaitDialog.dismiss();
}
@Override
public void onResume()
{
super.onResume();
if ((backgroundLoginTask != null) && (backgroundLoginTask.getStatus() == Status.RUNNING))
{
if (pleaseWaitDialog != null)
pleaseWaitDialog.show();
}
}
/**
* Go to main menu, finishing this activity
*/
private void gotoMainMenu()
{
startActivity(new Intent(getApplicationContext(), NSFkioskMainMenuActivity.class));
finish();
}
/**
*
* @param setValidatedBooleanTrue If set true, method will set CREDS_HAVE_BEEN_VALIDATED to true in addition to saving username/password.
*/
private void saveCredentialsToPreferences(boolean setValidatedBooleanTrue)
{
SharedPreferences settings = getSharedPreferences(APP_PREFERENCES, MODE_PRIVATE);
SharedPreferences.Editor prefEditor = settings.edit();
EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
EditText pswText = (EditText) findViewById(R.id.editTextPassword);
prefEditor.putString(USERNAME, usernameText.getText().toString());
prefEditor.putString(PASSWORD, pswText.getText().toString());
if (setValidatedBooleanTrue)
prefEditor.putBoolean(CREDS_HAVE_BEEN_VALIDATED, true);
prefEditor.commit();
}
/**
* Checks if user is already signed in
*/
private boolean CredentialsAvailableAndValidated() {
SharedPreferences settings = getSharedPreferences(APP_PREFERENCES,
MODE_PRIVATE);
if (settings.contains(USERNAME) && settings.contains(PASSWORD) && settings.getBoolean(CREDS_HAVE_BEEN_VALIDATED, false) == true)
return true;
else
return false;
}
//Populate stored credentials, if any available
private void populateStoredCredentials()
{
SharedPreferences settings = getSharedPreferences(APP_PREFERENCES,
MODE_PRIVATE);
settings.getString(USERNAME, "");
EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
usernameText.setText(settings.getString(USERNAME, ""));
EditText pswText = (EditText) findViewById(R.id.editTextPassword);
pswText.setText(settings.getString(PASSWORD, ""));
}
/**
* Validate credentials in a seperate thread, displaying a progress circle in the meantime
* If successful, save credentials in preferences and proceed to main menu activity
* If not, display an error message
*/
public void loginButtonClick(View view)
{
if (phoneIsOnline())
{
EditText usernameText = (EditText) findViewById(R.id.editTextUsername);
EditText pswText = (EditText) findViewById(R.id.editTextPassword);
//Call background task worker with username and password params
backgroundLoginTask = new BackgroundLoginTask();
backgroundLoginTask.execute(usernameText.getText().toString(), pswText.getText().toString());
}
else
{
//Display toast informing of no internet access
String notOnlineMessage = getResources().getString(R.string.noNetworkAccessAvailable);
Toast toast = Toast.makeText(getApplicationContext(), notOnlineMessage, Toast.LENGTH_SHORT);
toast.show();
}
}
/**
*
* Takes two params: username and password
*
*/
public class BackgroundLoginTask extends AsyncTask<Object, String, Boolean>
{
private Exception e = null;
@Override
protected void onPreExecute()
{
cont = Controller.getInstance();
//Show progress dialog
String pleaseWait = getResources().getString(R.string.pleaseWait);
String commWithServer = getResources().getString(R.string.communicatingWithServer);
if (pleaseWaitDialog == null)
pleaseWaitDialog= ProgressDialog.show(NSFkioskLoginActivity.this, pleaseWait, commWithServer, true);
}
@Override
protected Boolean doInBackground(Object... params)
{
try {
//Returns true if credentials were valid. False if not. Exception if server could not be reached.
return cont.validateCredentials((String)params[0], (String)params[1]);
} catch (Exception e) {
this.e=e;
return false;
}
}
/**
* result is passed from doInBackground. Indicates whether credentials were validated.
*/
@Override
protected void onPostExecute(Boolean result)
{
//Hide progress dialog and handle exceptions
//Progress dialog may be null if rotation has been switched
if (pleaseWaitDialog != null)
{
pleaseWaitDialog.dismiss();
pleaseWaitDialog = null;
}
if (e != null)
{
//Show toast with exception text
String networkError = getResources().getString(R.string.serverErrorException);
Toast toast = Toast.makeText(getApplicationContext(), networkError, Toast.LENGTH_SHORT);
toast.show();
}
else
{
if (result == true)
{
saveCredentialsToPreferences(true);
gotoMainMenu();
}
else
{
String toastText = getResources().getString(R.string.invalidCredentialsEntered);
Toast toast = Toast.makeText(getApplicationContext(), toastText, Toast.LENGTH_SHORT);
toast.show();
}
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
我绝不是经验丰富的Android开发人员,所以请随时发表评论.
| 归档时间: |
|
| 查看次数: |
226432 次 |
| 最近记录: |