Android Camera Develop: add settings to camera app

概述

继上一篇实现了一个最简单的相机APP后,本篇主要介绍实现相机的各种偏好设置,比如分辨率、闪光灯、对焦等。添加这些设置看起来很容易,但实际实现时还是有很多需要注意的。

添加设置菜单

我们使用Android推荐的PreferenceFragment作为相机偏好设置的菜单。PreferenceFragment继承自Fragment,而Fragment从简单来说可以理解成Activity里的Activity,但可以让我们不再非常关心其UI布局,因为Android已经帮我们搞定了;PreferenceFragment则可以理解为专门为设置量身定做的Fragment,我们只用向其添加设置的内容,而不用关心具体设置参数用户交互设计等繁琐的内容。

添加SettingsFragment类

就像上篇介绍的CameraPreview继承自SurfaceView实现了一个自定义的View,现在我们需要创建SettingsFragment继承自PreferenceFragment实现一个自定义的Fragment

新建SettingsFragment

Settings Fragment File List

内容如下:

import android.os.Bundle;
import android.preference.PreferenceFragment;


public class SettingsFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        addPreferencesFromResource(R.xml.preferences);
    }
}

addPreferencesFromResource(R.xml.preferences);是加载来自preferences.xml文件中的菜单条目,对于PreferenceFragment,其菜单条目都是存储在xml文件中,实例化时从xml文件加载的,别着急,我们马上创建preferences.xml文件。

添加菜单条目

对于PreferenceFragment来说,需要从xml文件加载菜单条目等内容的。在res下新建xml文件夹,在其内新建preferences.xml文件

Preferences File List

内容如下:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <ListPreference
        android:defaultValue="100"
        android:entries="@array/pref_jpegQuality"
        android:entryValues="@array/pref_jpegQuality"
        android:key="jpeg_quality"
        android:title="照片品质" />
</PreferenceScreen>

菜单的每一个条目都是一个Preference。这里的ListPreference继承自Preference,就是条目选项是列表的菜单条目。其名字是照片品质,对应的key(条目以key-value对形式存储)是jpeg_quality;其对用户可见的列表选项由entries定义,而对代码而言是entryValues,提供可供选择的value,其内容相同,都是一个叫做pref_jpegQualityarray,这个稍后解释;最后为这个条目设定一个默认值即defaultValue,设为100。

添加array值

res->values下新建arrays.xml文件

Arrays File List

内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="pref_jpegQuality">
        <item>100</item>
        <item>90</item>
        <item>80</item>
        <item>70</item>
        <item>60</item>
        <item>50</item>
    </string-array>
</resources>

这样就创建了一个名字为pref_jpegQualityarray,供上一步的jpeg_quality使用,现在回去看preferences.xml已经没有红色错误提示啦。

主窗口中加入按钮,调用菜单

刚才创建了一个非常简单的设置菜单,接下来让这个菜单通过点击主窗口中的“设置”按钮就能显示出来。

修改activity_main.xml

修改activity_main.xml,在FrameLayout后加入Button,修改后内容如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/camera_preview"
        android:layout_width="0px"
        android:layout_height="fill_parent"
        android:layout_weight="1" />

    <Button
        android:id="@+id/button_settings"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:text="设置" />
</LinearLayout>

修改MainActivity

修改MainActivity类,在onCreate()方法最后添加

Button buttonSettings = (Button) findViewById(R.id.button_settings);
buttonSettings.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        getFragmentManager().beginTransaction().replace(R.id.camera_preview,
                new SettingsFragment()).addToBackStack(null).commit();
    }
});

即为设置按钮绑定点击监听,当点击设置按钮时,就显示设置菜单。

getFragmentManager().beginTransaction().replace(R.id.camera_preview,
        new SettingsFragment()).addToBackStack(null).commit();

是调用Fragment的比较通用的写法,关于具体细节还请自行查找。

修改后的MainActivity内容如下:

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;

public class MainActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        CameraPreview mPreview = new CameraPreview(this);
        FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);
        preview.addView(mPreview);

        Button buttonSettings = (Button) findViewById(R.id.button_settings);
        buttonSettings.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getFragmentManager().beginTransaction().replace(R.id.camera_preview,
                        new SettingsFragment()).addToBackStack(null).commit();
            }
        });
    }
}

运行一下看看

现在就可以运行你的APP啦,现在APP的最右边会有“设置”按钮,点击此按钮后就会在相机预览上显示“照片品质”这个菜单条目,而点击这个条目,就会弹出从50到100共6个选项,其中100已经默认选中。效果如下:

Screenshot Simple Settings

但是现在这个设置菜单很简单,而且没有实际功能,我们接下来进行完善。

动态添加设置条目

像相机支持的分辨率,支持的对焦方式等都因设备而异,我们不大可能在preferences.xml就写死相机支持的分辨率的值;而为了能够让我们的APP能够运行在不同的设备上(如果只需要运行在特定的设备上,会容易许多),我们尝试动态加载设置条目的列表选项。

完善菜单条目

菜单条目名称我们是能够直接写在文件中的,因为不同相机基本都支持这些设置,只是value不同而已。

修改preferences.xml,修改后内容如下:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <ListPreference
        android:key="preview_size"
        android:title="相机预览分辨率" />
    <ListPreference
        android:key="picture_size"
        android:title="照片分辨率" />
    <ListPreference
        android:key="video_size"
        android:title="视频分辨率" />
    <SwitchPreference
        android:defaultValue="true"
        android:key="gps_data"
        android:title="地理位置" />
    <ListPreference
        android:defaultValue="auto"
        android:key="flash_mode"
        android:title="闪光灯" />
    <ListPreference
        android:key="focus_mode"
        android:title="对焦模式" />
    <ListPreference
        android:defaultValue="auto"
        android:key="white_balance"
        android:title="白平衡" />
    <ListPreference
        android:defaultValue="auto"
        android:key="scene_mode"
        android:title="场景" />
    <ListPreference
        android:defaultValue="0"
        android:key="exposure_compensation"
        android:title="曝光补偿" />
    <ListPreference
        android:defaultValue="100"
        android:entries="@array/pref_jpegQuality"
        android:entryValues="@array/pref_jpegQuality"
        android:key="jpeg_quality"
        android:title="照片品质" />
</PreferenceScreen>

上面的这些选项基本覆盖到了所有常用的选项(在你读完这篇文章后你还可以自己添加想要的选项)。注意到除了“照片品质”外的所有ListPreference都没有entriesentryValues,这些值就是我们需要在代码中动态添加的;上面的“地理位置”是一个SwitchPreference,就是开关那种样式的,只有两个值。注意到有些条目有defaultValue默认值,是因为这些默认值对于不同相机都是通用的。

这样修改后,由于条目内容不完整,APP暂时就不能运行了

动态添加

获取相机

添加条目的列表选项当然是在SettingsFragment中了。想要知道相机支持的参数,首先需要获得一个打开的相机,这个先不追究,在SettingsFragment中添加

import android.hardware.Camera;

static Camera mCamera;
static Camera.Parameters mParameters;

public static void passCamera(Camera camera) {
    mCamera = camera;
    mParameters = camera.getParameters();
}

passCamera()用来将相机传输给SettingsFragmentSettingsFragment将相机保存到静态成员变量mCamera中,getParameters()则用来获取相机参数,将相机参数保存到静态成员变量mParameters中。

动态加载预览分辨率

首先以动态加载预览分辨率为例,介绍动态加载过程。预览分辨率即相机预览时屏幕显示的分辨率大小。在SettingsFragment中添加

import android.preference.ListPreference;
import java.util.ArrayList;
import java.util.List;

public static final String KEY_PREF_PREV_SIZE = "preview_size";

private void loadSupportedPreviewSize() {
    cameraSizeListToListPreference(mParameters.getSupportedPreviewSizes(), KEY_PREF_PREV_SIZE);
}

private void cameraSizeListToListPreference(List<Camera.Size> list, String key) {
    List<String> stringList = new ArrayList<>();
    for (Camera.Size size : list) {
        String stringSize = size.width + "x" + size.height;
        stringList.add(stringSize);
    }
    stringListToListPreference(stringList, key);
}

private void stringListToListPreference(List<String> list, String key) {
    final CharSequence[] charSeq = list.toArray(new CharSequence[list.size()]);
    ListPreference listPref = (ListPreference) getPreferenceScreen().findPreference(key);
    listPref.setEntries(charSeq);
    listPref.setEntryValues(charSeq);
}

静态成员变量KEY_PREF_PREV_SIZE存储预览分辨率的key(在preferences.xml定义)。

先看stringListToListPreference(),其首先将List<String>转换为CharSequence[];由getPreferenceScreen().findPreference()获取由key指定的菜单条目;由setEntries()setEntryValues向这个菜单条目指定用户可见的所有value和代码可见的所有value,就完成了对这个key条目的列表内容的动态加载。

mParameters.getSupportedPreviewSizes()获取相机支持的所有预览分辨率,保存在List<Camera.Size>中,再由cameraSizeListToListPreference()List<Camera.Size>转换为List<String>

整个过程逻辑挺清晰的,理解起来应该没太大问题。

动态加载曝光补偿

再举个例子,在SettingsFragment中添加

public static final String KEY_PREF_EXPOS_COMP = "exposure_compensation";

private void loadSupportedExposeCompensation() {
    int minExposComp = mParameters.getMinExposureCompensation();
    int maxExposComp = mParameters.getMaxExposureCompensation();
    List<String> exposComp = new ArrayList<>();
    for (int value = minExposComp; value <= maxExposComp; value++) {
        exposComp.add(Integer.toString(value));
    }
    stringListToListPreference(exposComp, KEY_PREF_EXPOS_COMP);
}

mParameters.getMinExposureCompensation()获取相机支持的最低曝光补偿,mParameters.getMaxExposureCompensation()获取相机支持的最高曝光补偿,由最低到最高形成一个List<String>,指定key后交给stringListToListPreference()就好了。

全部的动态加载项

下面贴上全部的load,嫌乱可以到DEMO中去看完整代码,注意这里的load一个都不能少。

public static final String KEY_PREF_PREV_SIZE = "preview_size";
public static final String KEY_PREF_PIC_SIZE = "picture_size";
public static final String KEY_PREF_VIDEO_SIZE = "video_size";
public static final String KEY_PREF_FLASH_MODE = "flash_mode";
public static final String KEY_PREF_FOCUS_MODE = "focus_mode";
public static final String KEY_PREF_WHITE_BALANCE = "white_balance";
public static final String KEY_PREF_SCENE_MODE = "scene_mode";
public static final String KEY_PREF_GPS_DATA = "gps_data";
public static final String KEY_PREF_EXPOS_COMP = "exposure_compensation";
public static final String KEY_PREF_JPEG_QUALITY = "jpeg_quality";

private void loadSupportedPreviewSize() {
    cameraSizeListToListPreference(mParameters.getSupportedPreviewSizes(), KEY_PREF_PREV_SIZE);
}

private void loadSupportedPictureSize() {
    cameraSizeListToListPreference(mParameters.getSupportedPictureSizes(), KEY_PREF_PIC_SIZE);
}

private void loadSupportedVideoeSize() {
    cameraSizeListToListPreference(mParameters.getSupportedVideoSizes(), KEY_PREF_VIDEO_SIZE);
}

private void loadSupportedFlashMode() {
    stringListToListPreference(mParameters.getSupportedFlashModes(), KEY_PREF_FLASH_MODE);
}

private void loadSupportedFocusMode() {
    stringListToListPreference(mParameters.getSupportedFocusModes(), KEY_PREF_FOCUS_MODE);
}

private void loadSupportedWhiteBalance() {
    stringListToListPreference(mParameters.getSupportedWhiteBalance(), KEY_PREF_WHITE_BALANCE);
}

private void loadSupportedSceneMode() {
    stringListToListPreference(mParameters.getSupportedSceneModes(), KEY_PREF_SCENE_MODE);
}

private void loadSupportedExposeCompensation() {
    int minExposComp = mParameters.getMinExposureCompensation();
    int maxExposComp = mParameters.getMaxExposureCompensation();
    List<String> exposComp = new ArrayList<>();
    for (int value = minExposComp; value <= maxExposComp; value++) {
        exposComp.add(Integer.toString(value));
    }
    stringListToListPreference(exposComp, KEY_PREF_EXPOS_COMP);
}

设置菜单创建时即加载

onCreate()触发时就调用这些load进行动态加载,在onCreate()尾部添加

loadSupportedPreviewSize();
loadSupportedPictureSize();
loadSupportedVideoeSize();
loadSupportedFlashMode();
loadSupportedFocusMode();
loadSupportedWhiteBalance();
loadSupportedSceneMode();
loadSupportedExposeCompensation();

向SettingsFragment传入相机

所有上面的代码要执行必须首先获取到一个打开的相机,而CameraPreview正好有一个打开的相机,我们就可以通过MainActivity进行相机的传递。

首先需要修改一下CameraPreviewgetCameraInstance()方法,使其返回正在使用的相机,修改getCameraInstance()

public Camera getCameraInstance() {
    if (mCamera == null) {
        try {
            mCamera = Camera.open();
        } catch (Exception e) {
            Log.d(TAG, "camera is not available");
        }
    }
    return mCamera;
}

然后在MainActivity中按钮监听之前添加

SettingsFragment.passCamera(mPreview.getCameraInstance());

SettingsFragment传递来自mPreview的相机。

声明GPS权限

注意到菜单中有个GPS的选项,想要拍到的照片中包含GPS信息,就要在AndroidManifest.xml中声明需要GPS权限。在AndroidManifest.xml中添加

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

运行一下看看

现在就可以运行你的APP啦,点击“设置”按钮,就会出现很多的设置选项,而点击这些选项就是显示出动态加载得到的列表项目。效果如下:

Screenshot Settings

为菜单条目设定默认值

现在我们来谈谈偏好设置,即Preference的问题。通常来说,在Android中,每个APP的每个设置条目都是一个Preference,这些Preference以键值对(key-value)的形式,存储在每个APP的指定文件中。当APP首次运行时,则只有key没有value;不过可以通过Android提供的方法,加载xml文件中的默认值(defaultValue)到对应的value,Android推荐给每个key都指定默认值,否则默认值为空。记住这些key-value都是写在文件中的,一旦value发生变化,对应文件中value值也发生变化,而且以后启动APP这些值都不会被重置(不过可以通过代码重置)。所以通常的做法就是在APP首次启动时给所有的key都生成默认值即value,然后加载默认值实现不同的偏好;在以后APP启动时,就会读取这些key-value实现不同偏好,即初始化。

给xml中有默认值的添加默认值

虽然在xml文件中指定了默认值,但还是要用代码让其加载。在MainActivityonCreate()适当位置添加

PreferenceManager.setDefaultValues(this, R.xml.preferences, false);

其中false代表在执行这个方法时,如果key已经有value则不进行任何操作(即不覆盖),否则设置value为xml文件中的指定值。因为APP每次运行都会执行onCreate(),设为false很有必要。

动态添加默认值

仔细点看你会发现,如果没有进行手动设置,设置菜单的“相机预览分辨率”、“照片分辨率”、“对焦模式”是没有默认值的,现在我们就为其添加默认值。因为从“正规”来说,Android只提供了从xml文件添加默认值的方法(就像其他有默认值的条目一样),想要用代码实现默认值就得花点功夫了。

修改SettingsFragment

Update 20160504: 本块内容分割线以下为旧方法,不再采用

我们把目光放到SettingsFragment上,可以创建一个方法setDefault(),就像上面添加静态默认值那样,通过调用setDefault(),来添加动态默认值。但setDefault()只在最初“相机预览分辨率”等没有默认值时才为其指定默认值,否则不进行任何操作。所以setDefault()首先找到“相机预览分辨率”的value值,如果值为空则指定默认值,否则返回。我们还应该注意到,相机本身是有默认值的,只是我们的偏好设置中还没有设置这个默认值,因此我们可以获取到相机的默认值,然后构造成为偏好设置中的格式,将这个值指定为value就可以了。

这样setDefault()代码就出来了

public static void setDefault(SharedPreferences sharedPrefs) {
    String valPreviewSize = sharedPrefs.getString(KEY_PREF_PREV_SIZE, null);
    if (valPreviewSize == null) {
        SharedPreferences.Editor editor = sharedPrefs.edit();
        editor.putString(KEY_PREF_PREV_SIZE, getDefaultPreviewSize());
        editor.putString(KEY_PREF_PIC_SIZE, getDefaultPictureSize());
        editor.putString(KEY_PREF_VIDEO_SIZE, getDefaultVideoSize());
        editor.putString(KEY_PREF_FOCUS_MODE, getDefaultFocusMode());
        editor.apply();
    }
}

private static String getDefaultPreviewSize() {
    Camera.Size previewSize = mParameters.getPreviewSize();
    return previewSize.width + "x" + previewSize.height;
}

private static String getDefaultPictureSize() {
    Camera.Size pictureSize = mParameters.getPictureSize();
    return pictureSize.width + "x" + pictureSize.height;
}

private static String getDefaultVideoSize() {
    Camera.Size VideoSize = mParameters.getPreferredPreviewSizeForVideo();
    return VideoSize.width + "x" + VideoSize.height;
}

private static String getDefaultFocusMode() {
    List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
    if (supportedFocusModes.contains("continuous-picture")) {
        return "continuous-picture";
    }
    return "continuous-video";
}

SharedPreferencesMainActivity提供,是操作Preference的接口,既可以读取也可以写入。SharedPreferences.Editor就是编辑Preference,其putString()将key-value对写入到APP中,注意最后需要apply()保存这些更改(也可以用commit(),但效率会低一些)。具体怎么找到value代码很简单,就是通过mParameters获取此时相机预览的参数,然后转换为特定形式的String,就作为value返回了。

这样就完成动态添加默认值,可以发现我们只需要用到SharedPreferencesmParameters,所以只需要在MainActivity中传递了相机之后调用就好了;因为方法是静态方法,调用时甚至都不需要实例化。


我们把目光放到SettingsFragment上,可以创建一个方法setDefault(),在onCreate()中调用setDefault()。但setDefault()只在最初“相机预览分辨率”等没有默认值时才为其指定默认值,否则不进行任何操作。所以setDefault()首先找到“相机预览分辨率”的value值,如果值为空则指定默认值,否则返回;而指定默认值可以直接从其动态加载的value列表中选择第一个就好了。

这样setDefault()代码就出来了

public void setDefault() {
    ListPreference prefPreviewSize = (ListPreference) getPreferenceScreen().findPreference(KEY_PREF_PREV_SIZE);
    if (prefPreviewSize.getValue() == null) {
        prefPreviewSize.setValueIndex(0);
        ListPreference prefPictureSize = (ListPreference) getPreferenceScreen().findPreference(KEY_PREF_PIC_SIZE);
        prefPictureSize.setValueIndex(0);
        ListPreference prefVideoSize = (ListPreference) getPreferenceScreen().findPreference(KEY_PREF_VIDEO_SIZE);
        prefVideoSize.setValueIndex(0);
        ListPreference prefFocusMode = (ListPreference) getPreferenceScreen().findPreference(KEY_PREF_FOCUS_MODE);
        if (prefFocusMode.findIndexOfValue("continuous-picture") != -1) {
            prefFocusMode.setValue("continuous-picture");
        } else {
            prefFocusMode.setValue("continuous-video");
        }
    }
}

getPreferenceScreen().findPreference()获取菜单条目,setValueIndex(0)则将其value设置为value列表中的第一个。还记得之前APP相机预览一片模糊吗?现在就解决这个问题啦!首先查找对焦方式中是否存在continuous-picture,若存在则设置,否则设置为continuous-video(设备一般都支持这两种对焦模式)。这两种对焦模式都会在镜头移动时重新对焦,就像其他的相机APP那样。

注意setDefault()需要在条目已经动态生成后运行,所以在onCreate()中应当在所有load之后调用这个方法

修改MainActivity

Update 20160504: 本块内容分割线以下为旧方法,不再采用

上面的setDefault()每次调用时都会判断“相机预览分辨率”有没有默认值,因此在MainActivity中就可以放心大胆调用了,我们可以把这个方法的调用和onCreate中的setDefaultValues()放在一起;虽然onCreate()每次都会调用这个方法,但只有在APP第一次运行时才会真正进行动态添加默认值。

onCreate()中涉及到SettingsFragment的代码就是

SettingsFragment.passCamera(mPreview.getCameraInstance());
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
SettingsFragment.setDefault(PreferenceManager.getDefaultSharedPreferences(this));
SettingsFragment.init(PreferenceManager.getDefaultSharedPreferences(this));

很简单不必过多解释。


这里要注意一个问题,动态添加默认值会在SettingsFragmentonCreate()方法执行时执行,而SettingsFragmentonCreate()会在主窗口点击了“设置”后才会触发。那么如果APP从来没有点击“设置”就永远不会动态加载默认值了?APP每次运行都会读取设置菜单条目并进行偏好设置,所以在APP第一次运行时必须在不点击“设置”时就触发SettingsFragmentonCreate()

我想到的一个很“笨”的方法是,在MainActivityonCreate()中,也对“相机预览分辨率”是否有value进行判断,如果没有value则强行调用SettingsFragment,触发其onCreate();如果有value则不进行任何操作。这样,在MainActivityonCreate()中,passCamera()后加入如下代码:

if (PreferenceManager.getDefaultSharedPreferences(this).getString(SettingsFragment.KEY_PREF_PREV_SIZE, null) == null) {
     getFragmentManager().beginTransaction().replace(R.id.camera_preview, new SettingsFragment()).addToBackStack(null).commit();
     getFragmentManager().executePendingTransactions();
 }

即可像静态加载默认值那样,动态添加默认值了。效果如下:

Screenshot Preview Size Settings

需要注意的地方

Update 20160504: 本块内容分割线以下为旧内容

之前的方法在APP第一次运行时会出现设置菜单需要手动退出,虽然对用户体验影响不太大,但终究是不符合常理。更新后的代码不再存在这个问题,且逻辑更为清晰。


这样实现动态添加默认值后,造成的负面影响就是APP在首次运行时,会自动出现设置菜单,需要手动退出。但这种情况只会在APP第一次运行时产生,以后都不会再出现,所以还是可以忍受的。其实有更复杂一些的方法解决这个问题,但为了这个介绍的简介,就不考虑那些方法了。

APP启动时加载偏好

我们自然希望相机APP每次启动时都能自动加载好之前偏好设置,思路也很简单,在SettingsFragment中创建init()方法,负责设置相机;在MainActivityonCreate()中,在设置完静态和动态默认值后调用init()方法,完成相机设置。

修改SettingsFragment

SettingsFragment中添加

import android.content.SharedPreferences;

public static void init(SharedPreferences sharedPref) {
    setPreviewSize(sharedPref.getString(KEY_PREF_PREV_SIZE, ""));
    setPictureSize(sharedPref.getString(KEY_PREF_PIC_SIZE, ""));
    setFlashMode(sharedPref.getString(KEY_PREF_FLASH_MODE, ""));
    setFocusMode(sharedPref.getString(KEY_PREF_FOCUS_MODE, ""));
    setWhiteBalance(sharedPref.getString(KEY_PREF_WHITE_BALANCE, ""));
    setSceneMode(sharedPref.getString(KEY_PREF_SCENE_MODE, ""));
    setExposComp(sharedPref.getString(KEY_PREF_EXPOS_COMP, ""));
    setJpegQuality(sharedPref.getString(KEY_PREF_JPEG_QUALITY, ""));
    setGpsData(sharedPref.getBoolean(KEY_PREF_GPS_DATA, false));
    mCamera.stopPreview();
    mCamera.setParameters(mParameters);
    mCamera.startPreview();
}

private static void setPreviewSize(String value) {
    String[] split = value.split("x");
    mParameters.setPreviewSize(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
}

private static void setPictureSize(String value) {
    String[] split = value.split("x");
    mParameters.setPictureSize(Integer.parseInt(split[0]), Integer.parseInt(split[1]));
}

private static void setFocusMode(String value) {
    mParameters.setFocusMode(value);
}

private static void setFlashMode(String value) {
    mParameters.setFlashMode(value);
}

private static void setWhiteBalance(String value) {
    mParameters.setWhiteBalance(value);
}

private static void setSceneMode(String value) {
    mParameters.setSceneMode(value);
}

private static void setExposComp(String value) {
    mParameters.setExposureCompensation(Integer.parseInt(value));
}

private static void setJpegQuality(String value) {
    mParameters.setJpegQuality(Integer.parseInt(value));
}

private static void setGpsData(Boolean value) {
    if (value.equals(false)) {
        mParameters.removeGpsData();
    }
}

SharedPreferences即APP存储的key-value对;getString()即获取指定key的value值,对于ListPreferencegetString(),对于SwitchPreferencegetBoolean()。这些set就是分别修改相机参数mParameters的不同属性,很简单不详细解释了。最后,首先停止相机预览,然后应用修改后的mParameters到相机,最后再开始相机预览。这样就完成了相机的偏好设置。

修改MainActivity

MainActivityonCreate()设置完静态和动态默认值后添加

SettingsFragment.init(PreferenceManager.getDefaultSharedPreferences(this));

其中PreferenceManager.getDefaultSharedPreferences(this)就是得到此APP的SharedPreferences

运行一下看看

打开APP,修改偏好设置后关闭APP(需要退出后台,下一篇文章会介绍一个不需要这么麻烦的方法),再次打开APP就能看到效果了。

条目value变化时立即应用设置

我们肯定不希望修改设置后,需要重启APP才能看到效果。现在来实现修改设置后,相机立即应用新的设置。

我们只需要监听条目value变化就好了,一旦出现变化,就根据其key立即应用新的value到相机。

给SettingsFragment添加监听接口

修改

public class SettingsFragment extends PreferenceFragment

public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener

OnSharedPreferenceChangeListener为监听Preference变化的接口

给SettingsFragment添加监听事件

SettingsFragment中添加

public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
    switch (key) {
        case KEY_PREF_PREV_SIZE:
            setPreviewSize(sharedPreferences.getString(key, ""));
            break;
        case KEY_PREF_PIC_SIZE:
            setPictureSize(sharedPreferences.getString(key, ""));
            break;
        case KEY_PREF_FOCUS_MODE:
            setFocusMode(sharedPreferences.getString(key, ""));
            break;
        case KEY_PREF_FLASH_MODE:
            setFlashMode(sharedPreferences.getString(key, ""));
            break;
        case KEY_PREF_WHITE_BALANCE:
            setWhiteBalance(sharedPreferences.getString(key, ""));
            break;
        case KEY_PREF_SCENE_MODE:
            setSceneMode(sharedPreferences.getString(key, ""));
            break;
        case KEY_PREF_EXPOS_COMP:
            setExposComp(sharedPreferences.getString(key, ""));
            break;
        case KEY_PREF_JPEG_QUALITY:
            setJpegQuality(sharedPreferences.getString(key, ""));
            break;
        case KEY_PREF_GPS_DATA:
            setGpsData(sharedPreferences.getBoolean(key, false));
            break;
    }
    mCamera.stopPreview();
    mCamera.setParameters(mParameters);
    mCamera.startPreview();
}

@Override
public void onResume() {
    super.onResume();
    getPreferenceScreen().getSharedPreferences()
            .registerOnSharedPreferenceChangeListener(this);
}

@Override
public void onPause() {
    super.onPause();
    getPreferenceScreen().getSharedPreferences()
            .unregisterOnSharedPreferenceChangeListener(this);
}

onSharedPreferenceChanged()为监听事件回调,干的事情就像之前说的,代码也很简单。onResume()onPause()是Android推荐写法,防止由于Fragment的不断调用导致事件监听失效。

运行一下看看

现在在APP中修改设置马上就能看到效果了!

设置菜单中显示ListPreference的当前值

目前功能已经全部实现了,现在做一点美化。设置菜单中的ListPreference只显示了其条目的标题,而当前用户可见的value只有在点击条目后才会显示,现在就来实现让当前值直接显示在条目上。

添加方法

ListPreference显示当前值,就是为其summary赋值,即将当前值赋给其summary。

SettingsFragment中添加

private static void initSummary(Preference pref) {
    if (pref instanceof PreferenceGroup) {
        PreferenceGroup prefGroup = (PreferenceGroup) pref;
        for (int i = 0; i < prefGroup.getPreferenceCount(); i++) {
            initSummary(prefGroup.getPreference(i));
        }
    } else {
        updatePrefSummary(pref);
    }
}

private static void updatePrefSummary(Preference pref) {
    if (pref instanceof ListPreference) {
        pref.setSummary(((ListPreference) pref).getEntry());
    }
}

initSummary()处理全部的Preference,主要是处理含有PreferenceGroup的情况,这个APP目前没有这个情况,但还是保留这个功能。updatePrefSummary()则处理具体的ListPreference,将其用户可见的值getEntry()通过setSummary()赋给summary。

调用方法

有两个调用上述方法的地方。

首先是SettingsFragmentonCreate()中,给所有的Preference都设置summary。在onCreate最后添加

initSummary(getPreferenceScreen());

其次是在onSharedPreferenceChanged()中,每次条目value发生变化时,summary也随机变化。在onSharedPreferenceChanged()最头上添加

updatePrefSummary(findPreference(key));

运行一下看看

现在设置菜单的ListPreference都显示其当前值了。效果如下:

Screenshot Summary Settings

一点唠叨

现在看来实现偏好设置还是有些麻烦的,其实主要难点在动态加载和默认值上,为了能够自适应不同设备就是要这么折腾。像系统自带的相机可能就可以根据设备本身直接将分辨率等都写到xml文件中,这样工作量小了不少。本篇还是没有实现基本的拍照功能,不过已经是一步之遥了呢。

DEMO

本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-EnableSettings

参考