Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 屏幕旋轉 處理 AsyncTask 和 ProgressDialog 的最佳方案

Android 屏幕旋轉 處理 AsyncTask 和 ProgressDialog 的最佳方案

編輯:關於Android編程

 

1、概述

眾所周知,Activity在不明確指定屏幕方向和configChanges時,當用戶旋轉屏幕會重新啟動。當然了,應對這種情況,Android給出了幾種方案:

a、如果是少量數據,可以通過onSaveInstanceState()和onRestoreInstanceState()進行保存與恢復。

Android會在銷毀你的Activity之前調用onSaveInstanceState()方法,於是,你可以在此方法中存儲關於應用狀態的數據。然後你可以在onCreate()或onRestoreInstanceState()方法中恢復。

b、如果是大量數據,使用Fragment保持需要恢復的對象。

c、自已處理配置變化。

注:getLastNonConfigurationInstance()已經被棄用,被上述方法二替代。

2、難點

假設當前Activity在onCreate中啟動一個異步線程去夾在數據,當然為了給用戶一個很好的體驗,會有一個ProgressDialog,當數據加載完成,ProgressDialog消失,設置數據。

這裡,如果在異步數據完成加載之後,旋轉屏幕,使用上述a、b兩種方法都不會很難,無非是保存數據和恢復數據。

但是,如果正在線程加載的時候,進行旋轉,會存在以下問題:

a)此時數據沒有完成加載,onCreate重新啟動時,會再次啟動線程;而上個線程可能還在運行,並且可能會更新已經不存在的控件,造成錯誤。

b)關閉ProgressDialog的代碼在線程的onPostExecutez中,但是上個線程如果已經殺死,無法關閉之前ProgressDialog。

c)谷歌的官方不建議使用ProgressDialog,這裡我們會使用官方推薦的DialogFragment來創建我的加載框,如果你不了解:請看 Android 官方推薦 : DialogFragment 創建對話框。這樣,其實給我們帶來一個很大的問題,DialogFragment說白了是Fragment,和當前的Activity的生命周期會發生綁定,我們旋轉屏幕會造成Activity的銷毀,當然也會對DialogFragment造成影響。

下面我將使用幾個例子,分別使用上面的3種方式,和如何最好的解決上述的問題。

3、使用onSaveInstanceState()和onRestoreInstanceState()進行數據保存與恢復

代碼:

 

package com.example.zhy_handle_runtime_change;

import java.util.ArrayList;
import java.util.Arrays;

import android.app.DialogFragment;
import android.app.ListActivity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
/**
 * 不考慮加載時,進行旋轉的情況,有意的避開這種情況,後面例子會介紹解決方案
 * @author zhy
 *
 */
public class SavedInstanceStateUsingActivity extends ListActivity
{
	private static final String TAG = MainActivity;
	private ListAdapter mAdapter;
	private ArrayList mDatas;
	private DialogFragment mLoadingDialog;
	private LoadDataAsyncTask mLoadDataAsyncTask;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		Log.e(TAG, onCreate);
		initData(savedInstanceState);
	}

	/**
	 * 初始化數據
	 */
	private void initData(Bundle savedInstanceState)
	{
		if (savedInstanceState != null)
			mDatas = savedInstanceState.getStringArrayList(mDatas);

		if (mDatas == null)
		{
			mLoadingDialog = new LoadingDialog();
			mLoadingDialog.show(getFragmentManager(), LoadingDialog);
			mLoadDataAsyncTask = new LoadDataAsyncTask();
			mLoadDataAsyncTask.execute();
			
		} else
		{
			initAdapter();
		}

	}

	/**
	 * 初始化適配器
	 */
	private void initAdapter()
	{
		mAdapter = new ArrayAdapter(
				SavedInstanceStateUsingActivity.this,
				android.R.layout.simple_list_item_1, mDatas);
		setListAdapter(mAdapter);
	}

	@Override
	protected void onRestoreInstanceState(Bundle state)
	{
		super.onRestoreInstanceState(state);
		Log.e(TAG, onRestoreInstanceState);
	}

	@Override
	protected void onSaveInstanceState(Bundle outState)
	{
		super.onSaveInstanceState(outState);
		Log.e(TAG, onSaveInstanceState);
		outState.putSerializable(mDatas, mDatas);

	}

	/**
	 * 模擬耗時操作
	 * 
	 * @return
	 */
	private ArrayList generateTimeConsumingDatas()
	{
		try
		{
			Thread.sleep(2000);
		} catch (InterruptedException e)
		{
		}
		return new ArrayList(Arrays.asList(通過Fragment保存大量數據,
				onSaveInstanceState保存數據,
				getLastNonConfigurationInstance已經被棄用, RabbitMQ, Hadoop,
				Spark));
	}

	private class LoadDataAsyncTask extends AsyncTask
	{
		@Override
		protected Void doInBackground(Void... params)
		{
			mDatas = generateTimeConsumingDatas();
			return null;
		}

		@Override
		protected void onPostExecute(Void result)
		{
			mLoadingDialog.dismiss();
			initAdapter();
		}
	}

	@Override
	protected void onDestroy()
	{
		Log.e(TAG, onDestroy);
		super.onDestroy();
	}

}


 

界面為一個ListView,onCreate中啟動一個異步任務去加載數據,這裡使用Thread.sleep模擬了一個耗時操作;當用戶旋轉屏幕發生重新啟動時,會onSaveInstanceState中進行數據的存儲,在onCreate中對數據進行恢復,免去了不必要的再加載一遍。

運行結果:

當正常加載數據完成之後,用戶不斷進行旋轉屏幕,log會不斷打出:onSaveInstanceState->onDestroy->onCreate->onRestoreInstanceState,驗證我們的確是重新啟動了,但是我們沒有再次去進行數據加載。

如果在加載的時候,進行旋轉,則會發生錯誤,異常退出(退出原因:dialog.dismiss()時發生NullPointException,因為與當前對話框綁定的FragmentManager為null,又有興趣的可以去Debug,這個不是關鍵)。

效果圖:

\

 

 

4、使用Fragment來保存對象,用於恢復數據

如果重新啟動你的Activity需要恢復大量的數據,重新建立網絡連接,或者執行其他的密集型操作,這樣因為配置發生變化而完全重新啟動可能會是一個慢的用戶體驗。並且,使用系統提供的onSaveIntanceState()的回調中,使用Bundle來完全恢復你Activity的狀態是可能是不現實的(Bundle不是設計用來攜帶大量數據的(例如bitmap),並且Bundle中的數據必須能夠被序列化和反序列化),這樣會消耗大量的內存和導致配置變化緩慢。在這樣的情況下,當你的Activity因為配置發生改變而重啟,你可以通過保持一個Fragment來緩解重新啟動帶來的負擔。這個Fragment可以包含你想要保持的有狀態的對象的引用。

當Android系統因為配置變化關閉你的Activity的時候,你的Activity中被標識保持的fragments不會被銷毀。你可以在你的Activity中添加這樣的fragements來保存有狀態的對象。

在運行時配置發生變化時,在Fragment中保存有狀態的對象
a) 繼承Fragment,聲明引用指向你的有狀態的對象
b) 當Fragment創建時調用setRetainInstance(boolean)
c) 把Fragment實例添加到Activity中
d) 當Activity重新啟動後,使用FragmentManager對Fragment進行恢復
代碼:

首先是Fragment:

 

package com.example.zhy_handle_runtime_change;

import android.app.Fragment;
import android.graphics.Bitmap;
import android.os.Bundle;

public class RetainedFragment extends Fragment
{
	// data object we want to retain
	private Bitmap data;
	// this method is only called once for this fragment
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		// retain this fragment
		setRetainInstance(true);
	}

	public void setData(Bitmap data)
	{
		this.data = data;
	}

	public Bitmap getData()
	{
		return data;
	}
}

比較簡單,只需要聲明需要保存的數據對象,然後提供getter和setter,注意,一定要在onCreate調用setRetainInstance(true);

 

然後是:FragmentRetainDataActivity

 

package com.example.zhy_handle_runtime_change;

import android.app.Activity;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;

import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.toolbox.ImageRequest;
import com.android.volley.toolbox.Volley;

public class FragmentRetainDataActivity extends Activity
{

	private static final String TAG = FragmentRetainDataActivity;
	private RetainedFragment dataFragment;
	private DialogFragment mLoadingDialog;
	private ImageView mImageView;
	private Bitmap mBitmap;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		Log.e(TAG, onCreate);

		// find the retained fragment on activity restarts
		FragmentManager fm = getFragmentManager();
		dataFragment = (RetainedFragment) fm.findFragmentByTag(data);
		// create the fragment and data the first time
		if (dataFragment == null)
		{
			// add the fragment
			dataFragment = new RetainedFragment();
			fm.beginTransaction().add(dataFragment, data).commit();
		}
		mBitmap = collectMyLoadedData();
		initData();

		// the data is available in dataFragment.getData()
	}

	/**
	 * 初始化數據
	 */
	private void initData()
	{
		mImageView = (ImageView) findViewById(R.id.id_imageView);
		if (mBitmap == null)
		{
			mLoadingDialog = new LoadingDialog();
			mLoadingDialog.show(getFragmentManager(), LOADING_DIALOG);
			RequestQueue newRequestQueue = Volley
					.newRequestQueue(FragmentRetainDataActivity.this);
			ImageRequest imageRequest = new ImageRequest(
					http://img.my.csdn.net/uploads/201407/18/1405652589_5125.jpg,
					new Response.Listener()
					{
						@Override
						public void onResponse(Bitmap response)
						{
							mBitmap = response;
							mImageView.setImageBitmap(mBitmap);
							// load the data from the web
							dataFragment.setData(mBitmap);
							mLoadingDialog.dismiss();
						}
					}, 0, 0, Config.RGB_565, null);
			newRequestQueue.add(imageRequest);
		} else
		{
			mImageView.setImageBitmap(mBitmap);
		}

	}

	@Override
	public void onDestroy()
	{
		Log.e(TAG, onDestroy);
		super.onDestroy();
		// store the data in the fragment
		dataFragment.setData(mBitmap);
	}

	private Bitmap collectMyLoadedData()
	{
		return dataFragment.getData();
	}

}


 

這裡在onCreate總使用了Volley去加載 了一張美女照片,然後在onDestroy中對Bitmap進行存儲,在onCreate添加一個或者恢復一個Fragment的引用,然後對Bitmap進行讀取和設置。這種方式適用於比較大的數據的存儲與恢復。

注:這裡也沒有考慮加載時旋轉屏幕,問題與上面的一致。

效果圖:

\

5、配置configChanges,自己對屏幕旋轉的變化進行處理

在menifest中進行屬性設置:

 

    
        
低版本的API只需要加入orientation,而高版本的則需要加入screenSize。

 

ConfigChangesTestActivity

 

package com.example.zhy_handle_runtime_change;

import java.util.ArrayList;
import java.util.Arrays;

import android.app.DialogFragment;
import android.app.ListActivity;
import android.content.res.Configuration;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;
import android.widget.Toast;

/**
 * @author zhy
 * 
 */
public class ConfigChangesTestActivity extends ListActivity
{
	private static final String TAG = MainActivity;
	private ListAdapter mAdapter;
	private ArrayList mDatas;
	private DialogFragment mLoadingDialog;
	private LoadDataAsyncTask mLoadDataAsyncTask;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		Log.e(TAG, onCreate);
		initData(savedInstanceState);
	}

	/**
	 * 初始化數據
	 */
	private void initData(Bundle savedInstanceState)
	{

		mLoadingDialog = new LoadingDialog();
		mLoadingDialog.show(getFragmentManager(), LoadingDialog);
		mLoadDataAsyncTask = new LoadDataAsyncTask();
		mLoadDataAsyncTask.execute();

	}

	/**
	 * 初始化適配器
	 */
	private void initAdapter()
	{
		mAdapter = new ArrayAdapter(ConfigChangesTestActivity.this,
				android.R.layout.simple_list_item_1, mDatas);
		setListAdapter(mAdapter);
	}

	/**
	 * 模擬耗時操作
	 * 
	 * @return
	 */
	private ArrayList generateTimeConsumingDatas()
	{
		try
		{
			Thread.sleep(2000);
		} catch (InterruptedException e)
		{
		}
		return new ArrayList(Arrays.asList(通過Fragment保存大量數據,
				onSaveInstanceState保存數據,
				getLastNonConfigurationInstance已經被棄用, RabbitMQ, Hadoop,
				Spark));
	}

	/**
	 * 當配置發生變化時,不會重新啟動Activity。但是會回調此方法,用戶自行進行對屏幕旋轉後進行處理
	 */
	@Override
	public void onConfigurationChanged(Configuration newConfig)
	{
		super.onConfigurationChanged(newConfig);

		if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
		{
			Toast.makeText(this, landscape, Toast.LENGTH_SHORT).show();
		} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT)
		{
			Toast.makeText(this, portrait, Toast.LENGTH_SHORT).show();
		}

	}

	private class LoadDataAsyncTask extends AsyncTask
	{
		@Override
		protected Void doInBackground(Void... params)
		{
			mDatas = generateTimeConsumingDatas();
			return null;
		}

		@Override
		protected void onPostExecute(Void result)
		{
			mLoadingDialog.dismiss();
			initAdapter();
		}
	}

	@Override
	protected void onDestroy()
	{
		Log.e(TAG, onDestroy);
		super.onDestroy();
	}

}

對第一種方式的代碼進行了修改,去掉了保存與恢復的代碼,重寫了onConfigurationChanged;此時,無論用戶何時旋轉屏幕都不會重新啟動Activity,並且onConfigurationChanged中的代碼可以得到調用。從效果圖可以看到,無論如何旋轉不會重啟Activity.

效果圖:

\

6、旋轉屏幕的最佳實踐

下面要開始今天的難點了,就是處理文章開始時所說的,當異步任務在執行時,進行旋轉,如果解決上面的問題。

首先說一下探索過程:

起初,我認為此時旋轉無非是再啟動一次線程,並不會造成異常,我只要即使的在onDestroy裡面關閉上一個異步任務就可以了。事實上,如果我關閉了,上一次的對話框會一直存在;如果我不關閉,但是activity是一定會被銷毀的,對話框的dismiss也會出異常。真心很蛋疼,並且即使對話框關閉了,任務關閉了;用戶旋轉還是會造成重新創建任務,從頭開始加載數據。

下面我們希望有一種解決方案:在加載數據時旋轉屏幕,不會對加載任務進行中斷,且對用戶而言,等待框在加載完成之前都正常顯示:

當然我們還使用Fragment進行數據保存,畢竟這是官方推薦的:

OtherRetainedFragment

 

package com.example.zhy_handle_runtime_change;

import android.app.Fragment;
import android.os.Bundle;

/**
 * 保存對象的Fragment
 * 
 * @author zhy
 * 
 */
public class OtherRetainedFragment extends Fragment
{

	// data object we want to retain
	// 保存一個異步的任務
	private MyAsyncTask data;

	// this method is only called once for this fragment
	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		// retain this fragment
		setRetainInstance(true);
	}

	public void setData(MyAsyncTask data)
	{
		this.data = data;
	}

	public MyAsyncTask getData()
	{
		return data;
	}

	
}

和上面的差別不大,唯一不同的就是它要保存的對象編程一個異步的任務了,相信看到這,已經知道經常上述問題的一個核心了,保存一個異步任務,在重啟時,繼續這個任務。

 

 

package com.example.zhy_handle_runtime_change;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import android.os.AsyncTask;

public class MyAsyncTask extends AsyncTask
{
	private FixProblemsActivity activity;
	/**
	 * 是否完成
	 */
	private boolean isCompleted;
	/**
	 * 進度框
	 */
	private LoadingDialog mLoadingDialog;
	private List items;

	public MyAsyncTask(FixProblemsActivity activity)
	{
		this.activity = activity;
	}

	/**
	 * 開始時,顯示加載框
	 */
	@Override
	protected void onPreExecute()
	{
		mLoadingDialog = new LoadingDialog();
		mLoadingDialog.show(activity.getFragmentManager(), LOADING);
	}

	/**
	 * 加載數據
	 */
	@Override
	protected Void doInBackground(Void... params)
	{
		items = loadingData();
		return null;
	}

	/**
	 * 加載完成回調當前的Activity
	 */
	@Override
	protected void onPostExecute(Void unused)
	{
		isCompleted = true;
		notifyActivityTaskCompleted();
		if (mLoadingDialog != null)
			mLoadingDialog.dismiss();
	}

	public List getItems()
	{
		return items;
	}

	private List loadingData()
	{
		try
		{
			Thread.sleep(5000);
		} catch (InterruptedException e)
		{
		}
		return new ArrayList(Arrays.asList(通過Fragment保存大量數據,
				onSaveInstanceState保存數據,
				getLastNonConfigurationInstance已經被棄用, RabbitMQ, Hadoop,
				Spark));
	}

	/**
	 * 設置Activity,因為Activity會一直變化
	 * 
	 * @param activity
	 */
	public void setActivity(FixProblemsActivity activity)
	{
		// 如果上一個Activity銷毀,將與上一個Activity綁定的DialogFragment銷毀
		if (activity == null)
		{
			mLoadingDialog.dismiss();
		}
		// 設置為當前的Activity
		this.activity = activity;
		// 開啟一個與當前Activity綁定的等待框
		if (activity != null && !isCompleted)
		{
			mLoadingDialog = new LoadingDialog();
			mLoadingDialog.show(activity.getFragmentManager(), LOADING);
		}
		// 如果完成,通知Activity
		if (isCompleted)
		{
			notifyActivityTaskCompleted();
		}
	}

	private void notifyActivityTaskCompleted()
	{
		if (null != activity)
		{
			activity.onTaskCompleted();
		}
	}

}

異步任務中,管理一個對話框,當開始下載前,進度框顯示,下載結束進度框消失,並為Activity提供回調。當然了,運行過程中Activity不斷的重啟,我們也提供了setActivity方法,onDestory時,會setActivity(null)防止內存洩漏,同時我們也會關閉與其綁定的加載框;當onCreate傳入新的Activity時,我們會在再次打開一個加載框,當然了因為屏幕的旋轉並不影響加載的數據,所有後台的數據一直繼續在加載。是不是很完美~~

 

主Activity:

 

package com.example.zhy_handle_runtime_change;

import java.util.List;

import android.app.FragmentManager;
import android.app.ListActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListAdapter;

public class FixProblemsActivity extends ListActivity
{
	private static final String TAG = MainActivity;
	private ListAdapter mAdapter;
	private List mDatas;
	private OtherRetainedFragment dataFragment;
	private MyAsyncTask mMyTask;

	@Override
	public void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		Log.e(TAG, onCreate);

		// find the retained fragment on activity restarts
		FragmentManager fm = getFragmentManager();
		dataFragment = (OtherRetainedFragment) fm.findFragmentByTag(data);

		// create the fragment and data the first time
		if (dataFragment == null)
		{
			// add the fragment
			dataFragment = new OtherRetainedFragment();
			fm.beginTransaction().add(dataFragment, data).commit();
		}
		mMyTask = dataFragment.getData();
		if (mMyTask != null)
		{
			mMyTask.setActivity(this);
		} else
		{
			mMyTask = new MyAsyncTask(this);
			dataFragment.setData(mMyTask);
			mMyTask.execute();
		}
		// the data is available in dataFragment.getData()
	}


	@Override
	protected void onRestoreInstanceState(Bundle state)
	{
		super.onRestoreInstanceState(state);
		Log.e(TAG, onRestoreInstanceState);
	}


	@Override
	protected void onSaveInstanceState(Bundle outState)
	{
		mMyTask.setActivity(null);
		super.onSaveInstanceState(outState);
		Log.e(TAG, onSaveInstanceState);
	}

	@Override
	protected void onDestroy()
	{
		Log.e(TAG, onDestroy);
		super.onDestroy();

	}
	/**
	 * 回調
	 */
	public void onTaskCompleted()
	{
		mDatas = mMyTask.getItems();
		mAdapter = new ArrayAdapter(FixProblemsActivity.this,
				android.R.layout.simple_list_item_1, mDatas);
		setListAdapter(mAdapter);
	}

}

在onCreate中,如果沒有開啟任務(第一次進入),開啟任務;如果已經開啟了,調用setActivity(this);

 

在onSaveInstanceState把當前任務加入Fragment

我設置了等待5秒,足夠旋轉三四個來回了~~~~可以看到雖然在不斷的重啟,但是絲毫不影響加載數據任務的運行和加載框的顯示~~~~

效果圖:

\

可以看到我在加載的時候就三心病狂的旋轉屏幕~~但是絲毫不影響顯示效果與任務的加載~~

 

最後,說明一下,其實不僅是屏幕旋轉需要保存數據,當用戶在使用你的app時,忽然接到一個來電,長時間沒有回到你的app界面也會造成Activity的銷毀與重建,所以一個行為良好的App,是有必要擁有恢復數據的能力的~~。

 

查閱資料時的一些參考文檔:

http://developer.android.com/guide/topics/resources/runtime-changes.html

 

http://blog.doityourselfandroid.com/2010/11/14/handling-progress-dialogs-and-screen-orientation-changes/


有任何問題,歡迎留言

 

源碼點擊下載


 

 

 

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved