Android相机开发(二): 给相机加上偏好设置
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
类
内容如下:
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
文件
内容如下:
<?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_jpegQuality
的array
,这个稍后解释;最后为这个条目设定一个默认值即defaultValue
,设为100。
添加array值
在res
->values
下新建arrays.xml
文件
内容如下:
<?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_jpegQuality
的array
,供上一步的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已经默认选中。效果如下:
但是现在这个设置菜单很简单,而且没有实际功能,我们接下来进行完善。
动态添加设置条目
像相机支持的分辨率,支持的对焦方式等都因设备而异,我们不大可能在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
都没有entries
和entryValues
,这些值就是我们需要在代码中动态添加的;上面的“地理位置”是一个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()
用来将相机传输给SettingsFragment
,SettingsFragment
将相机保存到静态成员变量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
进行相机的传递。
首先需要修改一下CameraPreview
的getCameraInstance()
方法,使其返回正在使用的相机,修改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啦,点击“设置”按钮,就会出现很多的设置选项,而点击这些选项就是显示出动态加载得到的列表项目。效果如下:
为菜单条目设定默认值
现在我们来谈谈偏好设置,即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文件中指定了默认值,但还是要用代码让其加载。在MainActivity
的onCreate()
适当位置添加
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";
}
SharedPreferences
由MainActivity
提供,是操作Preference
的接口,既可以读取也可以写入。SharedPreferences.Editor
就是编辑Preference
,其putString()
将key-value对写入到APP中,注意最后需要apply()
保存这些更改(也可以用commit()
,但效率会低一些)。具体怎么找到value代码很简单,就是通过mParameters
获取此时相机预览的参数,然后转换为特定形式的String,就作为value返回了。
这样就完成动态添加默认值,可以发现我们只需要用到SharedPreferences
和mParameters
,所以只需要在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));
很简单不必过多解释。
这里要注意一个问题,动态添加默认值会在SettingsFragment
的onCreate()
方法执行时执行,而SettingsFragment
的onCreate()
会在主窗口点击了“设置”后才会触发。那么如果APP从来没有点击“设置”就永远不会动态加载默认值了?APP每次运行都会读取设置菜单条目并进行偏好设置,所以在APP第一次运行时必须在不点击“设置”时就触发SettingsFragment
的onCreate()
。
我想到的一个很“笨”的方法是,在MainActivity
的onCreate()
中,也对“相机预览分辨率”是否有value进行判断,如果没有value则强行调用SettingsFragment
,触发其onCreate()
;如果有value则不进行任何操作。这样,在MainActivity
的onCreate()
中,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();
}
即可像静态加载默认值那样,动态添加默认值了。效果如下:
需要注意的地方
Update 20160504: 本块内容分割线以下为旧内容
之前的方法在APP第一次运行时会出现设置菜单需要手动退出,虽然对用户体验影响不太大,但终究是不符合常理。更新后的代码不再存在这个问题,且逻辑更为清晰。
这样实现动态添加默认值后,造成的负面影响就是APP在首次运行时,会自动出现设置菜单,需要手动退出。但这种情况只会在APP第一次运行时产生,以后都不会再出现,所以还是可以忍受的。其实有更复杂一些的方法解决这个问题,但为了这个介绍的简介,就不考虑那些方法了。
APP启动时加载偏好
我们自然希望相机APP每次启动时都能自动加载好之前偏好设置,思路也很简单,在SettingsFragment
中创建init()
方法,负责设置相机;在MainActivity
的onCreate()
中,在设置完静态和动态默认值后调用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值,对于ListPreference
是getString()
,对于SwitchPreference
是getBoolean()
。这些set就是分别修改相机参数mParameters
的不同属性,很简单不详细解释了。最后,首先停止相机预览,然后应用修改后的mParameters
到相机,最后再开始相机预览。这样就完成了相机的偏好设置。
修改MainActivity
在MainActivity
的onCreate()
设置完静态和动态默认值后添加
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。
调用方法
有两个调用上述方法的地方。
首先是SettingsFragment
的onCreate()
中,给所有的Preference
都设置summary。在onCreate
最后添加
initSummary(getPreferenceScreen());
其次是在onSharedPreferenceChanged()
中,每次条目value发生变化时,summary也随机变化。在onSharedPreferenceChanged()
最头上添加
updatePrefSummary(findPreference(key));
运行一下看看
现在设置菜单的ListPreference
都显示其当前值了。效果如下:
一点唠叨
现在看来实现偏好设置还是有些麻烦的,其实主要难点在动态加载和默认值上,为了能够自适应不同设备就是要这么折腾。像系统自带的相机可能就可以根据设备本身直接将分辨率等都写到xml文件中,这样工作量小了不少。本篇还是没有实现基本的拍照功能,不过已经是一步之遥了呢。
DEMO
本文实现的相机APP源码都放在GitHub上,如果需要请点击zhantong/AndroidCamera-EnableSettings。
参考
- Settings | Android Developers
- Preference | Android Developers
- ListPreference | Android Developers
- SharedPreferences | Android Developers
- PreferenceFragment | Android Developers
- PreferenceScreen | Android Developers
- Camera | Android Developers
- Camera.Parameters | Android Developers
- android - How to pass a variable from Activity to Fragment, and pass it back? - Stack Overflow
- Android Shared Preferences example using PreferenceFragment - Store, fetch and edit
- android - How to fill ListPreference dynamically when onPreferenceClick is triggered? - Stack Overflow
- How to initialize a new Camera.Size in Android - Stack Overflow
- java - Android, how to populate a CharSequence array dynamically (not initializing?) - Stack Overflow
- Killing android application on pause - Stack Overflow
- user interface - How do I display the current value of an Android Preference in the Preference summary? - Stack Overflow
private static void setWhiteBalance(String value) {
mParameters.setWhiteBalance(value);}
这种mParameters是在onCreate中赋有效值的,采用static方式访问是否欠妥
理论上这个文件中的所有代码都是在oCreate()后才会执行的(这里没有static{}代码),所以正常情况下任何对mParameters的访问都是会在onCreate()赋值后才进行的。
当然也可以采用更为严谨的实现方法,但为简便起见这里就是这样实现的了。
您好,我想请问一下向本篇文章的动态加载这些设置在Camera2里面是如何实现的呢?有空能联系我一下吗?非常感谢。请联系我邮箱。
现在没太接触Camera2了,不过你可以参考https://github.com/googlesamples/android-Camera2Basic,以及https://github.com/pinguo-yuyidong/Camera2利用Camera2实现相机的示例,里面有类似的代码。
你好,我这些都看过了,我主要是现在需要实现这个参数的提取,我网上参考了好多博客和demo一直没有找到合适的,那天看到了您这个发现有很大的相似,我原来也是尝试过很多提取的方法,发现与结果相差有点儿大。谢谢您的建议啦。
这个应该是设备不支持这个偏好设置的问题,根源是Android系统的某些实现是由厂家来做,但厂家没有完全按照规范来。意思就是代码中有一些获取相机参数或设置相机参数的代码,我是按照Android规范来的,但厂商的实现没有照规范来,所以获取和设置参数的地方出错崩溃。
目前来说,解决方法是加一段先try再用这些参数的代码,即如果获取参数失败就不加入Preference,否则正常处理。这部分代码我好早就想加了,但限于没时间,而且增加代码难度,就一直没更新。
你直接删掉也是可以的,但你要是为兼容性考虑的话就老实用try catch咯~