Android 屏幕旋转 处理的最佳方案

1、概述(鸿洋)

众所周知,Activity在不明确指定屏幕方向和configChanges时,当用户旋转屏幕会重新启动。当然了,应对这种情况,Android给出了几种方案:

a、如果是少量数据,可以通过onSaveInstanceState()和onRestoreInstanceState()进行保存与恢复。

Android会在销毁你的Activity之前调用onSaveInstanceState()方法,于是,你可以在此方法中存储关于应用状态的数据。然后你可以在onCreate()或onRestoreInstanceState()方法中恢复。

b、如果是大量数据,使用Fragment保持需要恢复的对象。

c、清单文件设置config change属性

2、难点

假设当前Activity在onCreate中启动一个异步线程去夹在数据,当然为了给用户一个很好的体验,会有一个ProgressDialog,当数据加载完成,ProgressDialog消失,设置数据。

这里,如果在异步数据完成加载之后,旋转屏幕,使用上述a、b两种方法都不会很难,无非是保存数据和恢复数据。

但是,如果正在线程加载的时候,进行旋转,会存在以下问题:

a)此时数据没有完成加载,onCreate重新启动时,会再次启动线程;而上个线程可能还在运行,并且可能会更新已经不存在的控件,造成错误。

b)关闭ProgressDialog的代码在线程的onPostExecutez中,但是上个线程如果已经杀死,无法关闭之前ProgressDialog。

c)谷歌的官方不建议使用ProgressDialog,这里我们会使用官方推荐的DialogFragment来创建我的加载框,如果你不了解:请看 Android 官方推荐 : DialogFragment 创建对话框。这样,其实给我们带来一个很大的问题,DialogFragment说白了是Fragment,和当前的Activity的生命周期会发生绑定,我们旋转屏幕会造成Activity的销毁,当然也会对DialogFragment造成影响。

下面我将使用几个例子,分别使用上面的3种方式,和如何最好的解决上述的问题。

a、使用onSaveInstanceState()和onRestoreInstanceState()进行数据保存与恢复

代码:

public class saveInstanceActivity extends ListActivity {
    //获取到的数据源

    private ArrayList<String> data;
    private LoadingDialog loadingDialog;
    private ArrayAdapter<String> mAdapter;

    /****
     * 不考虑网络加载时,进行旋转的情况,有意的避开这种情况屏幕旋转导致生命周期发生变化的可以使用onsaveInstancestate()和onRestoreSaveInstance ()方法保存和读取数据
     *
     * 生命周期:onsaveInstancestate---ondestroy()---oncreat()---onRestoreSaveInstance ()
     *
     * 如果在加载的时候,进行旋转,则会发生错误,异常退出(退出原因:dialog.dismiss()时发生NullPointException,因为与当前对话框绑定的FragmentManager为null,
     * @param savedInstanceState
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_save_instance);
        initData(savedInstanceState);
    }

    private void initData(Bundle savedInstanceState) {
        //有保存的数据;
        if (savedInstanceState!=null){


            //请求数据操作表示里面有数据直接读取

            data = savedInstanceState.getStringArrayList("mDatas");


        }

        if (data==null){


            //第一次进来先加载数据

            loadingDialog = new LoadingDialog();


            loadingDialog.show(getFragmentManager(),"加载中");
            deAsyncTask myAsyncTask = new deAsyncTask();
            myAsyncTask.execute();


        }else{
           initAdapter();

        }
    }

    /***
     * 销毁前进行数据保存
     * @param outState
     */
    @Override
    protected void onSaveInstanceState(Bundle outState) {

        outState.putSerializable("mDatas",data);
    }

    /***
     * 异步操作
     */
    class deAsyncTask extends AsyncTask<Void,Void,Void>{

       //子线程执行的操作
        @Override
        protected Void doInBackground(Void... parmas) {
            //为代码仿照加载数据耗时
            data = requestData();
            return null;
        }



        //执行完回到主线成
        @Override
        protected void onPostExecute(Void aVoid) {
            loadingDialog.dismiss();
            initAdapter();
        }
    }

    private void initAdapter() {

        mAdapter = new ArrayAdapter<>(

                this,
                android.R.layout.simple_list_item_1, data);
        setListAdapter(mAdapter);
    }

    private ArrayList<String> requestData() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return new ArrayList<String>(Arrays.asList("通过Fragment保存大量数据",
                "onSaveInstanceState保存数据",
                "getLastNonConfigurationInstance已经被弃用", "RabbitMQ", "Hadoop",
                "Spark"));

    }


}


界面为一个ListView,onCreate中启动一个异步任务去加载数据,这里使用Thread.sleep模拟了一个耗时操作;当用户旋转屏幕发生重新启动时,会onSaveInstanceState中进行数据的存储,在onCreate中对数据进行恢复,免去了不必要的再加载一遍。

运行结果:

当正常加载数据完成之后,用户不断进行旋转屏幕,log会不断打出:onSaveInstanceState->onDestroy->onCreate->onRestoreInstanceState,验证我们的确是重新启动了,但是我们没有再次去进行数据加载。

如果在加载的时候,进行旋转,则会发生错误,异常退出(退出原因:dialog.dismiss()时发生NullPointException,因为与当前对话框绑定的FragmentManager为null,又有兴趣的可以去Debug,这个不是关键)。

 

b、使用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进行恢复
代码:

public class retainFragment extends Fragment {
    public Bitmap getBtmap() {
        return btmap;
    }

    public void setBtmap(Bitmap btmap) {
        this.btmap = btmap;
    }

    //要保存的实例引用
    private Bitmap  btmap;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //保存当前的这个fragment
        setRetainInstance(true);
    }





}

比较简单,只需要声明需要保存的数据对象,然后提供getter和setter,注意,一定要在onCreate调用setRetainInstance(true);

 

然后是:FragmentRetainDataActivity

 

public class FragmentRetainDataActivity extends Activity
{

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

	@SuppressLint("LongLogTag")
	@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
		//找到保存的那个fragment实例,在再一次打开的时候

		FragmentManager fm = getFragmentManager();
		dataFragment= (retainFragment) fm.findFragmentByTag("data");
		// 第一次进来创建fragment
		if (dataFragment == null)
		{
			// add the fragment
			dataFragment = new retainFragment();
			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(
					"https://c-ssl.duitang.com/uploads/item/201410/11/20141011040700_3iXsj.thumb.1200_0.jpeg",
					new Response.Listener<Bitmap>()
					{
						@Override
						public void onResponse(Bitmap response)
						{
							mBitmap = response;
							mImageView.setImageBitmap(mBitmap);
							// load the data from the web
							dataFragment.setBtmap(mBitmap);
							mLoadingDialog.dismiss();
						}
					}, 0, 0, Config.RGB_565, null);
			newRequestQueue.add(imageRequest);
		} else
		{
			mImageView.setImageBitmap(mBitmap);
		}

	}

	/***
	 * 界面销毁时将数据保存到fragment中
	 */
	@SuppressLint("LongLogTag")
	@Override
	public void onDestroy()
	{
		Log.e(TAG, "onDestroy");
		super.onDestroy();
		// store the data in the fragment
		dataFragment.setBtmap(mBitmap);
	}

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

}

这里在onCreate总使用了Volley去加载 了一张美女照片,然后在onDestroy中对Bitmap进行存储,在onCreate添加一个或者恢复一个Fragment的引用,然后对Bitmap进行读取和设置。这种方式适用于比较大的数据的存储与恢复。

c、配置configChanges,自己对屏幕旋转的变化进行处理

在menifest中进行属性设置:

    <activity
            android:name=".ConfigChangesTestActivity"
            android:configChanges="screenSize|orientation" >
        </activity>


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

 

旋转屏幕的最佳实践

下面要开始今天的难点了,就是处理文章开始时所说的,当异步任务在执行时,进行旋转,如果解决上面的问题。

首先说一下探索过程:

起初,我认为此时旋转无非是再启动一次线程,并不会造成异常,我只要即使的在onDestroy里面关闭上一个异步任务就可以了。事实上,如果我关闭了,上一次的对话框会一直存在;如果我不关闭,但是activity是一定会被销毁的,对话框的dismiss也会出异常。真心很蛋疼,并且即使对话框关闭了,任务关闭了;用户旋转还是会造成重新创建任务,从头开始加载数据。

下面我们希望有一种解决方案:在加载数据时旋转屏幕,不会对加载任务进行中断,且对用户而言,等待框在加载完成之前都正常显示:

当然我们还使用Fragment进行数据保存,毕竟这是官方推荐的:

OtherRetainedFragment
 

/**
 * 保存一个异布任务对象的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;
	}

	
}

和上面的差别不大,唯一不同的就是它要保存的对象编程一个异步的任务了,相信看到这,已经知道经常上述问题的一个核心了,保存一个异步任务,在重启时,继续这个任务。

 

 


public class MyAsyncTask extends AsyncTask<Void, Void, Void>
{
	private FixProblemsActivity activity;
	/**
	 * 是否完成
	 */
	private boolean isCompleted;
	/**
	 * 进度框
	 */
	private LoadingDialog mLoadingDialog;
	private List<String> 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<String> getItems()
	{
		return items;
	}

	private List<String> loadingData()
	{
		try
		{
			Thread.sleep(5000);
		} catch (InterruptedException e)
		{
		}
		return new ArrayList<String>(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:

 

public class FixProblemsActivity extends ListActivity
{
	private static final String TAG = "MainActivity";
	private ListAdapter mAdapter;
	private List<String> 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<String>(FixProblemsActivity.this,
				android.R.layout.simple_list_item_1, mDatas);
		setListAdapter(mAdapter);
	}

}

在onCreate中,如果没有开启任务(第一次进入),开启任务;如果已经开启了,调用setActivity(this);

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值