安卓应用程序在屏幕方向变化时丢失数据

10

我有一个应用程序,是从教程中复制的,它使用 MediaStore.ACTION_IMAGE_CAPTURE 来捕获图像。但当我在手机上运行该应用程序时,出现了一些奇怪的问题。

摄像头应用程序在操作过程中会自动翻转屏幕方向几次,即使我没有移动手机。 它在暂时切换到横屏模式,然后返回到教程应用程序。 因此,在控制权返回到教程应用程序后,教程应用程序会重新切换回竖屏模式,并且图像会丢失。 我尝试将相机活动的方向设置为横向,这样图像就不会丢失。

但是应用程序的布局是为竖屏模式设计的。 或者,如果我在拍摄照片时将相机保持在横向方向,则可以在我的应用程序重新获得焦点后旋转手机,而不会丢失图像。

我在网上进行了一些搜索。 Stackoverflow 上的某位用户提到屏幕方向的更改导致了对 onCreate 的额外调用。“调用 onCreate() 的原因是因为当您在纵向方向上调用相机活动时,它将更改方向并销毁之前的活动。” 我在调试模式下运行应用程序,并设置了在 onCreateonActivityResult 方法中断点。 的确如此,当我在纵向方向上拍照时,onCreate 会被调用。 调用顺序是 onCreate、onActivityResult、onCreate。 如果我在横向模式下拍摄照片(无论如何我的相机应用程序最终都会进入横向模式),则不会调用 onCreate。 现在我已经知道了问题出在哪里,那么怎么解决呢? 下面是当前应用程序的样子:

package com.example.testapp;

import java.io.IOException;
import java.io.InputStream;

import android.app.Activity;
import android.app.WallpaperManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;

public class CameraActivity extends Activity implements View.OnClickListener {

    ImageButton ib;
    Button b;
    ImageView iv;
    Intent i;
    final static int cameraData = 0;
    Bitmap bmp;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.photo_activity);
        initialize();
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        // TODO Auto-generated method stub
        super.onConfigurationChanged(newConfig);
        setContentView(R.layout.photo_activity);
        initialize();
    }

    private void initialize() {
        iv = (ImageView)findViewById(R.id.imageViewReturnedPicture);
        ib = (ImageButton)findViewById(R.id.imageButtonTakePicture);
        b = (Button)findViewById(R.id.buttonSetWallpaper);
        b.setOnClickListener(this);
        ib.setOnClickListener(this);
    }

    @Override
    public void onClick(View arg0) {
        switch (arg0.getId()) {

        case R.id.buttonSetWallpaper:
            try {
                WallpaperManager wm = WallpaperManager.getInstance(getApplicationContext());
                wm.setBitmap(bmp);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            break;

        case R.id.imageButtonTakePicture:
            i = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
            startActivityForResult(i, cameraData);
            break;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // TODO Auto-generated method stub
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            Bundle extras = data.getExtras();
            bmp = (Bitmap)extras.get("data");
            iv.setImageBitmap(bmp);
        }
    }
}

以下是这个活动清单中的代码:

                android:name="com.example.testapp.CameraActivity"
                android:label="相机活动"
                android:configChanges="方向"
                android:screenOrientation="竖屏" 

我进行了不少搜索,但大部分内容缺乏具体示例。 我需要知道代码长什么样,而不仅仅是要使用哪个功能。

我的手机是LG Motion。 有其他人遇到过这个问题吗? 该怎么解决?

7个回答

18
在清单文件中,每个活动都使用了configChanges属性:
 <activity
        android:name=".MainActivity"
        android:configChanges="screenLayout|orientation|screenSize">

我希望这能帮助到你。


1
请注意,orientation 标志是答案中最重要的部分,它拯救了我的一天... - JamesC

8

您需要重写 onRetainNonConfigurationInstance 方法,并使用 getLastNonConfigurationInstance 方法来保存/恢复位图。

示例代码如下:

// during onCreate(Bundle), after initialise()
bmp = getLastNonConfigurationInstance();
if(bmp!=null){ iv.setImageBitmap(bmp); }
else { /* the image was not taken yet */ }

然后在您的活动中覆盖:

@Override
public Object onRetainNonConfigurationInstance (){
     return bmp;
}

这将在旋转期间“保存”位图。

编辑:

使用建议的onSaveInstanceState示例,可以正常工作,但不建议使用,因为它会使用大量内存并且非常慢,但您很快就需要用于其他情况:

public class SomethingSomething extends Activity{

    String name="";
    int page=0;

    // This is called when the activity is been created
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

            // if you saved something on outState you can recover them here
            if(savedInstanceState!=null){
                name = savedInstanceState.getString("name");
                page = savedInstanceState.getInt("page");
            }
    }

    // This is called before the activity is destroyed
@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

            outState.putString("name", name);
            outState.putInt("page", page);
    }
}

正如你所看到的,这个解决方案不适合你的情况,因为用于此解决方案的Android Bundle是Android特殊类型的序列化,可以处理基元、字符串和实现Parcelable的类(这些类只是封装它们的基元)。虽然位图确实实现了Parcelable,但将每个字节复制到bundle中需要花费很长时间,并且会使位图的内存消耗倍增。

现在让我们来看一下使用setRetainInstance的解决方案(略微从示例中复制,您可以在\sdk\extras\android\support\samples\Support4Demos\src\com\example\android\supportv4\app\FragmentRetainInstanceSupport.Java找到它)。

确保还要检查示例,因为它显示了一些其他花哨的技巧。

// This fragment will be managed by the framework but we won't built a UI for it.
public class FragRetained extends Fragment{
   public static final String TAG = "TAG.FragRetained";
   private Bitmap bitmap;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Tell the framework to try to keep this fragment around
        // during a configuration change.
        setRetainInstance(true);
    }
    public Bitmap getBitmap() { return bitmap; }
    public void setBitmap(Bitmap bmp) { bitmap = bmp; }
}

public class MyActivity extends Activity{

    private FragRetained myFragRetained;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
            // set the content view
            img = (ImageView)findViewById(R.id.byImgV);


            myFragRetained = getFragmentManger.findFragmentByTag(FragRetained.TAG);
            if(myFragRetained == null){
                myFragRetained = new FragRetained ();
            }else{
               Bitmap b = myFragRetained.getBitmap();
               if(b==null){
                  // the user still did not choose the photo
               }else{
                  img.setImageBitmap(b);
               }
            }
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            Bundle extras = data.getExtras();
            bmp = (Bitmap)extras.get("data");
            iv.setImageBitmap(bmp);
            myFragRetained.setBitmap(bmp);
        }
    }

}

请确保从您的清单中删除android:configChanges="orientation"这一行,因为它可能会带来更多的问题而不是好处。以下是有关此问题的一小段引用:

注意:应避免使用此属性,并仅在最后一种情况下使用。请阅读有关如何正确处理由于配置更改而导致重新启动的信息,请参阅处理运行时更改。


1
显然,onRetainNonConfigurationInstance已经被弃用。文档建议使用Fragment API的setRetainInstance(boolean)代替。有人知道如何使用吗? - Roxann Higuera
1
确实如此,但是现在你还在学习阶段,没有什么可以阻止你使用它。要使用片段版本,您需要一个无UI的片段,在onCreate()期间请求保留其实例,并且可以在其中创建setBitmap()和getBitmap()方法。因此,在获取图像后,您可以初始化片段并设置位图。旋转后(在activity onCreate()期间),您可以使用getFragmentManager().findFragmentByTag(String)获取片段,并在其上调用getBitmap()。但我认为对于刚开始学习该平台的人来说,这太多了。 - Budius
1
好吧,也许我是新手Android和Java开发者,但我自1980年以来就一直在编程。如果有一个好的例子,我很擅长理解所需的内容。但当没有例子时,文档对我来说看起来像胡言乱语。这是我在尝试弄明白它的过程中遇到的问题。人们说要使用onSaveInstanceState等功能,但我没有找到任何关于如何使用onSaveInstanceState的示例。Android文档严重缺乏示例。 - Roxann Higuera
1
我并不是有意居高临下,但你只有1个声望值,而且我能看出你正在开始学习Android,所以我试图简化一下。但无论如何,我会编辑我的答案,提供如何使用onSaveInstanceState的两个示例(根据我的评论,这在你的情况下将不起作用,但在不久的将来可能会有用),以及setRetainInstance()的示例。请再次检查一下。 - Budius
1
这怎么没有得到+1呢?虽然我不是安卓程序员,但是你做得很好(@Roxann)。 - halfer
显示剩余2条评论

3
我星期一去了一个移动开发小组的会议,得到了一个提示。一位为一家大公司编写各种平台应用程序的程序员建议将位图附加到应用程序对象上。当我考虑如何做到这一点时,我终于想出了一个解决方案(在帖子末尾有一个新问题),该解决方案基于以下博客文章中的建议: http://android-er.blogspot.com/2011/10/share-bitmap-between-activities-as.html 基本上,这需要在任何应用程序活动都可以访问的位置设置位图作为公共资源。我创建了一个新类,如下所示:
package com.example.testapp;
 
import android.graphics.Bitmap;
 
public class CommonResources {
        public static Bitmap cameraBmp = null;
}

然后我将CameraActivity.java中所有对bmp的引用都改为CommonResources.cameraBmp。

在初始化期间,如果CommonResources.cameraBmp不为null,则使用它来设置ImageView中的位图。

现在onCreate和initialize看起来是这样的:

@Override
protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
              super.onCreate(savedInstanceState);
        setContentView(R.layout.photo_activity);
        initialize();
}

private void initialize() {
        iv = (ImageView)findViewById(R.id.imageViewReturnedPicture);
        ib = (ImageButton)findViewById(R.id.imageButtonTakePicture);
        b = (Button)findViewById(R.id.buttonSetWallpaper);
        if (CommonResources.cameraBmp != null) {
                   iv.setImageBitmap(CommonResources.cameraBmp);
           }
        b.setOnClickListener(this);
        ib.setOnClickListener(this);
}

然后,为了进行一些清理工作,我添加了以下onPause:

@Override
protected void onPause() {
        // TODO Auto-generated method stub
              super.onPause();
        if (isFinishing()) {
                   // Save the bitmap.
                        CommonResources.cameraBmp = null;
           }
}

我现在的一个大问题是:我是否正确处理了内存,还是创建了垃圾回收问题?


1

在网上搜索了相当长一段时间后,我查看了一些代码并得出了这个简单的解决方案,希望它能帮助未来查看此线程的任何人!

在我的Activity类中有一个全局变量public Uri imageURI = null;,用于设置我正在使用的ImageView的图像URI;并且在onCreate()onSaveInstanceState()中使用以下代码。

@Override
protected void onCreate(Bundle savedInstanceState) 
{   
    super.onCreate(savedInstanceState); 
    setContentView(R.layout.activity_thisactivity);

    //restore the bitmap for the image
    if (savedInstanceState!=null)
    {
        imageURI=savedInstanceState.getParcelable("imageURI");
        ImageView photo=(ImageView)findViewById(R.id.image);
        photo.setImageURI(imageURI);
    }
}

@Override
public void onSaveInstanceState(Bundle savedInstanceState)
{
    super.onSaveInstanceState(savedInstanceState);
    savedInstanceState.putParcelable("imageURI", imageURI);
}

我希望有人能从中受益-如果您需要更多细节,请询问。

1
你应该通过保存实例状态来处理方向变化。如果你填写onSaveInstanceState方法,你可以在onCreate期间将保存的数据从bundle中取回。这对于视图已经自动完成,但是其他数据需要你自己保存。任何原始类型、可包裹类型或可序列化对象都可以通过这种方式保存,包括位图。
你不仅应该在配置更改时这样做,还应该在低内存条件下保持状态。

onSaveInstanceState 无法保存位图。 - Budius
@Budius 是的,它可以 - http://it-ride.blogspot.com/2010/08/save-image-in-instance-state-android.html - Charles Munger
1
尽管您的链接无法使用,但在我的评论之后,我注意到我是错误的,对此我深表歉意,并且很乐意取消对您的负评,但除非您编辑答案,否则我无法这样做。但我坚持认为,尽管位图可被分解,但在SavedInstanceState期间使用它并不明智,因为完成此类事务需要时间和内存,并可能使旋转变得非常缓慢。 - Budius
取决于位图的大小 - 如果它很小,可能不是问题。此外,对于旋转,进程不会被终止,并且意图中的对象实际上不应该被打包,因此它应该只保留一个引用。 - Charles Munger

1

你是否尝试将 android:launchMode="singleTop" 添加到 Android 清单文件中的活动中?这对我很有用。问题在于当方向更改时,Android 会重新启动活动并且数据会丢失,但是如果你的活动已经在运行,则不会创建新实例。

        <activity 
            android:name=".FacebookActivity" 
            android:label="@string/facebook_app_name"
            android:launchMode="singleTop"
            android:configChanges="keyboardHidden|orientation"
            >
        </activity>

谢谢。只是为了好玩,我尝试了一下,但它并没有改变我的应用程序的行为。 - Roxann Higuera

0

你可以使用onSaveInstanceState和通过onCreate获取的参数,或者在清单文件中设置configChanges来处理屏幕方向变化,并指定你希望自己处理哪些情况(例如方向|屏幕尺寸|屏幕布局)。


我不是提问者。我只是在向你解释,你建议他使用onSavedInstanceState,但是Bitmaps不能用onSavedInstanceState保存,因为它们不是Parcelable类型。 - Budius
所以他可以将其保存到内部/外部存储文件夹中。不过我仍然不理解这个问题。 - android developer
是的。我在清单文件中设置了configChanges。我还重写了onConfigurationChanged方法。主要问题是当手机旋转时,我的应用程序视图中的数据会丢失。该程序在模拟器中运行良好。因此,需要采取一些措施来显式保存视图数据。 - Roxann Higuera
但是你在onConfigurationChanged中调用了setContentView()和initialize(),请将它们移除,以便它们不会重新初始化所有内容。这样,它们应该与之前的内容相同。另外,请设置我提到的configChanges的所有标志,并删除方向,因为您希望处理横向和纵向模式。 - android developer
@Budius 但是你没有考虑另一种解决方案,也没有考虑在 onSaveInstanceState 之前,你可以将位图保存到文件中。你只是在不让我解释你可以做什么的情况下投了反对票。 - android developer
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接