<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2011 The Android Open Source Project
+<!-- Copyright (C) 2013 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
limitations under the License.
-->
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_enabled="false"
- android:drawable="@drawable/ic_share_disabled" />
- <item android:state_enabled="true"
- android:drawable="@drawable/ic_share_normal" />
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:constantSize="true">
+ <item android:state_enabled="false">
+ <bitmap
+ android:gravity="center"
+ android:src="@drawable/ic_share_disabled" />
+ </item>
+ <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@drawable/ic_share_normal" />
+ </item>
</selector>
<ImageButton
android:id="@+id/filmstrip_bottom_control_share"
style="@style/FilmstripBottomControlButton"
- android:src="@drawable/ic_menu_share"
+ android:src="@drawable/ic_share"
android:visibility="invisible"
android:contentDescription="@string/share_button_description" />
<string name="crop_save" msgid="2841974981340098579">"አስቀምጥ"</string>
<string name="cannot_load_image" msgid="4100136187076585580">"ምስሉን መጫን አልተቻለም!"</string>
<string name="switch_photo_filmstrip" msgid="1448511001008888767">"የድርድር ፊልም እይታ"</string>
- <string name="setting_wallpaper" msgid="2397759659347872725">"á\8b¨á\8c\8dá\8bµá\8c\8dá\8b³ á\8b\88á\88¨á\89\80á\89µ በማዘጋጀት ላይ"</string>
+ <string name="setting_wallpaper" msgid="2397759659347872725">"á\88\8dá\8c£á\8d\8d በማዘጋጀት ላይ"</string>
<string name="mode_settings" msgid="2021937261522670921">"ቅንብሮች"</string>
<string name="mode_camera" msgid="279763925715250603">"ካሜራ"</string>
<string name="mode_video" msgid="8633929034048169139">"ቪዲዮ"</string>
<string name="set_duration" msgid="1638453882581604341">"Define la duración en segundos"</string>
<string name="count_down_title_text" msgid="7586031110595513050">"Cuenta atrás para hacer una foto"</string>
<string name="remember_location_title" msgid="3045040613094030429">"¿Recordar ubicaciones de las fotos?"</string>
- <string name="remember_location_prompt" msgid="5104210757873140169">"Etiqueta tus fotos y vídeos con las ubicaciones donde se han realizado.\n\nOtras aplicaciones pueden acceder a esta información, así como a las imágenes guardadas."</string>
+ <string name="remember_location_prompt" msgid="5104210757873140169">"Etiqueta tus fotos y vídeos con las ubicaciones donde se han realizado.\n\nOtras aplicaciones pueden acceder a esta información y a imágenes guardadas."</string>
<string name="remember_location_no" msgid="4412802756840226925">"No, gracias"</string>
<string name="remember_location_yes" msgid="4339424460683531388">"Sí"</string>
<string name="camera_menu_more_label" msgid="7951917844735828365">"MÁS OPCIONES"</string>
<string name="setting_front_camera_photo" msgid="4131886734622868637">"Foto de cámara frontal"</string>
<string name="setting_front_camera_video" msgid="2178799452805359752">"Vídeo de cámara frontal"</string>
<string name="setting_default_camera" msgid="6954076799301004779">"Cámara predeterminada"</string>
- <string name="setting_google_help_and_feedback" msgid="2079580537079242775">"Ayuda y opiniones"</string>
+ <string name="setting_google_help_and_feedback" msgid="2079580537079242775">"Ayuda y sugerencias"</string>
<string name="processing_hdr_plus" msgid="9160093263037540304">"Procesando HDR+…"</string>
<string name="open_source_licenses" msgid="2169711954264883060">"Licencias de código abierto"</string>
<string name="pref_category_general" msgid="6737748849700581019">"Ajustes generales"</string>
<string name="setting_summary_x_megapixels" msgid="6533463462760866830">"%1$s megapíxeles"</string>
<string name="setting_summary_aspect_ratio_and_megapixels" msgid="5828440902461064821">"(%1$d:%2$d) %3$s MP"</string>
<string name="cling_text_for_refocus_editor_button" msgid="4785017397116829802">"Toca para volver a enfocar"</string>
- <string name="pref_category_advanced" msgid="5921085080077574872">"Avanzados"</string>
+ <string name="pref_category_advanced" msgid="5921085080077574872">"Avanzado"</string>
<string name="pref_camera_exposure_compensation" msgid="4143245817259719147">"Exposición manual"</string>
<string name="settings_cling_text" msgid="5368054627268181292">"Cambia la resolución y la calidad en Ajustes o prueba las funciones avanzadas."</string>
<string name="photo_size_selection_title" msgid="2789753590719172645">"Selecciona el tamaño de foto"</string>
<string name="pref_camera_id_label_back" msgid="1645608049757733858">"सामने का कैमरा"</string>
<string name="pref_camera_id_label_front" msgid="349308803062874842">"पीछे का कैमरा"</string>
<string name="dialog_ok" msgid="774141340500181131">"ठीक"</string>
- <string name="dialog_cancel" msgid="692365061128351656">"रदà¥\8dद à¤\95रें"</string>
+ <string name="dialog_cancel" msgid="692365061128351656">"रहनà¥\87 दें"</string>
<string name="spaceIsLow_content" product="default" msgid="4522771065344332702">"आपके SD कार्ड में स्थान कम है. गुणवत्ता सेटिंग बदलें या कुछ चित्र या अन्य फ़ाइलें हटाएं."</string>
<string name="video_reach_size_limit" msgid="9196836111505731836">"आकार सीमा तक पहुंच गए."</string>
<string name="pano_too_fast_prompt" msgid="2503148095578052177">"बहुत तेज़"</string>
<string name="effect_silly_faces" msgid="7952713419757286453">"मज़ाकिया चेहरे"</string>
<string name="effect_background" msgid="1358432220077975015">"पृष्ठभूमि"</string>
<string name="accessibility_shutter_button" msgid="6040483605347230438">"शटर"</string>
- <string name="accessibility_cancel_button" msgid="5679989494636116448">"रदà¥\8dद à¤\95रें"</string>
+ <string name="accessibility_cancel_button" msgid="5679989494636116448">"रहनà¥\87 दें"</string>
<string name="accessibility_menu_button" msgid="7692103503958544723">"मेनू बटन"</string>
<string name="accessibility_check_box" msgid="1084094675439953723">"%1$s चेक बॉक्स"</string>
<string name="accessibility_switch_to_camera" msgid="4518394037216725274">"फ़ोटो पर स्विच करें"</string>
<string name="accessibility_switch_to_photo_sphere" msgid="5803217570370854725">"Photo Sphere पर स्विच करें"</string>
<string name="accessibility_switch_to_gcam" msgid="7562625440767034695">"उच्च गुणवत्ता पर स्विच करें"</string>
<string name="accessibility_switch_to_refocus" msgid="6796169367953860106">"फिर से फ़ोकस पर स्विच करें"</string>
- <string name="accessibility_review_cancel" msgid="5462850829869569629">"समà¥\80à¤\95à¥\8dषा रदà¥\8dद करें"</string>
+ <string name="accessibility_review_cancel" msgid="5462850829869569629">"समà¥\80à¤\95à¥\8dषा ना करें"</string>
<string name="accessibility_review_ok" msgid="3486465319880320270">"समीक्षा पूर्ण"</string>
<string name="accessibility_review_retake" msgid="2547112860787022130">"समीक्षा रीटेक"</string>
<string name="accessibility_mode_options" msgid="6376831760155403217">"विकल्प"</string>
<string name="countdown_timer_duration_3s" msgid="7435393834886072664">"काउंटडाउन टाइमर अवधि 3 सेकंड पर सेट है"</string>
<string name="countdown_timer_duration_10s" msgid="9085308782250002795">"काउंटडाउन टाइमर अवधि 10 सेकंड पर सेट है"</string>
<string name="more_options_desc" msgid="4628738800610478353">"अधिक विकल्प"</string>
- <string name="cancel_button_description" msgid="3801167024006905033">"रदà¥\8dद à¤\95रें"</string>
+ <string name="cancel_button_description" msgid="3801167024006905033">"रहनà¥\87 दें"</string>
<string name="done_button_description" msgid="1334963435441544592">"पूर्ण"</string>
<string name="retake_button_description" msgid="4234613030674787714">"फिर से लें"</string>
<string name="share_button_description" msgid="5108508790540832053">"साझा करें"</string>
<string name="pref_category_general" msgid="6737748849700581019">"सामान्य सेटिंग"</string>
<string name="pref_category_resolution_quality" msgid="6641462402321962896">"रिज़ॉल्यूशन और गुणवत्ता"</string>
<string name="pref_category_about" msgid="1966255405679342337">"संक्षिप्त विवरण"</string>
- <string name="pref_title_build_version" msgid="481489988124832651">"बिलà¥\8dड सà¤\82सà¥\8dà¤\95रण"</string>
+ <string name="pref_title_build_version" msgid="481489988124832651">"बिलà¥\8dड वरà¥\8dशन"</string>
<string name="pref_video_quality_entry_low" msgid="737962621299050603">"निम्न"</string>
<string name="pref_video_quality_entry_high" msgid="1613578418842803393">"उच्च"</string>
<string name="pref_video_quality_entry_qcif" msgid="1717816794399266216">"QCIF"</string>
<string name="photo_aspect_ratio_selection_content" msgid="6668224437962196229">"आप इसे बाद में सेटिंग में बदल सकते हैं."</string>
<string name="share_to" msgid="5144911209144798122">"इनसे साझा करें:"</string>
<string name="edit_with" msgid="615569327230783971">"इससे संपादित करें:"</string>
- <string name="startup_dialog_button_next" msgid="1011831256978228993">"à¤\85à¤\97ला"</string>
+ <string name="startup_dialog_button_next" msgid="1011831256978228993">"à¤\86à¤\97à¥\87"</string>
<string name="confirm_button_text" msgid="7389949384482206814">"ठीक है, समझ लिया"</string>
<string name="full_sensor_4x3_aspect_ratio" msgid="1270461419743888925">"पूर्ण सेंसर \n(4:3)"</string>
<string name="cropped_sensor_16x9_aspect_ratio" msgid="4742161537633251795">"काटा गया सेंसर \n(16:9)"</string>
*/
private LocalDataAdapter mDataAdapter;
+ private OneCameraManager mCameraManager;
private SettingsManager mSettingsManager;
private ModeListView mModeListView;
private boolean mModeListVisible = false;
@Override
public OneCameraManager getCameraManager() {
- return OneCameraManager.get(this);
+ return mCameraManager;
}
private void removeData(int dataID) {
super.onCreate(state);
if (!Glide.isSetup()) {
Glide.setup(new GlideBuilder(this)
- .setResizeService(new FifoPriorityThreadPoolExecutor(1)));
+ .setResizeService(new FifoPriorityThreadPoolExecutor(2)));
Glide.get(this).setMemoryCategory(MemoryCategory.HIGH);
}
mAppContext = getApplicationContext();
mSoundPlayer = new SoundPlayer(mAppContext);
+ mCameraManager = OneCameraManager.get(this);
+
// TODO: Try to move all the resources allocation to happen as soon as
// possible so we can call module.init() at the earliest time.
mModuleManager = new ModuleManagerImpl();
import android.location.Location;
import android.net.Uri;
import android.os.Handler;
+import android.os.HandlerThread;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.view.KeyEvent;
import com.android.ex.camera2.portability.CameraAgent.CameraProxy;
import java.io.File;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
/**
* New Capture module that is made to support photo and video capture on top of
*/
private static final int ON_RESUME_TASKS_DELAY_MSEC = 20;
+ /** Timeout for camera open/close operations. */
+ private static final int CAMERA_OPEN_CLOSE_TIMEOUT_MILLIS = 2500;
+
/** System Properties switch to enable debugging focus UI. */
private static final boolean CAPTURE_DEBUG_UI = DebugPropertyHelper.showCaptureDebugUI();
private CaptureModuleUI mUI;
/** The camera manager used to open cameras. */
private OneCameraManager mCameraManager;
- /** The currently opened camera device. */
+ /** The currently opened camera device, or null if the camera is closed. */
private OneCamera mCamera;
+ /** Held when opening or closing the camera. */
+ private final Semaphore mCameraOpenCloseLock = new Semaphore(1);
/** The direction the currently opened camera is facing to. */
private Facing mCameraFacing = Facing.BACK;
/** Whether HDR is currently enabled. */
/** Main thread handler. */
private Handler mMainHandler;
+ /** Handler thread for camera-related operations. */
+ private Handler mCameraHandler;
/** Current display rotation in degrees. */
private int mDisplayRotation;
Log.d(TAG, "init");
mIsResumeFromLockScreen = isResumeFromLockscreen(activity);
mMainHandler = new Handler(activity.getMainLooper());
+ HandlerThread thread = new HandlerThread("CaptureModule.mCameraHandler");
+ thread.start();
+ mCameraHandler = new Handler(thread.getLooper());
mCameraManager = mAppController.getCameraManager();
mLocationManager = mAppController.getLocationManager();
mDisplayRotation = CameraUtil.getDisplayRotation(mContext);
@Override
public void onRemainingSecondsChanged(int remainingSeconds) {
if (remainingSeconds == 1) {
- mCountdownSoundPlayer.play(R.raw.beep_twice, 0.6f);
+ mCountdownSoundPlayer.play(R.raw.timer_final_second, 0.6f);
} else if (remainingSeconds == 2 || remainingSeconds == 3) {
- mCountdownSoundPlayer.play(R.raw.beep_once, 0.6f);
+ mCountdownSoundPlayer.play(R.raw.timer_increment, 0.6f);
}
}
initSurface(mPreviewTexture);
}
- mCountdownSoundPlayer.loadSound(R.raw.beep_once);
- mCountdownSoundPlayer.loadSound(R.raw.beep_twice);
+ mCountdownSoundPlayer.loadSound(R.raw.timer_final_second);
+ mCountdownSoundPlayer.loadSound(R.raw.timer_increment);
}
@Override
cancelCountDown();
resetTextureBufferSize();
closeCamera();
- mCountdownSoundPlayer.unloadSound(R.raw.beep_once);
- mCountdownSoundPlayer.unloadSound(R.raw.beep_twice);
+ mCountdownSoundPlayer.unloadSound(R.raw.timer_final_second);
+ mCountdownSoundPlayer.unloadSound(R.raw.timer_increment);
// Remove delayed resume trigger, if it hasn't been executed yet.
mMainHandler.removeCallbacksAndMessages(null);
@Override
public void destroy() {
mCountdownSoundPlayer.release();
+ mCameraHandler.getLooper().quitSafely();
}
@Override
// PhotoModule uses FocusOverlayManager which uses API1/portability
// logic and coordinates.
private void triggerFocusAtScreenCoord(int x, int y) {
+ if (mCamera == null) {
+ // If we receive this after the camera is closed, do nothing.
+ return;
+ }
+
mTapToFocusWaitForActiveScan = true;
// Show UI immediately even though scan has not started yet.
float minEdge = Math.min(mPreviewArea.width(), mPreviewArea.height());
private void openCameraAndStartPreview() {
// Only enable HDR on the back camera
boolean useHdr = mHdrEnabled && mCameraFacing == Facing.BACK;
+
+ try {
+ // TODO Given the current design, we cannot guarantee that one of
+ // CaptureReadyCallback.onSetupFailed or onReadyForCapture will
+ // be called (see below), so it's possible that
+ // mCameraOpenCloseLock.release() is never called under extremely
+ // rare cases. If we leak the lock, this timeout ensures that we at
+ // least crash so we don't deadlock the app.
+ if (!mCameraOpenCloseLock.tryAcquire(CAMERA_OPEN_CLOSE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
+ throw new RuntimeException("Time out waiting to acquire camera-open lock.");
+ }
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting to acquire camera-open lock.", e);
+ }
mCameraManager.open(mCameraFacing, useHdr, getPictureSizeFromSettings(),
new OpenCallback() {
@Override
public void onFailure() {
Log.e(TAG, "Could not open camera.");
mCamera = null;
+ mCameraOpenCloseLock.release();
mAppController.showErrorAndFinish(R.string.cannot_connect_camera);
}
@Override
+ public void onCameraClosed() {
+ mCamera = null;
+ mCameraOpenCloseLock.release();
+ }
+
+ @Override
public void onCameraOpened(final OneCamera camera) {
Log.d(TAG, "onCameraOpened: " + camera);
mCamera = camera;
new CaptureReadyCallback() {
@Override
public void onSetupFailed() {
+ // We must release this lock here, before posting
+ // to the main handler since we may be blocked
+ // in pause(), getting ready to close the camera.
+ mCameraOpenCloseLock.release();
Log.e(TAG, "Could not set up preview.");
- mCamera.close(null);
- mCamera = null;
- // TODO: Show an error message and exit.
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mCamera == null) {
+ Log.d(TAG, "Camera closed, aborting.");
+ return;
+ }
+ mCamera.close(null);
+ mCamera = null;
+ // TODO: Show an error message and exit.
+ }
+ });
}
@Override
public void onReadyForCapture() {
- Log.d(TAG, "Ready for capture.");
- onPreviewStarted();
- // Enable zooming after preview has
- // started.
- mUI.initializeZoom(mCamera.getMaxZoom());
- mCamera.setFocusStateListener(CaptureModule.this);
- mCamera.setReadyStateChangedListener(CaptureModule.this);
+ // We must release this lock here, before posting
+ // to the main handler since we may be blocked
+ // in pause(), getting ready to close the camera.
+ mCameraOpenCloseLock.release();
+ mMainHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Log.d(TAG, "Ready for capture.");
+ if (mCamera == null) {
+ Log.d(TAG, "Camera closed, aborting.");
+ return;
+ }
+ onPreviewStarted();
+ // Enable zooming after preview has
+ // started.
+ mUI.initializeZoom(mCamera.getMaxZoom());
+ mCamera.setFocusStateListener(CaptureModule.this);
+ mCamera.setReadyStateChangedListener(CaptureModule.this);
+ }
+ });
}
});
}
- });
+ }, mCameraHandler);
}
private void closeCamera() {
- if (mCamera != null) {
- mCamera.setFocusStateListener(null);
- mCamera.close(null);
- mCamera = null;
+ try {
+ mCameraOpenCloseLock.acquire();
+ } catch(InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting to acquire camera-open lock.", e);
+ }
+ try {
+ if (mCamera != null) {
+ mCamera.setFocusStateListener(null);
+ mCamera.close(null);
+ mCamera = null;
+ }
+ } finally {
+ mCameraOpenCloseLock.release();
}
}
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
+import android.media.AudioManager;
import android.media.CameraProfile;
+import android.media.SoundPool;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
CameraCapabilities.SceneMode.AUTO));
}
updateParametersSceneMode();
- mCameraDevice.applySettings(mCameraSettings);
+ if (mCameraDevice != null) {
+ mCameraDevice.applySettings(mCameraSettings);
+ }
updateSceneMode();
}
}
@Override
public void startFaceDetection() {
- if (mFaceDetectionStarted) {
+ if (mFaceDetectionStarted || mCameraDevice == null) {
return;
}
if (mCameraCapabilities.getMaxNumOfFacesSupported() > 0) {
@Override
public void stopFaceDetection() {
- if (!mFaceDetectionStarted) {
+ if (!mFaceDetectionStarted || mCameraDevice == null) {
return;
}
if (mCameraCapabilities.getMaxNumOfFacesSupported() > 0) {
int xOffset = (originalWidth - newWidth)/2;
int yOffset = (originalHeight - newHeight)/2;
- // For some reason L needs this to work.
- // This code is only run on the Nexus 5.
- // TODO: Determine why this is needed.
- if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
- Log.v(TAG,"xOffset = " + xOffset);
- Log.v(TAG,"yOffset = " + yOffset);
- xOffset *= 2;
- yOffset = 0;
- Log.v(TAG,"new xOffset = " + xOffset);
- Log.v(TAG,"new yOffset = " + yOffset);
- }
-
if (xOffset < 0 || yOffset < 0) {
return dataBundle;
}
CameraCapabilities.FocusMode focusMode) {
CameraCapabilities.Stringifier stringifier = mCameraCapabilities.getStringifier();
SettingsManager settingsManager = mActivity.getSettingsManager();
- settingsManager.set(mAppController.getCameraScope(), Keys.KEY_FLASH_MODE,
- stringifier.stringify(flashMode));
+ if (!CameraCapabilities.FlashMode.NO_FLASH.equals(flashMode)) {
+ settingsManager.set(mAppController.getCameraScope(), Keys.KEY_FLASH_MODE,
+ stringifier.stringify(flashMode));
+ }
settingsManager.set(mAppController.getCameraScope(), Keys.KEY_FOCUS_MODE,
- stringifier.stringify(focusMode));
+ stringifier.stringify(focusMode));
}
@Override
@Override
public void onRemainingSecondsChanged(int remainingSeconds) {
if (remainingSeconds == 1) {
- mCountdownSoundPlayer.play(R.raw.beep_twice, 0.6f);
+ mCountdownSoundPlayer.play(R.raw.timer_final_second, 0.6f);
} else if (remainingSeconds == 2 || remainingSeconds == 3) {
- mCountdownSoundPlayer.play(R.raw.beep_once, 0.6f);
+ mCountdownSoundPlayer.play(R.raw.timer_increment, 0.6f);
}
}
}
Log.v(TAG, "Executing onResumeTasks.");
- mCountdownSoundPlayer.loadSound(R.raw.beep_once);
- mCountdownSoundPlayer.loadSound(R.raw.beep_twice);
+ mCountdownSoundPlayer.loadSound(R.raw.timer_final_second);
+ mCountdownSoundPlayer.loadSound(R.raw.timer_increment);
if (mFocusManager != null) {
// If camera is not open when resume is called, focus manager will
// not be initialized yet, in which case it will start listening to
@Override
public void autoFocus() {
+ if (mCameraDevice == null) {
+ return;
+ }
Log.v(TAG,"Starting auto focus");
mFocusStartTime = System.currentTimeMillis();
mCameraDevice.autoFocus(mHandler, mAutoFocusCallback);
@Override
public void cancelAutoFocus() {
+ if (mCameraDevice == null) {
+ return;
+ }
mCameraDevice.cancelAutoFocus();
setCameraState(IDLE);
setCameraParameters(UPDATE_PARAM_PREFERENCE);
// eventually recurse back into startPreview().
// To avoid calling startPreview() twice, we must acquire
// mStartPreviewLock.
- if (mStartPreviewLock) {
+ if (mStartPreviewLock || mCameraDevice == null) {
// do nothing
return;
}
}
private void updateParametersPictureSize() {
+ if (mCameraDevice == null) {
+ return;
+ }
+
SettingsManager settingsManager = mActivity.getSettingsManager();
String pictureSizeKey = isCameraFrontFacing() ? Keys.KEY_PICTURE_SIZE_FRONT
: Keys.KEY_PICTURE_SIZE_BACK;
(double) size.width() / size.height());
Size original = mCameraSettings.getCurrentPreviewSize();
if (!optimalSize.equals(original)) {
- Log.v(TAG, "setting preview size");
+ Log.v(TAG, "setting preview size. optimal: " + optimalSize + "original: " + original);
mCameraSettings.setPreviewSize(optimalSize);
// Zoom related settings will be changed for different preview
// sizes, so set and read the parameters to get latest values
if (mHandler.getLooper() == Looper.myLooper()) {
+ Log.v(TAG, "matched looper, setting up preview");
// On UI thread only, not when camera starts up
setupPreview();
} else {
+ Log.v(TAG, "no looper match, directly applying settings");
mCameraDevice.applySettings(mCameraSettings);
}
mCameraSettings = mCameraDevice.getSettings();
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void updateAutoFocusMoveCallback() {
+ if (mCameraDevice == null) {
+ return;
+ }
if (mCameraSettings.getCurrentFocusMode() ==
CameraCapabilities.FocusMode.CONTINUOUS_PICTURE) {
mCameraDevice.setAutoFocusMoveCallback(mHandler,
updateCameraParametersPreference();
}
- mCameraDevice.applySettings(mCameraSettings);
+ if (mCameraDevice != null) {
+ mCameraDevice.applySettings(mCameraSettings);
+ }
}
// If the Camera is idle, update the parameters immediately, otherwise
}
});
}
+
+ /**
+ * This class manages the loading/releasing/playing of the sounds needed for
+ * countdown timer.
+ */
+ private class CountdownSoundPlayer {
+ private SoundPool mSoundPool;
+ private int mTimerIncrement;
+ private int mTimerFinalSecond;
+
+ void loadSounds() {
+ // Load the sounds.
+ if (mSoundPool == null) {
+ mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0);
+ mTimerIncrement = mSoundPool.load(mAppController.getAndroidContext(), R.raw.timer_increment, 1);
+ mTimerFinalSecond = mSoundPool.load(mAppController.getAndroidContext(), R.raw.timer_final_second, 1);
+ }
+ }
+
+ void onRemainingSecondsChanged(int newVal) {
+ if (mSoundPool == null) {
+ Log.e(TAG, "Cannot play sound - they have not been loaded.");
+ return;
+ }
+ if (newVal == 1) {
+ mSoundPool.play(mTimerFinalSecond, 1.0f, 1.0f, 0, 0, 1.0f);
+ } else if (newVal == 2 || newVal == 3) {
+ mSoundPool.play(mTimerIncrement, 1.0f, 1.0f, 0, 0, 1.0f);
+ }
+ }
+
+ void release() {
+ if (mSoundPool != null) {
+ mSoundPool.release();
+ mSoundPool = null;
+ }
+ }
+ }
}
import com.android.camera.ui.FaceView;
import com.android.camera.ui.PreviewOverlay;
import com.android.camera.ui.PreviewStatusListener;
+import com.android.camera.util.ApiHelper;
import com.android.camera.util.CameraUtil;
+import com.android.camera.util.GservicesHelper;
import com.android.camera.widget.AspectRatioDialogLayout;
import com.android.camera.widget.AspectRatioSelector;
import com.android.camera.widget.LocationDialogLayout;
* intro dialog on.
*/
private boolean showAspectRatioDialogOnThisDevice() {
- // We only want to show that dialog on N4 and N5
- return "hammerhead".equals(Build.DEVICE) || "mako".equals(Build.DEVICE);
+ // We only want to show that dialog on N4/N5/N6
+ // Don't show if using API2 portability, b/17462976
+ return !GservicesHelper.useCamera2ApiThroughPortabilityLayer(mActivity) &&
+ (ApiHelper.IS_NEXUS_4 || ApiHelper.IS_NEXUS_5 || ApiHelper.IS_NEXUS_6);
}
public void initializeZoom(CameraCapabilities capabilities, CameraSettings settings) {
+++ /dev/null
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera;
-
-import android.view.GestureDetector;
-import android.view.MotionEvent;
-import android.view.ScaleGestureDetector;
-import android.view.View;
-
-import com.android.camera.debug.Log;
-import com.android.camera.ui.PieRenderer;
-import com.android.camera.ui.RenderOverlay;
-import com.android.camera.ui.ZoomRenderer;
-
-/* PreviewGestures disambiguates touch events received on RenderOverlay
- * and dispatch them to the proper recipient (i.e. zoom renderer or pie renderer).
- * Touch events on CameraControls will be handled by framework.
- * */
-public class PreviewGestures
- implements ScaleGestureDetector.OnScaleGestureListener {
-
- private static final Log.Tag TAG = new Log.Tag("PreviewGestures");
-
- private static final int MODE_NONE = 0;
- private static final int MODE_ZOOM = 2;
-
- public static final int DIR_UP = 0;
- public static final int DIR_DOWN = 1;
- public static final int DIR_LEFT = 2;
- public static final int DIR_RIGHT = 3;
-
- private final SingleTapListener mTapListener;
- private RenderOverlay mOverlay;
- private final PieRenderer mPie;
- private final ZoomRenderer mZoom;
- private MotionEvent mDown;
- private MotionEvent mCurrent;
- private final ScaleGestureDetector mScale;
- private int mMode;
- private boolean mZoomEnabled;
- private boolean mEnabled;
- private boolean mZoomOnly;
- private final GestureDetector mGestureDetector;
-
- private final GestureDetector.SimpleOnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
- @Override
- public void onLongPress (MotionEvent e) {
- // Open pie
- if (!mZoomOnly && mPie != null && !mPie.showsItems()) {
- openPie();
- }
- }
-
- @Override
- public boolean onSingleTapUp (MotionEvent e) {
- // Tap to focus when pie is not open
- if (mPie == null || !mPie.showsItems()) {
- mTapListener.onSingleTapUp(null, (int) e.getX(), (int) e.getY());
- return true;
- }
- return false;
- }
-
- @Override
- public boolean onScroll (MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
- if (e1 == null) {
- // e1 can be null if for some cases.
- return false;
- }
- if (mZoomOnly || mMode == MODE_ZOOM) return false;
- int deltaX = (int) (e1.getX() - e2.getX());
- int deltaY = (int) (e1.getY() - e2.getY());
- if (deltaY > 2 * deltaX && deltaY > -2 * deltaX) {
- // Open pie on swipe up
- if (mPie != null && !mPie.showsItems()) {
- openPie();
- return true;
- }
- }
- return false;
- }
- };
-
- public interface SingleTapListener {
- public void onSingleTapUp(View v, int x, int y);
- }
-
- public PreviewGestures(CameraActivity ctx, SingleTapListener tapListener,
- ZoomRenderer zoom, PieRenderer pie) {
- mTapListener = tapListener;
- mPie = pie;
- mZoom = zoom;
- mMode = MODE_NONE;
- mScale = new ScaleGestureDetector(ctx, this);
- mEnabled = true;
- mGestureDetector = new GestureDetector(mGestureListener);
- }
-
- public void setRenderOverlay(RenderOverlay overlay) {
- mOverlay = overlay;
- }
-
- public void setEnabled(boolean enabled) {
- mEnabled = enabled;
- }
-
- public void setZoomEnabled(boolean enable) {
- mZoomEnabled = enable;
- }
-
- public void setZoomOnly(boolean zoom) {
- mZoomOnly = zoom;
- }
-
- public boolean isEnabled() {
- return mEnabled;
- }
-
- public boolean dispatchTouch(MotionEvent m) {
- if (!mEnabled) {
- return false;
- }
- mCurrent = m;
- if (MotionEvent.ACTION_DOWN == m.getActionMasked()) {
- mMode = MODE_NONE;
- mDown = MotionEvent.obtain(m);
- }
-
- // If pie is open, redirects all the touch events to pie.
- if (mPie != null && mPie.isOpen()) {
- return sendToPie(m);
- }
-
- // If pie is not open, send touch events to gesture detector and scale
- // listener to recognize the gesture.
- mGestureDetector.onTouchEvent(m);
- if (mZoom != null) {
- mScale.onTouchEvent(m);
- if (MotionEvent.ACTION_POINTER_DOWN == m.getActionMasked()) {
- mMode = MODE_ZOOM;
- if (mZoomEnabled) {
- // Start showing zoom UI as soon as there is a second finger down
- mZoom.onScaleBegin(mScale);
- }
- } else if (MotionEvent.ACTION_POINTER_UP == m.getActionMasked()) {
- mZoom.onScaleEnd(mScale);
- }
- }
- return true;
- }
-
- private MotionEvent makeCancelEvent(MotionEvent m) {
- MotionEvent c = MotionEvent.obtain(m);
- c.setAction(MotionEvent.ACTION_CANCEL);
- return c;
- }
-
- private void openPie() {
- mGestureDetector.onTouchEvent(makeCancelEvent(mDown));
- mScale.onTouchEvent(makeCancelEvent(mDown));
- mOverlay.directDispatchTouch(mDown, mPie);
- }
-
- private boolean sendToPie(MotionEvent m) {
- return mOverlay.directDispatchTouch(m, mPie);
- }
-
- // OnScaleGestureListener implementation
- @Override
- public boolean onScale(ScaleGestureDetector detector) {
- return mZoom.onScale(detector);
- }
-
- @Override
- public boolean onScaleBegin(ScaleGestureDetector detector) {
- if (mPie == null || !mPie.isOpen()) {
- mMode = MODE_ZOOM;
- mGestureDetector.onTouchEvent(makeCancelEvent(mCurrent));
- if (!mZoomEnabled) return false;
- return mZoom.onScaleBegin(detector);
- }
- return false;
- }
-
- @Override
- public void onScaleEnd(ScaleGestureDetector detector) {
- mZoom.onScaleEnd(detector);
- }
-}
-
private static final int[] SOUND_RES = { // Soundtrack res IDs.
R.raw.focus_complete,
R.raw.video_record,
+ R.raw.video_record,
+ R.raw.shutter
};
// ID returned by load() should be non-zero.
private static final int ID_NOT_LOADED = 0;
// Maps a sound action to the id;
- private final int[] mSoundRes = {0, 1, 1, 1};
+ private final int[] mSoundRes = {0, 1, 2, 3};
// Store the context for lazy loading.
private Context mContext;
// mSoundPool is created every time load() is called and cleared every
}
if (!mIsVideoCaptureIntent) {
if (!mMediaRecorderRecording || mPaused || mSnapshotInProgress
- || !mAppController.isShutterEnabled()) {
+ || !mAppController.isShutterEnabled() || mCameraDevice == null) {
return;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void updateAutoFocusMoveCallback() {
- if (mPaused) {
+ if (mPaused || mCameraDevice == null) {
return;
}
@Override
public void onCameraAvailable(CameraProxy cameraProxy) {
+ if (cameraProxy == null) {
+ Log.w(TAG, "onCameraAvailable returns a null CameraProxy object");
+ return;
+ }
mCameraDevice = cameraProxy;
mCameraCapabilities = mCameraDevice.getCapabilities();
mCameraSettings = mCameraDevice.getSettings();
@Override
public void stopPreview() {
- if (!mPreviewing) {
+ if (!mPreviewing || mCameraDevice == null) {
return;
}
mCameraDevice.stopPreview();
// Prepares media recorder.
private void initializeRecorder() {
- Log.i(TAG, "initializeRecorder");
+ Log.i(TAG, "initializeRecorder: " + Thread.currentThread());
// If the mCameraDevice is null, then this activity is going to finish
if (mCameraDevice == null) {
return;
requestedSizeLimit = myExtras.getLong(MediaStore.EXTRA_SIZE_LIMIT);
}
mMediaRecorder = new MediaRecorder();
-
// Unlock the camera object before passing it to media recorder.
mCameraDevice.unlock();
mMediaRecorder.setCamera(mCameraDevice.getCamera());
}
private void startVideoRecording() {
- Log.i(TAG, "startVideoRecording");
+ Log.i(TAG, "startVideoRecording: " + Thread.currentThread());
mUI.cancelAnimations();
mUI.setSwipingEnabled(false);
mUI.showFocusUI(false);
if (bytes <= Storage.LOW_STORAGE_THRESHOLD_BYTES) {
Log.w(TAG, "Storage issue, ignore the start request");
} else {
- //??
- //if (!mCameraDevice.waitDone()) return;
+ if (mCameraDevice == null) {
+ Log.v(TAG, "in storage callback after camera closed");
+ return;
+ }
if (mPaused == true) {
Log.v(TAG, "in storage callback after module paused");
return;
}
+
+ // Monkey is so fast so it could trigger startVideoRecording twice. To prevent
+ // app crash (b/17313985), do nothing here for the second storage-checking
+ // callback because recording is already started.
+ if (mMediaRecorderRecording) {
+ Log.v(TAG, "in storage callback after recording started");
+ return;
+ }
+
mCurrentVideoUri = null;
initializeRecorder();
private boolean stopVideoRecording() {
Log.i(TAG, "stopVideoRecording");
+
+ // Do nothing if camera device is still capturing photo. Monkey test can trigger app crashes
+ // (b/17313985) without this check. Crash could also be reproduced by continuously tapping
+ // on shutter button and preview with two fingers.
+ if (mSnapshotInProgress) {
+ return true;
+ }
+
mUI.setSwipingEnabled(true);
mUI.showFocusUI(true);
mUI.showVideoRecordingHints(true);
mAppController.getCameraAppUI().showModeOptions();
mAppController.getCameraAppUI().animateBottomBarToFullSize(mShutterIconId);
- if (!mPaused) {
+ if (!mPaused && mCameraDevice != null) {
setFocusParameters();
mCameraDevice.lock();
if (!ApiHelper.HAS_SURFACE_TEXTURE_RECORDING) {
CameraProfile.QUALITY_HIGH);
mCameraSettings.setPhotoJpegCompressionQuality(jpegQuality);
- mCameraDevice.applySettings(mCameraSettings);
- // Nexus 5 through KitKat 4.4.2 requires a second call to
- // .setParameters() for frame rate settings to take effect.
- mCameraDevice.applySettings(mCameraSettings);
+ if (mCameraDevice != null) {
+ mCameraDevice.applySettings(mCameraSettings);
+ // Nexus 5 through KitKat 4.4.2 requires a second call to
+ // .setParameters() for frame rate settings to take effect.
+ mCameraDevice.applySettings(mCameraSettings);
+ }
// Update UI based on the new parameters.
mUI.updateOnScreenIndicators(mCameraSettings);
mParameters.setFlashMode(flashMode);
}
}*/
- mCameraDevice.applySettings(mCameraSettings);
+ if (mCameraDevice != null) {
+ mCameraDevice.applySettings(mCameraSettings);
+ }
mUI.updateOnScreenIndicators(mCameraSettings);
}
/***********************FocusOverlayManager Listener****************************/
@Override
public void autoFocus() {
- mCameraDevice.autoFocus(mHandler, mAutoFocusCallback);
+ if (mCameraDevice != null) {
+ mCameraDevice.autoFocus(mHandler, mAutoFocusCallback);
+ }
}
@Override
public void cancelAutoFocus() {
- mCameraDevice.cancelAutoFocus();
- setFocusParameters();
+ if (mCameraDevice != null) {
+ mCameraDevice.cancelAutoFocus();
+ setFocusParameters();
+ }
}
@Override
@Override
public void setFocusParameters() {
- updateFocusParameters();
- mCameraDevice.applySettings(mCameraSettings);
+ if (mCameraDevice != null) {
+ updateFocusParameters();
+ mCameraDevice.applySettings(mCameraSettings);
+ }
}
-
}
buttonManager.setToInitialState();
/** Standard mode options */
- if (hardwareSpec.isFrontCameraSupported()) {
+ if (mController.getCameraProvider().getNumberOfCameras() > 1 &&
+ hardwareSpec.isFrontCameraSupported()) {
if (bottomBarSpec.enableCamera) {
buttonManager.initializeButton(ButtonManager.BUTTON_CAMERA,
bottomBarSpec.cameraCallback);
protected final double mLongitude;
protected final Bundle mMetaData;
+ private static final int JPEG_COMPRESS_QUALITY = 90;
+ private static final BitmapEncoder JPEG_ENCODER =
+ new BitmapEncoder(Bitmap.CompressFormat.JPEG, JPEG_COMPRESS_QUALITY);
+
/**
* Used for thumbnail loading optimization. True if this data has a
* corresponding visible view.
private static final int mSupportedDataActions =
DATA_ACTION_DELETE | DATA_ACTION_EDIT | DATA_ACTION_SHARE;
- private static final int JPEG_COMPRESS_QUALITY = 90;
- private static final BitmapEncoder JPEG_ENCODER = new BitmapEncoder(null, JPEG_COMPRESS_QUALITY);
-
/** from MediaStore, can only be 0, 90, 180, 270 */
private final int mOrientation;
/** @see #getSignature() */
return;
}
- BitmapRequestBuilder<Uri, Bitmap> request = Glide.with(context)
- .loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds, mOrientation)
- .asBitmap()
- .encoder(JPEG_ENCODER)
- .placeholder(placeHolderResourceId)
- .fitCenter();
+ final int overrideWidth;
+ final int overrideHeight;
+ final BitmapRequestBuilder<Uri, Bitmap> thumbnailRequest;
if (full) {
- request.thumbnail(Glide.with(context)
- .loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds,
- mOrientation)
- .asBitmap()
- .encoder(JPEG_ENCODER)
- .override(thumbWidth, thumbHeight)
- .fitCenter())
- .override(Math.min(getWidth(), MAXIMUM_TEXTURE_SIZE),
- Math.min(getHeight(), MAXIMUM_TEXTURE_SIZE));
+ // Load up to the maximum size Bitmap we can render.
+ overrideWidth = Math.min(getWidth(), MAXIMUM_TEXTURE_SIZE);
+ overrideHeight = Math.min(getHeight(), MAXIMUM_TEXTURE_SIZE);
+
+ // Load two thumbnails, first the small low quality thumb from the media store,
+ // then a medium quality thumbWidth/thumbHeight image. Using two thumbnails ensures
+ // we don't flicker to grey while we load the maximum size image.
+ thumbnailRequest = loadUri(context)
+ .override(thumbWidth, thumbHeight)
+ .fitCenter()
+ .thumbnail(loadMediaStoreThumb(context));
} else {
- request.thumbnail(Glide.with(context)
- .loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds,
- mOrientation)
- .asBitmap()
- .encoder(JPEG_ENCODER)
- .override(MEDIASTORE_THUMB_WIDTH, MEDIASTORE_THUMB_HEIGHT))
- .override(thumbWidth, thumbHeight);
+ // Load a medium quality thumbWidth/thumbHeight image.
+ overrideWidth = thumbWidth;
+ overrideHeight = thumbHeight;
+
+ // Load a single small low quality thumbnail from the media store.
+ thumbnailRequest = loadMediaStoreThumb(context);
}
- request.into(imageView);
+
+ loadUri(context)
+ .placeholder(placeHolderResourceId)
+ .fitCenter()
+ .override(overrideWidth, overrideHeight)
+ .thumbnail(thumbnailRequest)
+ .into(imageView);
+ }
+
+ /** Loads a thumbnail with a size targeted to use MediaStore.Images.Thumbnails. */
+ private BitmapRequestBuilder<Uri, Bitmap> loadMediaStoreThumb(Context context) {
+ return loadUri(context)
+ .override(MEDIASTORE_THUMB_WIDTH, MEDIASTORE_THUMB_HEIGHT);
+ }
+
+ /** Loads an image using a MediaStore Uri with our default options. */
+ private BitmapRequestBuilder<Uri, Bitmap> loadUri(Context context) {
+ return Glide.with(context)
+ .loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds, mOrientation)
+ .asBitmap()
+ .encoder(JPEG_ENCODER);
}
@Override
Glide.with(context)
.loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds, 0)
+ .asBitmap()
+ .encoder(JPEG_ENCODER)
.thumbnail(Glide.with(context)
.loadFromMediaStore(getUri(), mMimeType, mDateModifiedInSeconds, 0)
+ .asBitmap()
+ .encoder(JPEG_ENCODER)
.override(MEDIASTORE_THUMB_WIDTH, MEDIASTORE_THUMB_HEIGHT))
.placeholder(placeHolderResourceId)
.fitCenter()
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;
+
import com.android.camera.Storage;
import com.android.camera2.R;
import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DecodeFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
int currentVersion = Storage.getJpegVersionForSession(mUri);
Glide.with(context)
.loadFromImage(jpegData, mUri.toString() + currentVersion)
+ .skipDiskCache(true)
.fitCenter()
.into(imageView);
* Called if opening the camera failed.
*/
public void onFailure();
+
+ /**
+ * Called if the camera is closed or disconnected while attempting to
+ * open.
+ */
+ public void onCameraClosed();
}
/**
/**
* Meters and triggers auto focus scan with ROI around tap point.
* <p/>
- * Normalized coordinates are referenced to portrait preview window with 0,0
- * top left and 1,1 bottom right. Rotation has no effect.
+ * Normalized coordinates are referenced to portrait preview window with
+ * (0, 0) top left and (1, 1) bottom right. Rotation has no effect.
*
* @param nx normalized x coordinate.
- * @param nx normalized y coordinate.
+ * @param ny normalized y coordinate.
*/
public void triggerFocusAndMeterAtPoint(float nx, float ny);
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
+import android.os.Build;
+import android.os.Handler;
import android.util.DisplayMetrics;
import android.view.WindowManager;
* Attempts to open the camera facing the given direction with the given
* capture size.
*
+ * Exactly one call will always be made to a single method in the provided
+ * {@link OpenCallback}.
+ *
* @param facing which camera to open. The first camera found in the given
* direction will be opened.
* @param enableHdr if an HDR feature exists, open a camera that supports it
* sizes.
* @param callback this listener is called when the camera was opened or
* when it failed to open.
+ * @param handler the handler on which callback methods are invoked.
*/
public abstract void open(Facing facing, boolean enableHdr, Size captureSize,
- OpenCallback callback);
+ OpenCallback callback, Handler handler);
/**
* Returns whether the device has a camera facing the given direction.
public abstract boolean hasCameraFacing(Facing facing);
/**
- * Singleton camera manager to be used throughout the app.
- */
- private static OneCameraManager sCameraManager;
-
- /**
- * Returns a camera manager that is based on Camera2 API, if available, or
+ * Creates a camera manager that is based on Camera2 API, if available, or
* otherwise uses the portability layer API.
- * <p>
- * The instance is created the first time this method is called and cached
- * in a singleton thereafter, so successive calls are cheap.
*/
public static OneCameraManager get(CameraActivity activity) {
- if (sCameraManager == null) {
- sCameraManager = create(activity);
- }
- return sCameraManager;
+ return create(activity);
}
/**
* HALs.
*/
private static boolean isCamera2Supported(CameraManager cameraManager) {
+ if (Build.VERSION.SDK_INT < 21) {
+ return false;
+ }
try {
final String id = cameraManager.getCameraIdList()[0];
// TODO: We should check for all the flags we need to ensure the
public class Settings3A {
/**
- * Width of touch AF region relative to shortest edge at 1.0 zoom.
- * Was 0.125 * longest edge prior to L release.
+ * Width of touch AF region in [0,1] relative to shorter edge of the current
+ * crop region. Multiply this number by the number of pixels along the
+ * shorter edge of the current crop region's width to get a value in pixels.
+ *
+ * <p>
+ * This value has been tested on Nexus 5 and Shamu, but will need to be
+ * tuned per device depending on how its ISP interprets the metering box and weight.
+ * </p>
+ *
+ * <p>
+ * Values prior to L release:
+ * Normal mode: 0.125 * longest edge
+ * Gcam: Fixed at 300px x 300px.
+ * </p>
*/
private static final float AF_REGION_BOX = 0.2f;
/**
- * Width of touch metering region relative to shortest edge at 1.0 zoom.
- * Larger than {@link #AF_REGION_BOX} because exposure is sensitive and it is
- * easy to over- or underexposure if area is too small.
+ * Width of touch metering region in [0,1] relative to shorter edge of the
+ * current crop region. Multiply this number by the number of pixels along
+ * shorter edge of the current crop region's width to get a value in pixels.
+ *
+ * <p>
+ * This value has been tested on Nexus 5 and Shamu, but will need to be
+ * tuned per device depending on how its ISP interprets the metering box and weight.
+ * </p>
+ *
+ * <p>
+ * Values prior to L release:
+ * Normal mode: 0.1875 * longest edge
+ * Gcam: Fixed at 300px x 300px.
+ * </p>
*/
private static final float AE_REGION_BOX = 0.3f;
- /** Metering region weight between 0 and 1. */
- private static final float REGION_WEIGHT = 0.25f;
+ /** Metering region weight between 0 and 1.
+ *
+ * <p>
+ * This value has been tested on Nexus 5 and Shamu, but will need to be
+ * tuned per device depending on how its ISP interprets the metering box and weight.
+ * </p>
+ */
+ private static final float REGION_WEIGHT = 0.022f;
/** Duration to hold after manual tap to focus. */
private static final int FOCUS_HOLD_MILLIS = 3000;
+ /**
+ * Width of touch metering region in [0,1] relative to shorter edge of the
+ * current crop region. Multiply this number by the number of pixels along
+ * shorter edge of the current crop region's width to get a value in pixels.
+ *
+ * <p>
+ * This value has been tested on Nexus 5 and Shamu, but will need to be
+ * tuned per device depending on how its ISP interprets the metering box and weight.
+ * </p>
+ *
+ * <p>
+ * Was fixed at 300px x 300px prior to L release.
+ * </p>
+ */
+ private static final float GCAM_METERING_REGION_FRACTION = 0.1225f;
+
+ /**
+ * Weight of a touch metering region, in [0, \inf).
+ *
+ * <p>
+ * This value has been tested on Nexus 5 and Shamu, but will need to be
+ * tuned per device.
+ * </p>
+ *
+ * <p>
+ * Was fixed at 15.0f prior to L release.
+ * </p>
+ */
+ private static final float GCAM_METERING_REGION_WEIGHT = 22.0f;
public static float getAutoFocusRegionWidth() {
return AF_REGION_BOX;
return REGION_WEIGHT;
}
+ public static float getGcamMeteringRegionFraction() {
+ return GCAM_METERING_REGION_FRACTION;
+ }
+
+ public static float getGcamMeteringRegionWeight() {
+ return GCAM_METERING_REGION_WEIGHT;
+ }
+
public static int getFocusHoldMillis() {
return FOCUS_HOLD_MILLIS;
}
package com.android.camera.one.v1;
+import android.os.Handler;
+
import com.android.camera.one.OneCamera.Facing;
import com.android.camera.one.OneCamera.OpenCallback;
import com.android.camera.one.OneCameraManager;
public class OneCameraManagerImpl extends OneCameraManager {
@Override
- public void open(Facing facing, boolean enableHdr, Size pictureSize, OpenCallback callback) {
+ public void open(Facing facing, boolean enableHdr, Size pictureSize, OpenCallback callback, Handler handler) {
throw new RuntimeException("Not implemented yet.");
}
package com.android.camera.one.v2;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureResult;
import com.android.camera.debug.Log;
import com.android.camera.one.OneCamera;
import com.android.camera.one.Settings3A;
+import com.android.camera.util.CameraUtil;
/**
* Helper class to implement auto focus and 3A in camera2-based
/** camera2 API metering region weight. */
private static final int CAMERA2_REGION_WEIGHT = (int)
- (((1 - Settings3A.getMeteringRegionWeight()) * MeteringRectangle.METERING_WEIGHT_MIN +
- Settings3A.getMeteringRegionWeight() * MeteringRectangle.METERING_WEIGHT_MAX));
+ (CameraUtil.lerp(MeteringRectangle.METERING_WEIGHT_MIN, MeteringRectangle.METERING_WEIGHT_MAX,
+ Settings3A.getMeteringRegionWeight()));
/** Zero weight 3A region, to reset regions per API. */
private static final MeteringRectangle[] ZERO_WEIGHT_3A_REGION = new MeteringRectangle[]{
));
}
- /** Compute 3A regions for a sensor-referenced touch coordinate. */
- private static MeteringRectangle[] regionsForSensorCoord(int xc, int yc, float width,
- Rect cropRegion) {
- float minCropEdge = (float) Math.min(cropRegion.width(), cropRegion.height());
- int delta = (int) (0.5 * width * minCropEdge);
- Rect region = new Rect(xc - delta, yc - delta, xc + delta, yc + delta);
- // Make sure region is inside the sensor area.
- if (!region.intersect(cropRegion)) {
- region = cropRegion;
- }
- return new MeteringRectangle[]{new MeteringRectangle(region, CAMERA2_REGION_WEIGHT)};
+ /** Compute 3A regions for a sensor-referenced touch coordinate.
+ * Returns a MeteringRectangle[] with length 1.
+ *
+ * @param nx x coordinate of the touch point, in normalized portrait coordinates.
+ * @param ny y coordinate of the touch point, in normalized portrait coordinates.
+ * @param fraction Fraction in [0,1]. Multiplied by min(cropRegion.width(), cropRegion.height())
+ * to determine the side length of the square MeteringRectangle.
+ * @param cropRegion Crop region of the image.
+ * @param sensorOrientation sensor orientation as defined by
+ * CameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION).
+ *
+ * */
+ private static MeteringRectangle[] regionsForNormalizedCoord(float nx, float ny, float fraction,
+ final Rect cropRegion, int sensorOrientation) {
+ // Compute half side length in pixels.
+ int minCropEdge = Math.min(cropRegion.width(), cropRegion.height());
+ int halfSideLength = (int) (0.5f * fraction * minCropEdge);
+
+ // Compute the output MeteringRectangle in sensor space.
+ // nx, ny is normalized to the screen.
+ // Crop region itself is specified in sensor coordinates.
+
+ // Normalized coordinates, now rotated into sensor space.
+ PointF nsc = CameraUtil.normalizedSensorCoordsForNormalizedDisplayCoords(
+ nx, ny, sensorOrientation);
+
+ int xCenterSensor = (int)(cropRegion.left + nsc.x * cropRegion.width());
+ int yCenterSensor = (int)(cropRegion.top + nsc.y * cropRegion.height());
+
+ Rect meteringRegion = new Rect(xCenterSensor - halfSideLength,
+ yCenterSensor - halfSideLength,
+ xCenterSensor + halfSideLength,
+ yCenterSensor + halfSideLength);
+
+ // Clamp meteringRegion to cropRegion.
+ meteringRegion.left = CameraUtil.clamp(meteringRegion.left, cropRegion.left, cropRegion.right);
+ meteringRegion.top = CameraUtil.clamp(meteringRegion.top, cropRegion.top, cropRegion.bottom);
+ meteringRegion.right = CameraUtil.clamp(meteringRegion.right, cropRegion.left, cropRegion.right);
+ meteringRegion.bottom = CameraUtil.clamp(meteringRegion.bottom, cropRegion.top, cropRegion.bottom);
+
+ return new MeteringRectangle[]{new MeteringRectangle(meteringRegion, CAMERA2_REGION_WEIGHT)};
}
/**
* Return AF region(s) for a sensor-referenced touch coordinate.
*
+ * <p>
+ * Normalized coordinates are referenced to portrait preview window with
+ * (0, 0) top left and (1, 1) bottom right. Rotation has no effect.
+ * </p>
+ *
* @return AF region(s).
*/
- public static MeteringRectangle[] afRegionsForSensorCoord(int xc, int yc, Rect cropRegion) {
- return regionsForSensorCoord(xc, yc, Settings3A.getAutoFocusRegionWidth(), cropRegion);
+ public static MeteringRectangle[] afRegionsForNormalizedCoord(float nx,
+ float ny, final Rect cropRegion, int sensorOrientation) {
+ return regionsForNormalizedCoord(nx, ny, Settings3A.getAutoFocusRegionWidth(),
+ cropRegion, sensorOrientation);
}
/**
* Return AE region(s) for a sensor-referenced touch coordinate.
*
+ * <p>
+ * Normalized coordinates are referenced to portrait preview window with
+ * (0, 0) top left and (1, 1) bottom right. Rotation has no effect.
+ * </p>
+ *
* @return AE region(s).
*/
- public static MeteringRectangle[] aeRegionsForSensorCoord(int xc, int yc, Rect cropRegion) {
- return regionsForSensorCoord(xc, yc, Settings3A.getMeteringRegionWidth(), cropRegion);
+ public static MeteringRectangle[] aeRegionsForNormalizedCoord(float nx,
+ float ny, final Rect cropRegion, int sensorOrientation) {
+ return regionsForNormalizedCoord(nx, ny, Settings3A.getMeteringRegionWidth(),
+ cropRegion, sensorOrientation);
}
/**
@Override
public void triggerFocusAndMeterAtPoint(float nx, float ny) {
- // xc, yc is center of tap point in sensor coordinate system.
- int xc = mCropRegion.left + (int) (mCropRegion.width() * ny);
- int yc = mCropRegion.top + (int) (mCropRegion.height() * (1f - nx));
-
- mAERegions = AutoFocusHelper.aeRegionsForSensorCoord(xc, yc, mCropRegion);
- mAFRegions = AutoFocusHelper.afRegionsForSensorCoord(xc, yc, mCropRegion);
+ int sensorOrientation = mCharacteristics.get(
+ CameraCharacteristics.SENSOR_ORIENTATION);
+ mAERegions = AutoFocusHelper.aeRegionsForNormalizedCoord(nx, ny, mCropRegion, sensorOrientation);
+ mAFRegions = AutoFocusHelper.afRegionsForNormalizedCoord(nx, ny, mCropRegion, sensorOrientation);
sendAutoFocusTriggerCaptureRequest(RequestTag.TAP_TO_FOCUS);
}
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
+import android.os.Handler;
import android.util.DisplayMetrics;
import com.android.camera.SoundPlayer;
@Override
public void open(Facing facing, final boolean useHdr, final Size pictureSize,
- final OpenCallback openCallback) {
+ final OpenCallback openCallback, Handler handler) {
try {
final String cameraId = getCameraId(facing);
Log.i(TAG, "Opening Camera ID " + cameraId);
mCameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
+ // We may get multiple calls to StateCallback, but only the
+ // first callback indicates the status of the camera-opening
+ // operation. For example, we may receive onOpened() and later
+ // onClosed(), but only the first should be relayed to
+ // openCallback.
+ private boolean isFirstCallback = true;
@Override
public void onDisconnected(CameraDevice device) {
- // TODO, Re-route through the camera instance?
+ if (isFirstCallback) {
+ isFirstCallback = false;
+ // If the camera is disconnected before it is opened
+ // then we must call close.
+ device.close();
+ openCallback.onCameraClosed();
+ }
+ }
+
+ @Override
+ public void onClosed(CameraDevice device) {
+ if (isFirstCallback) {
+ isFirstCallback = false;
+ openCallback.onCameraClosed();
+ }
}
@Override
public void onError(CameraDevice device, int error) {
- openCallback.onFailure();
+ if (isFirstCallback) {
+ isFirstCallback = false;
+ device.close();
+ openCallback.onFailure();
+ }
}
@Override
public void onOpened(CameraDevice device) {
- try {
- CameraCharacteristics characteristics = mCameraManager
- .getCameraCharacteristics(device.getId());
- // TODO: Set boolean based on whether HDR+ is enabled.
- OneCamera oneCamera = OneCameraCreator.create(mContext, useHdr, device,
- characteristics, pictureSize, mMaxMemoryMB, mDisplayMetrics,
- mSoundPlayer);
- openCallback.onCameraOpened(oneCamera);
- } catch (CameraAccessException e) {
- Log.d(TAG, "Could not get camera characteristics");
- openCallback.onFailure();
+ if (isFirstCallback) {
+ isFirstCallback = false;
+ try {
+ CameraCharacteristics characteristics = mCameraManager
+ .getCameraCharacteristics(device.getId());
+ // TODO: Set boolean based on whether HDR+ is enabled.
+ OneCamera oneCamera = OneCameraCreator.create(mContext, useHdr, device,
+ characteristics, pictureSize, mMaxMemoryMB, mDisplayMetrics,
+ mSoundPlayer);
+ openCallback.onCameraOpened(oneCamera);
+ } catch (CameraAccessException e) {
+ Log.d(TAG, "Could not get camera characteristics");
+ openCallback.onFailure();
+ }
}
}
- }, null);
+ }, handler);
} catch (CameraAccessException ex) {
Log.e(TAG, "Could not open camera. " + ex.getMessage());
- openCallback.onFailure();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ openCallback.onFailure();
+ }
+ });
} catch (UnsupportedOperationException ex) {
Log.e(TAG, "Could not open camera. " + ex.getMessage());
- openCallback.onFailure();
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ openCallback.onFailure();
+ }
+ });
}
}
*/
@Override
public void triggerFocusAndMeterAtPoint(float nx, float ny) {
- // xc, yc is center of tap point in sensor coordinate system.
- int xc = mCropRegion.left + (int) (mCropRegion.width() * ny);
- int yc = mCropRegion.top + (int) (mCropRegion.height() * (1f - nx));
-
- mAERegions = AutoFocusHelper.aeRegionsForSensorCoord(xc, yc, mCropRegion);
- mAFRegions = AutoFocusHelper.afRegionsForSensorCoord(xc, yc, mCropRegion);
+ int sensorOrientation = mCharacteristics.get(
+ CameraCharacteristics.SENSOR_ORIENTATION);
+ mAERegions = AutoFocusHelper.aeRegionsForNormalizedCoord(nx, ny, mCropRegion, sensorOrientation);
+ mAFRegions = AutoFocusHelper.afRegionsForNormalizedCoord(nx, ny, mCropRegion, sensorOrientation);
startAFCycle();
}
private class PeepholeAnimationEffect extends AnimationEffects {
private final static int UNSET = -1;
- private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 300;
+ private final static int PEEP_HOLE_ANIMATION_DURATION_MS = 500;
private final Paint mMaskPaint = new Paint();
private final RectF mBackgroundDrawArea = new RectF();
+++ /dev/null
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera.ui;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.view.MotionEvent;
-
-import com.android.camera.debug.Log;
-
-public abstract class OverlayRenderer implements RenderOverlay.Renderer {
-
- private static final Log.Tag TAG = new Log.Tag("OverlayRenderer");
- protected RenderOverlay mOverlay;
-
- protected int mLeft, mTop, mRight, mBottom;
-
- protected boolean mVisible;
-
- public void setVisible(boolean vis) {
- mVisible = vis;
- update();
- }
-
- public boolean isVisible() {
- return mVisible;
- }
-
- // default does not handle touch
- @Override
- public boolean handlesTouch() {
- return false;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent evt) {
- return false;
- }
-
- public abstract void onDraw(Canvas canvas);
-
- public void draw(Canvas canvas) {
- if (mVisible) {
- onDraw(canvas);
- }
- }
-
- @Override
- public void setOverlay(RenderOverlay overlay) {
- mOverlay = overlay;
- }
-
- @Override
- public void layout(int left, int top, int right, int bottom) {
- mLeft = left;
- mRight = right;
- mTop = top;
- mBottom = bottom;
- }
-
- protected Context getContext() {
- if (mOverlay != null) {
- return mOverlay.getContext();
- } else {
- return null;
- }
- }
-
- public int getWidth() {
- return mRight - mLeft;
- }
-
- public int getHeight() {
- return mBottom - mTop;
- }
-
- protected void update() {
- if (mOverlay != null) {
- mOverlay.update();
- }
- }
-
-}
+++ /dev/null
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.camera.ui;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Path;
-import android.graphics.drawable.Drawable;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Pie menu item
- */
-public class PieItem {
-
- public static interface OnClickListener {
- void onClick(PieItem item);
- }
-
- private Drawable mDrawable;
- private int level;
-
- private boolean mSelected;
- private boolean mEnabled;
- private List<PieItem> mItems;
- private Path mPath;
- private OnClickListener mOnClickListener;
- private float mAlpha;
- private CharSequence mLabel;
-
- // Gray out the view when disabled
- private static final float ENABLED_ALPHA = 1;
- private static final float DISABLED_ALPHA = (float) 0.3;
- private boolean mChangeAlphaWhenDisabled = true;
-
- public PieItem(Drawable drawable, int level) {
- mDrawable = drawable;
- this.level = level;
- if (drawable != null) {
- setAlpha(1f);
- }
- mEnabled = true;
- }
-
- public void setLabel(CharSequence txt) {
- mLabel = txt;
- }
-
- public CharSequence getLabel() {
- return mLabel;
- }
-
- public boolean hasItems() {
- return mItems != null;
- }
-
- public List<PieItem> getItems() {
- return mItems;
- }
-
- public void addItem(PieItem item) {
- if (mItems == null) {
- mItems = new ArrayList<PieItem>();
- }
- mItems.add(item);
- }
-
- public void clearItems() {
- mItems = null;
- }
-
- public void setLevel(int level) {
- this.level = level;
- }
-
- public void setPath(Path p) {
- mPath = p;
- }
-
- public Path getPath() {
- return mPath;
- }
-
- public void setChangeAlphaWhenDisabled (boolean enable) {
- mChangeAlphaWhenDisabled = enable;
- }
-
- public void setAlpha(float alpha) {
- mAlpha = alpha;
- mDrawable.setAlpha((int) (255 * alpha));
- }
-
- public void setEnabled(boolean enabled) {
- mEnabled = enabled;
- if (mChangeAlphaWhenDisabled) {
- if (mEnabled) {
- setAlpha(ENABLED_ALPHA);
- } else {
- setAlpha(DISABLED_ALPHA);
- }
- }
- }
-
- public boolean isEnabled() {
- return mEnabled;
- }
-
- public void setSelected(boolean s) {
- mSelected = s;
- }
-
- public boolean isSelected() {
- return mSelected;
- }
-
- public int getLevel() {
- return level;
- }
-
-
- public void setOnClickListener(OnClickListener listener) {
- mOnClickListener = listener;
- }
-
- public void performClick() {
- if (mOnClickListener != null) {
- mOnClickListener.onClick(this);
- }
- }
-
- public int getIntrinsicWidth() {
- return mDrawable.getIntrinsicWidth();
- }
-
- public int getIntrinsicHeight() {
- return mDrawable.getIntrinsicHeight();
- }
-
- public void setBounds(int left, int top, int right, int bottom) {
- mDrawable.setBounds(left, top, right, bottom);
- }
-
- public void draw(Canvas canvas) {
- mDrawable.draw(canvas);
- }
-
- public void setImageResource(Context context, int resId) {
- Drawable d = context.getResources().getDrawable(resId).mutate();
- d.setBounds(mDrawable.getBounds());
- mDrawable = d;
- setAlpha(mAlpha);
- }
-
-}
+++ /dev/null
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera.ui;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-
-public class PieMenuButton extends View {
- private boolean mPressed;
- private boolean mReadyToClick = false;
- public PieMenuButton(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
- mPressed = isPressed();
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- boolean handled = super.onTouchEvent(event);
- if (MotionEvent.ACTION_UP == event.getAction() && mPressed) {
- // Perform a customized click as soon as the ACTION_UP event
- // is received. The reason for doing this is that Framework
- // delays the performClick() call after ACTION_UP. But we do not
- // want the delay because it affects an important state change
- // for PieRenderer.
- mReadyToClick = true;
- performClick();
- }
- return handled;
- }
-
- @Override
- public boolean performClick() {
- if (mReadyToClick) {
- // We only respond to our customized click which happens right
- // after ACTION_UP event is received, with no delay.
- mReadyToClick = false;
- return super.performClick();
- }
- return false;
- }
-};
+++ /dev/null
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera.ui;
-
-import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.graphics.RectF;
-import android.os.Handler;
-import android.os.Message;
-import android.util.FloatMath;
-import android.view.MotionEvent;
-import android.view.ViewConfiguration;
-import android.view.animation.Animation;
-import android.view.animation.Transformation;
-
-import com.android.camera.debug.Log;
-import com.android.camera.drawable.TextDrawable;
-import com.android.camera2.R;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * An overlay renderer that is used to display focus state and progress state.
- */
-public class PieRenderer extends OverlayRenderer
- implements FocusIndicator {
-
- private static final Log.Tag TAG = new Log.Tag("PieRenderer");
-
- // Sometimes continuous autofocus starts and stops several times quickly.
- // These states are used to make sure the animation is run for at least some
- // time.
- private volatile int mState;
- private ScaleAnimation mAnimation = new ScaleAnimation();
- private static final int STATE_IDLE = 0;
- private static final int STATE_FOCUSING = 1;
- private static final int STATE_FINISHING = 2;
- private static final int STATE_PIE = 8;
-
- private static final float MATH_PI_2 = (float)(Math.PI / 2);
-
- private Runnable mDisappear = new Disappear();
- private Animation.AnimationListener mEndAction = new EndAction();
- private static final int SCALING_UP_TIME = 600;
- private static final int SCALING_DOWN_TIME = 100;
- private static final int DISAPPEAR_TIMEOUT = 200;
- private static final int DIAL_HORIZONTAL = 157;
- // fade out timings
- private static final int PIE_FADE_OUT_DURATION = 600;
-
- private static final long PIE_FADE_IN_DURATION = 200;
- private static final long PIE_XFADE_DURATION = 200;
- private static final long PIE_SELECT_FADE_DURATION = 300;
- private static final long PIE_OPEN_SUB_DELAY = 400;
- private static final long PIE_SLICE_DURATION = 80;
-
- private static final int MSG_OPEN = 0;
- private static final int MSG_CLOSE = 1;
- private static final int MSG_OPENSUBMENU = 2;
-
- protected static float CENTER = (float) Math.PI / 2;
- protected static float RAD24 = (float)(24 * Math.PI / 180);
- protected static final float SWEEP_SLICE = 0.14f;
- protected static final float SWEEP_ARC = 0.23f;
-
- // geometry
- private int mRadius;
- private int mRadiusInc;
-
- // the detection if touch is inside a slice is offset
- // inbounds by this amount to allow the selection to show before the
- // finger covers it
- private int mTouchOffset;
-
- private List<PieItem> mOpen;
-
- private Paint mSelectedPaint;
- private Paint mSubPaint;
- private Paint mMenuArcPaint;
-
- // touch handling
- private PieItem mCurrentItem;
-
- private Paint mFocusPaint;
- private int mSuccessColor;
- private int mFailColor;
- private int mCircleSize;
- private int mFocusX;
- private int mFocusY;
- private int mCenterX;
- private int mCenterY;
- private int mArcCenterY;
- private int mSliceCenterY;
- private int mPieCenterX;
- private int mPieCenterY;
- private int mSliceRadius;
- private int mArcRadius;
- private int mArcOffset;
-
- private int mDialAngle;
- private RectF mCircle;
- private RectF mDial;
- private Point mPoint1;
- private Point mPoint2;
- private int mStartAnimationAngle;
- private boolean mFocused;
- private int mInnerOffset;
- private int mOuterStroke;
- private int mInnerStroke;
- private boolean mTapMode;
- private boolean mBlockFocus;
- private int mTouchSlopSquared;
- private Point mDown;
- private boolean mOpening;
- private ValueAnimator mXFade;
- private ValueAnimator mFadeIn;
- private ValueAnimator mFadeOut;
- private ValueAnimator mSlice;
- private volatile boolean mFocusCancelled;
- private PointF mPolar = new PointF();
- private TextDrawable mLabel;
- private int mDeadZone;
- private int mAngleZone;
- private float mCenterAngle;
-
- private Handler mHandler = new Handler() {
- public void handleMessage(Message msg) {
- switch(msg.what) {
- case MSG_OPEN:
- if (mListener != null) {
- mListener.onPieOpened(mPieCenterX, mPieCenterY);
- }
- break;
- case MSG_CLOSE:
- if (mListener != null) {
- mListener.onPieClosed();
- }
- break;
- case MSG_OPENSUBMENU:
- onEnterOpen();
- break;
- }
-
- }
- };
-
- private PieListener mListener;
-
- static public interface PieListener {
- public void onPieOpened(int centerX, int centerY);
- public void onPieClosed();
- }
-
- public void setPieListener(PieListener pl) {
- mListener = pl;
- }
-
- public PieRenderer(Context context) {
- init(context);
- }
-
- private void init(Context ctx) {
- setVisible(false);
- mOpen = new ArrayList<PieItem>();
- mOpen.add(new PieItem(null, 0));
- Resources res = ctx.getResources();
- mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start);
- mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment);
- mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
- mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset);
- mSelectedPaint = new Paint();
- mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
- mSelectedPaint.setAntiAlias(true);
- mSubPaint = new Paint();
- mSubPaint.setAntiAlias(true);
- mSubPaint.setColor(Color.argb(200, 250, 230, 128));
- mFocusPaint = new Paint();
- mFocusPaint.setAntiAlias(true);
- mFocusPaint.setColor(Color.WHITE);
- mFocusPaint.setStyle(Paint.Style.STROKE);
- mSuccessColor = Color.GREEN;
- mFailColor = Color.RED;
- mCircle = new RectF();
- mDial = new RectF();
- mPoint1 = new Point();
- mPoint2 = new Point();
- mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
- mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
- mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
- mState = STATE_IDLE;
- mBlockFocus = false;
- mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
- mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
- mDown = new Point();
- mMenuArcPaint = new Paint();
- mMenuArcPaint.setAntiAlias(true);
- mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255));
- mMenuArcPaint.setStrokeWidth(10);
- mMenuArcPaint.setStyle(Paint.Style.STROKE);
- mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius);
- mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius);
- mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset);
- mLabel = new TextDrawable(res);
- mLabel.setDropShadow(true);
- mDeadZone = res.getDimensionPixelSize(R.dimen.pie_deadzone_width);
- mAngleZone = res.getDimensionPixelSize(R.dimen.pie_anglezone_width);
- }
-
- private PieItem getRoot() {
- return mOpen.get(0);
- }
-
- public boolean showsItems() {
- return mTapMode;
- }
-
- public void addItem(PieItem item) {
- // add the item to the pie itself
- getRoot().addItem(item);
- }
-
- public void clearItems() {
- getRoot().clearItems();
- }
-
- public void showInCenter() {
- if ((mState == STATE_PIE) && isVisible()) {
- mTapMode = false;
- show(false);
- } else {
- if (mState != STATE_IDLE) {
- cancelFocus();
- }
- mState = STATE_PIE;
- resetPieCenter();
- setCenter(mPieCenterX, mPieCenterY);
- mTapMode = true;
- show(true);
- }
- }
-
- public void hide() {
- show(false);
- }
-
- /**
- * guaranteed has center set
- * @param show
- */
- private void show(boolean show) {
- if (show) {
- if (mXFade != null) {
- mXFade.cancel();
- }
- mState = STATE_PIE;
- // ensure clean state
- mCurrentItem = null;
- PieItem root = getRoot();
- for (PieItem openItem : mOpen) {
- if (openItem.hasItems()) {
- for (PieItem item : openItem.getItems()) {
- item.setSelected(false);
- }
- }
- }
- mLabel.setText("");
- mOpen.clear();
- mOpen.add(root);
- layoutPie();
- fadeIn();
- } else {
- mState = STATE_IDLE;
- mTapMode = false;
- if (mXFade != null) {
- mXFade.cancel();
- }
- if (mLabel != null) {
- mLabel.setText("");
- }
- }
- setVisible(show);
- mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
- }
-
- public boolean isOpen() {
- return mState == STATE_PIE && isVisible();
- }
-
- private void fadeIn() {
- mFadeIn = new ValueAnimator();
- mFadeIn.setFloatValues(0f, 1f);
- mFadeIn.setDuration(PIE_FADE_IN_DURATION);
- // linear interpolation
- mFadeIn.setInterpolator(null);
- mFadeIn.addListener(new AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) {
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- mFadeIn = null;
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) {
- }
-
- @Override
- public void onAnimationCancel(Animator arg0) {
- }
- });
- mFadeIn.start();
- }
-
- public void setCenter(int x, int y) {
- mPieCenterX = x;
- mPieCenterY = y;
- mSliceCenterY = y + mSliceRadius - mArcOffset;
- mArcCenterY = y - mArcOffset + mArcRadius;
- }
-
- @Override
- public void layout(int l, int t, int r, int b) {
- super.layout(l, t, r, b);
- mCenterX = (r - l) / 2;
- mCenterY = (b - t) / 2;
-
- mFocusX = mCenterX;
- mFocusY = mCenterY;
- resetPieCenter();
- setCircle(mFocusX, mFocusY);
- if (isVisible() && mState == STATE_PIE) {
- setCenter(mPieCenterX, mPieCenterY);
- layoutPie();
- }
- }
-
- private void resetPieCenter() {
- mPieCenterX = mCenterX;
- mPieCenterY = (int) (getHeight() - 2.5f * mDeadZone);
- }
-
- private void layoutPie() {
- mCenterAngle = getCenterAngle();
- layoutItems(0, getRoot().getItems());
- layoutLabel(getLevel());
- }
-
- private void layoutLabel(int level) {
- int x = mPieCenterX - (int) (FloatMath.sin(mCenterAngle - CENTER)
- * (mArcRadius + (level + 2) * mRadiusInc));
- int y = mArcCenterY - mArcRadius - (level + 2) * mRadiusInc;
- int w = mLabel.getIntrinsicWidth();
- int h = mLabel.getIntrinsicHeight();
- mLabel.setBounds(x - w/2, y - h/2, x + w/2, y + h/2);
- }
-
- private void layoutItems(int level, List<PieItem> items) {
- int extend = 1;
- Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend,
- mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4,
- mPieCenterX, mArcCenterY - level * mRadiusInc);
- final int count = items.size();
- int pos = 0;
- for (PieItem item : items) {
- // shared between items
- item.setPath(path);
- float angle = getArcCenter(item, pos, count);
- int w = item.getIntrinsicWidth();
- int h = item.getIntrinsicHeight();
- // move views to outer border
- int r = mArcRadius + mRadiusInc * 2 / 3;
- int x = (int) (r * Math.cos(angle));
- int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2;
- x = mPieCenterX + x - w / 2;
- item.setBounds(x, y, x + w, y + h);
- item.setLevel(level);
- if (item.hasItems()) {
- layoutItems(level + 1, item.getItems());
- }
- pos++;
- }
- }
-
- private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) {
- RectF bb =
- new RectF(cx - outer, cy - outer, cx + outer,
- cy + outer);
- RectF bbi =
- new RectF(cx - inner, cy - inner, cx + inner,
- cy + inner);
- Path path = new Path();
- path.arcTo(bb, start, end - start, true);
- path.arcTo(bbi, end, start - end);
- path.close();
- return path;
- }
-
- private float getArcCenter(PieItem item, int pos, int count) {
- return getCenter(pos, count, SWEEP_ARC);
- }
-
- private float getSliceCenter(PieItem item, int pos, int count) {
- float center = (getCenterAngle() - CENTER) * 0.5f + CENTER;
- return center + (count - 1) * SWEEP_SLICE / 2f
- - pos * SWEEP_SLICE;
- }
-
- private float getCenter(int pos, int count, float sweep) {
- return mCenterAngle + (count - 1) * sweep / 2f - pos * sweep;
- }
-
- private float getCenterAngle() {
- float center = CENTER;
- if (mPieCenterX < mDeadZone + mAngleZone) {
- center = CENTER - (mAngleZone - mPieCenterX + mDeadZone) * RAD24
- / (float) mAngleZone;
- } else if (mPieCenterX > getWidth() - mDeadZone - mAngleZone) {
- center = CENTER + (mPieCenterX - (getWidth() - mDeadZone - mAngleZone)) * RAD24
- / (float) mAngleZone;
- }
- return center;
- }
-
- /**
- * converts a
- * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
- * @return skia angle
- */
- private float getDegrees(double angle) {
- return (float) (360 - 180 * angle / Math.PI);
- }
-
- private void startFadeOut(final PieItem item) {
- if (mFadeIn != null) {
- mFadeIn.cancel();
- }
- if (mXFade != null) {
- mXFade.cancel();
- }
- mFadeOut = new ValueAnimator();
- mFadeOut.setFloatValues(1f, 0f);
- mFadeOut.setDuration(PIE_FADE_OUT_DURATION);
- mFadeOut.addListener(new AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animator) {
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- item.performClick();
- mFadeOut = null;
- deselect();
- show(false);
- mOverlay.setAlpha(1);
- }
-
- @Override
- public void onAnimationRepeat(Animator animator) {
- }
-
- @Override
- public void onAnimationCancel(Animator animator) {
- }
-
- });
- mFadeOut.start();
- }
-
- // root does not count
- private boolean hasOpenItem() {
- return mOpen.size() > 1;
- }
-
- // pop an item of the open item stack
- private PieItem closeOpenItem() {
- PieItem item = getOpenItem();
- mOpen.remove(mOpen.size() -1);
- return item;
- }
-
- private PieItem getOpenItem() {
- return mOpen.get(mOpen.size() - 1);
- }
-
- // return the children either the root or parent of the current open item
- private PieItem getParent() {
- return mOpen.get(Math.max(0, mOpen.size() - 2));
- }
-
- private int getLevel() {
- return mOpen.size() - 1;
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- float alpha = 1;
- if (mXFade != null) {
- alpha = (Float) mXFade.getAnimatedValue();
- } else if (mFadeIn != null) {
- alpha = (Float) mFadeIn.getAnimatedValue();
- } else if (mFadeOut != null) {
- alpha = (Float) mFadeOut.getAnimatedValue();
- }
- int state = canvas.save();
- if (mFadeIn != null) {
- float sf = 0.9f + alpha * 0.1f;
- canvas.scale(sf, sf, mPieCenterX, mPieCenterY);
- }
- if (mState != STATE_PIE) {
- drawFocus(canvas);
- }
- if (mState == STATE_FINISHING) {
- canvas.restoreToCount(state);
- return;
- }
- if (mState != STATE_PIE) return;
- if (!hasOpenItem() || (mXFade != null)) {
- // draw base menu
- drawArc(canvas, getLevel(), getParent());
- List<PieItem> items = getParent().getItems();
- final int count = items.size();
- int pos = 0;
- for (PieItem item : getParent().getItems()) {
- drawItem(Math.max(0, mOpen.size() - 2), pos, count, canvas, item, alpha);
- pos++;
- }
- mLabel.draw(canvas);
- }
- if (hasOpenItem()) {
- int level = getLevel();
- drawArc(canvas, level, getOpenItem());
- List<PieItem> items = getOpenItem().getItems();
- final int count = items.size();
- int pos = 0;
- for (PieItem inner : items) {
- if (mFadeOut != null) {
- drawItem(level, pos, count, canvas, inner, alpha);
- } else {
- drawItem(level, pos, count, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
- }
- pos++;
- }
- mLabel.draw(canvas);
- }
- canvas.restoreToCount(state);
- }
-
- private void drawArc(Canvas canvas, int level, PieItem item) {
- // arc
- if (mState == STATE_PIE) {
- final int count = item.getItems().size();
- float start = mCenterAngle + (count * SWEEP_ARC / 2f);
- float end = mCenterAngle - (count * SWEEP_ARC / 2f);
- int cy = mArcCenterY - level * mRadiusInc;
- canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius,
- mPieCenterX + mArcRadius, cy + mArcRadius),
- getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint);
- }
- }
-
- private void drawItem(int level, int pos, int count, Canvas canvas, PieItem item, float alpha) {
- if (mState == STATE_PIE) {
- if (item.getPath() != null) {
- int y = mArcCenterY - level * mRadiusInc;
- if (item.isSelected()) {
- Paint p = mSelectedPaint;
- int state = canvas.save();
- float angle = 0;
- if (mSlice != null) {
- angle = (Float) mSlice.getAnimatedValue();
- } else {
- angle = getArcCenter(item, pos, count) - SWEEP_ARC / 2f;
- }
- angle = getDegrees(angle);
- canvas.rotate(angle, mPieCenterX, y);
- if (mFadeOut != null) {
- p.setAlpha((int)(255 * alpha));
- }
- canvas.drawPath(item.getPath(), p);
- if (mFadeOut != null) {
- p.setAlpha(255);
- }
- canvas.restoreToCount(state);
- }
- if (mFadeOut == null) {
- alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
- // draw the item view
- item.setAlpha(alpha);
- }
- item.draw(canvas);
- }
- }
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent evt) {
- float x = evt.getX();
- float y = evt.getY();
- int action = evt.getActionMasked();
- getPolar(x, y, !mTapMode, mPolar);
- if (MotionEvent.ACTION_DOWN == action) {
- if ((x < mDeadZone) || (x > getWidth() - mDeadZone)) {
- return false;
- }
- mDown.x = (int) evt.getX();
- mDown.y = (int) evt.getY();
- mOpening = false;
- if (mTapMode) {
- PieItem item = findItem(mPolar);
- if ((item != null) && (mCurrentItem != item)) {
- mState = STATE_PIE;
- onEnter(item);
- }
- } else {
- setCenter((int) x, (int) y);
- show(true);
- }
- return true;
- } else if (MotionEvent.ACTION_UP == action) {
- if (isVisible()) {
- PieItem item = mCurrentItem;
- if (mTapMode) {
- item = findItem(mPolar);
- if (mOpening) {
- mOpening = false;
- return true;
- }
- }
- if (item == null) {
- mTapMode = false;
- show(false);
- } else if (!mOpening && !item.hasItems()) {
- startFadeOut(item);
- mTapMode = false;
- } else {
- mTapMode = true;
- }
- return true;
- }
- } else if (MotionEvent.ACTION_CANCEL == action) {
- if (isVisible() || mTapMode) {
- show(false);
- }
- deselect();
- mHandler.removeMessages(MSG_OPENSUBMENU);
- return false;
- } else if (MotionEvent.ACTION_MOVE == action) {
- if (pulledToCenter(mPolar)) {
- mHandler.removeMessages(MSG_OPENSUBMENU);
- if (hasOpenItem()) {
- if (mCurrentItem != null) {
- mCurrentItem.setSelected(false);
- }
- closeOpenItem();
- mCurrentItem = null;
- } else {
- deselect();
- }
- mLabel.setText("");
- return false;
- }
- PieItem item = findItem(mPolar);
- boolean moved = hasMoved(evt);
- if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
- mHandler.removeMessages(MSG_OPENSUBMENU);
- // only select if we didn't just open or have moved past slop
- if (moved) {
- // switch back to swipe mode
- mTapMode = false;
- }
- onEnterSelect(item);
- mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY);
- }
- }
- return false;
- }
-
- @Override
- public boolean isVisible() {
- return super.isVisible();
- }
-
- private boolean pulledToCenter(PointF polarCoords) {
- return polarCoords.y < mArcRadius - mRadiusInc;
- }
-
- private boolean inside(PointF polar, PieItem item, int pos, int count) {
- float start = getSliceCenter(item, pos, count) - SWEEP_SLICE / 2f;
- boolean res = (mArcRadius < polar.y)
- && (start < polar.x)
- && (start + SWEEP_SLICE > polar.x)
- && (!mTapMode || (mArcRadius + mRadiusInc > polar.y));
- return res;
- }
-
- private void getPolar(float x, float y, boolean useOffset, PointF res) {
- // get angle and radius from x/y
- res.x = (float) Math.PI / 2;
- x = x - mPieCenterX;
- float y1 = mSliceCenterY - getLevel() * mRadiusInc - y;
- float y2 = mArcCenterY - getLevel() * mRadiusInc - y;
- res.y = (float) Math.sqrt(x * x + y2 * y2);
- if (x != 0) {
- res.x = (float) Math.atan2(y1, x);
- if (res.x < 0) {
- res.x = (float) (2 * Math.PI + res.x);
- }
- }
- res.y = res.y + (useOffset ? mTouchOffset : 0);
- }
-
- private boolean hasMoved(MotionEvent e) {
- return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x)
- + (e.getY() - mDown.y) * (e.getY() - mDown.y);
- }
-
- private void onEnterSelect(PieItem item) {
- if (mCurrentItem != null) {
- mCurrentItem.setSelected(false);
- }
- if (item != null && item.isEnabled()) {
- moveSelection(mCurrentItem, item);
- item.setSelected(true);
- mCurrentItem = item;
- mLabel.setText(mCurrentItem.getLabel());
- layoutLabel(getLevel());
- } else {
- mCurrentItem = null;
- }
- }
-
- private void onEnterOpen() {
- if ((mCurrentItem != null) && (mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
- openCurrentItem();
- }
- }
-
- /**
- * enter a slice for a view
- * updates model only
- * @param item
- */
- private void onEnter(PieItem item) {
- if (mCurrentItem != null) {
- mCurrentItem.setSelected(false);
- }
- if (item != null && item.isEnabled()) {
- item.setSelected(true);
- mCurrentItem = item;
- mLabel.setText(mCurrentItem.getLabel());
- if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) {
- openCurrentItem();
- layoutLabel(getLevel());
- }
- } else {
- mCurrentItem = null;
- }
- }
-
- private void deselect() {
- if (mCurrentItem != null) {
- mCurrentItem.setSelected(false);
- }
- if (hasOpenItem()) {
- PieItem item = closeOpenItem();
- onEnter(item);
- } else {
- mCurrentItem = null;
- }
- }
-
- private int getItemPos(PieItem target) {
- List<PieItem> items = getOpenItem().getItems();
- return items.indexOf(target);
- }
-
- private int getCurrentCount() {
- return getOpenItem().getItems().size();
- }
-
- private void moveSelection(PieItem from, PieItem to) {
- final int count = getCurrentCount();
- final int fromPos = getItemPos(from);
- final int toPos = getItemPos(to);
- if (fromPos != -1 && toPos != -1) {
- float startAngle = getArcCenter(from, getItemPos(from), count)
- - SWEEP_ARC / 2f;
- float endAngle = getArcCenter(to, getItemPos(to), count)
- - SWEEP_ARC / 2f;
- mSlice = new ValueAnimator();
- mSlice.setFloatValues(startAngle, endAngle);
- // linear interpolater
- mSlice.setInterpolator(null);
- mSlice.setDuration(PIE_SLICE_DURATION);
- mSlice.addListener(new AnimatorListener() {
- @Override
- public void onAnimationEnd(Animator arg0) {
- mSlice = null;
- }
-
- @Override
- public void onAnimationRepeat(Animator arg0) {
- }
-
- @Override
- public void onAnimationStart(Animator arg0) {
- }
-
- @Override
- public void onAnimationCancel(Animator arg0) {
- }
- });
- mSlice.start();
- }
- }
-
- private void openCurrentItem() {
- if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
- mOpen.add(mCurrentItem);
- layoutLabel(getLevel());
- mOpening = true;
- if (mFadeIn != null) {
- mFadeIn.cancel();
- }
- mXFade = new ValueAnimator();
- mXFade.setFloatValues(1f, 0f);
- mXFade.setDuration(PIE_XFADE_DURATION);
- // Linear interpolation
- mXFade.setInterpolator(null);
- final PieItem ci = mCurrentItem;
- mXFade.addListener(new AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animation) {
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- mXFade = null;
- ci.setSelected(false);
- mOpening = false;
- }
-
- @Override
- public void onAnimationRepeat(Animator animation) {
- }
-
- @Override
- public void onAnimationCancel(Animator arg0) {
- }
- });
- mXFade.start();
- }
- }
-
- /**
- * @param polar x: angle, y: dist
- * @return the item at angle/dist or null
- */
- private PieItem findItem(PointF polar) {
- // find the matching item:
- List<PieItem> items = getOpenItem().getItems();
- final int count = items.size();
- int pos = 0;
- for (PieItem item : items) {
- if (inside(polar, item, pos, count)) {
- return item;
- }
- pos++;
- }
- return null;
- }
-
-
- @Override
- public boolean handlesTouch() {
- return true;
- }
-
- // focus specific code
-
- public void setBlockFocus(boolean blocked) {
- mBlockFocus = blocked;
- if (blocked) {
- clear();
- }
- }
-
- public void setFocus(int x, int y) {
- mOverlay.removeCallbacks(mDisappear);
- mFocusX = x;
- mFocusY = y;
- setCircle(mFocusX, mFocusY);
- }
-
- public int getSize() {
- return 2 * mCircleSize;
- }
-
- private int getRandomRange() {
- return (int)(-60 + 120 * Math.random());
- }
-
- private void setCircle(int cx, int cy) {
- mCircle.set(cx - mCircleSize, cy - mCircleSize,
- cx + mCircleSize, cy + mCircleSize);
- mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset,
- cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset);
- }
-
- public void drawFocus(Canvas canvas) {
- if (mBlockFocus) return;
- mFocusPaint.setStrokeWidth(mOuterStroke);
- canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
- if (mState == STATE_PIE) return;
- int color = mFocusPaint.getColor();
- if (mState == STATE_FINISHING) {
- mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
- }
- mFocusPaint.setStrokeWidth(mInnerStroke);
- drawLine(canvas, mDialAngle, mFocusPaint);
- drawLine(canvas, mDialAngle + 45, mFocusPaint);
- drawLine(canvas, mDialAngle + 180, mFocusPaint);
- drawLine(canvas, mDialAngle + 225, mFocusPaint);
- canvas.save();
- // rotate the arc instead of its offset to better use framework's shape caching
- canvas.rotate(mDialAngle, mFocusX, mFocusY);
- canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
- canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
- canvas.restore();
- mFocusPaint.setColor(color);
- }
-
- private void drawLine(Canvas canvas, int angle, Paint p) {
- convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
- convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
- canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY,
- mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
- }
-
- private static void convertCart(int angle, int radius, Point out) {
- double a = 2 * Math.PI * (angle % 360) / 360;
- out.x = (int) (radius * Math.cos(a) + 0.5);
- out.y = (int) (radius * Math.sin(a) + 0.5);
- }
-
- @Override
- public void showStart() {
- if (mState == STATE_PIE) return;
- cancelFocus();
- mStartAnimationAngle = 67;
- int range = getRandomRange();
- startAnimation(SCALING_UP_TIME,
- false, mStartAnimationAngle, mStartAnimationAngle + range);
- mState = STATE_FOCUSING;
- }
-
- @Override
- public void showSuccess(boolean timeout) {
- if (mState == STATE_FOCUSING) {
- startAnimation(SCALING_DOWN_TIME,
- timeout, mStartAnimationAngle);
- mState = STATE_FINISHING;
- mFocused = true;
- }
- }
-
- @Override
- public void showFail(boolean timeout) {
- if (mState == STATE_FOCUSING) {
- startAnimation(SCALING_DOWN_TIME,
- timeout, mStartAnimationAngle);
- mState = STATE_FINISHING;
- mFocused = false;
- }
- }
-
- private void cancelFocus() {
- mFocusCancelled = true;
- mOverlay.removeCallbacks(mDisappear);
- if (mAnimation != null && !mAnimation.hasEnded()) {
- mAnimation.cancel();
- }
- mFocusCancelled = false;
- mFocused = false;
- mState = STATE_IDLE;
- }
-
- public void clear(boolean waitUntilProgressIsHidden) {
- if (mState == STATE_PIE)
- return;
- cancelFocus();
- mOverlay.post(mDisappear);
- }
-
- @Override
- public void clear() {
- clear(false);
- }
-
- private void startAnimation(long duration, boolean timeout,
- float toScale) {
- startAnimation(duration, timeout, mDialAngle,
- toScale);
- }
-
- private void startAnimation(long duration, boolean timeout,
- float fromScale, float toScale) {
- setVisible(true);
- mAnimation.reset();
- mAnimation.setDuration(duration);
- mAnimation.setScale(fromScale, toScale);
- mAnimation.setAnimationListener(timeout ? mEndAction : null);
- mOverlay.startAnimation(mAnimation);
- update();
- }
-
- private class EndAction implements Animation.AnimationListener {
- @Override
- public void onAnimationEnd(Animation animation) {
- // Keep the focus indicator for some time.
- if (!mFocusCancelled && mOverlay != null) {
- mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
- }
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) {
- }
-
- @Override
- public void onAnimationStart(Animation animation) {
- }
- }
-
- private class Disappear implements Runnable {
- @Override
- public void run() {
- if (mState == STATE_PIE) return;
- setVisible(false);
- mFocusX = mCenterX;
- mFocusY = mCenterY;
- mState = STATE_IDLE;
- setCircle(mFocusX, mFocusY);
- mFocused = false;
- }
- }
-
- private class ScaleAnimation extends Animation {
- private float mFrom = 1f;
- private float mTo = 1f;
-
- public ScaleAnimation() {
- setFillAfter(true);
- }
-
- public void setScale(float from, float to) {
- mFrom = from;
- mTo = to;
- }
-
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime);
- }
- }
-
-}
if (ev.getPointerCount() > 1) {
mDeltaX = ev.getX(1) - ev.getX(0);
mDeltaY = ev.getY(1) - ev.getY(0);
- if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
- if (!mZoomProcessor.isVisible()) {
- mZoomProcessor.showZoomUI();
- }
- } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
- if (mZoomProcessor.isVisible()) {
- mZoomProcessor.hideZoomUI();
- }
- }
}
return handled;
}
public boolean onScale(ScaleGestureDetector detector) {
final float sf = detector.getScaleFactor();
mCurrentRatio = (0.33f + mCurrentRatio) * sf * sf - 0.33f;
- if (mCurrentRatio < mMinRatio) mCurrentRatio = mMinRatio;
- if (mCurrentRatio > mMaxRatio) mCurrentRatio = mMaxRatio;
+ if (mCurrentRatio < mMinRatio) {
+ mCurrentRatio = mMinRatio;
+ }
+ if (mCurrentRatio > mMaxRatio) {
+ mCurrentRatio = mMaxRatio;
+ }
// Only call the listener with a certain frequency. This is
// necessary because these listeners will make repeated
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
+ mZoomProcessor.showZoomUI();
if (mZoomListener == null) {
return false;
}
- mVisible = true;
if (mZoomListener != null) {
mZoomListener.onZoomStart();
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
- mVisible = false;
+ mZoomProcessor.hideZoomUI();
if (mZoomListener != null) {
mZoomListener.onZoomEnd();
}
+++ /dev/null
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera.ui;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.FrameLayout;
-
-import com.android.camera.PreviewGestures;
-import com.android.camera.debug.Log;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class RenderOverlay extends FrameLayout {
-
- private static final Log.Tag TAG = new Log.Tag("RenderOverlay");
- private PreviewGestures.SingleTapListener mTapListener;
-
- interface Renderer {
-
- public boolean handlesTouch();
- public boolean onTouchEvent(MotionEvent evt);
- public void setOverlay(RenderOverlay overlay);
- public void layout(int left, int top, int right, int bottom);
- public void draw(Canvas canvas);
-
- }
-
- private final RenderView mRenderView;
- private final List<Renderer> mClients;
- private PreviewGestures mGestures;
- // reverse list of touch clients
- private final List<Renderer> mTouchClients;
- private final int[] mPosition = new int[2];
-
- public RenderOverlay(Context context, AttributeSet attrs) {
- super(context, attrs);
- mRenderView = new RenderView(context);
- addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT,
- LayoutParams.MATCH_PARENT));
- mClients = new ArrayList<Renderer>(10);
- mTouchClients = new ArrayList<Renderer>(10);
- setWillNotDraw(false);
- }
-
- public void setGestures(PreviewGestures gestures) {
- mGestures = gestures;
- }
-
- public void addRenderer(Renderer renderer) {
- mClients.add(renderer);
- renderer.setOverlay(this);
- if (renderer.handlesTouch()) {
- mTouchClients.add(0, renderer);
- }
- renderer.layout(getLeft(), getTop(), getRight(), getBottom());
- }
-
- public void addRenderer(int pos, Renderer renderer) {
- mClients.add(pos, renderer);
- renderer.setOverlay(this);
- renderer.layout(getLeft(), getTop(), getRight(), getBottom());
- }
-
- public void remove(Renderer renderer) {
- mClients.remove(renderer);
- renderer.setOverlay(null);
- }
-
- public int getClientSize() {
- return mClients.size();
- }
-
- // TODO: Remove this when refactor is done. This is only here temporarily
- // to keep pie working before it's replaced with bottom bar.
- public void directTouchEventsToPie(MotionEvent ev) {
- PieRenderer pie = null;
- for (int i = 0; i < mClients.size(); i++) {
- if (mClients.get(i) instanceof PieRenderer) {
- pie = (PieRenderer) mClients.get(i);
- break;
- }
- }
- if (pie!= null && pie.isOpen()) {
- pie.onTouchEvent(ev);
- } else {
- if (mTapListener != null && ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
- mTapListener.onSingleTapUp(null, ((int) ev.getX()), ((int) ev.getY()));
- }
- }
- }
-
- // TODO: Remove this when refactor is done. This is only here temporarily
- // to keep pie working before it's replaced with bottom bar.
- public void clear() {
- mGestures = null;
- while (mClients.size() > 0) {
- remove(mClients.get(0));
- }
- mTouchClients.clear();
- mTapListener = null;
- }
-
- // TODO: Design a touch flow for each module to handle the leftover touch
- // events from the app
- public void setTapListener(PreviewGestures.SingleTapListener listener) {
- mTapListener = listener;
- }
-
-
- // Only Gcam is using this right now, which is only using the pie
- // menu for the capture progress.
- // TODO: migrate all modes to PreviewOverlay.
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- if (mTapListener != null && ev.getActionMasked() == MotionEvent.ACTION_UP) {
- mTapListener.onSingleTapUp(null, ((int) ev.getX()), ((int) ev.getY()));
- }
- return true;
- }
-
- public boolean directDispatchTouch(MotionEvent m, Renderer target) {
- mRenderView.setTouchTarget(target);
- boolean res = mRenderView.dispatchTouchEvent(m);
- mRenderView.setTouchTarget(null);
- return res;
- }
-
- private void adjustPosition() {
- getLocationInWindow(mPosition);
- }
-
- public int getWindowPositionX() {
- return mPosition[0];
- }
-
- public int getWindowPositionY() {
- return mPosition[1];
- }
-
- public void update() {
- mRenderView.invalidate();
- }
-
- private class RenderView extends View {
-
- private Renderer mTouchTarget;
-
- public RenderView(Context context) {
- super(context);
- setWillNotDraw(false);
- }
-
- public void setTouchTarget(Renderer target) {
- mTouchTarget = target;
- }
-
- @Override
- public boolean dispatchTouchEvent(MotionEvent evt) {
- if (mGestures == null) {
- return false;
- }
-
- if (mTouchTarget != null) {
- return mTouchTarget.onTouchEvent(evt);
- }
- if (mTouchClients != null) {
- boolean res = false;
- for (Renderer client : mTouchClients) {
- res |= client.onTouchEvent(evt);
- }
- return res;
- }
- return false;
- }
-
- @Override
- public void onLayout(boolean changed, int left, int top, int right, int bottom) {
- adjustPosition();
- super.onLayout(changed, left, top, right, bottom);
- if (mClients == null) return;
- for (Renderer renderer : mClients) {
- renderer.layout(left, top, right, bottom);
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
- if (mClients == null) return;
- boolean redraw = false;
- for (Renderer renderer : mClients) {
- renderer.draw(canvas);
- redraw = redraw || ((OverlayRenderer) renderer).isVisible();
- }
- if (redraw) {
- invalidate();
- }
- }
- }
-
-}
+++ /dev/null
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.camera.ui;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.view.ScaleGestureDetector;
-
-import com.android.camera.debug.Log;
-import com.android.camera2.R;
-
-// TODO: remove this; functionality has been moved to PreviewOverlay.
-@Deprecated
-public class ZoomRenderer extends OverlayRenderer
- implements ScaleGestureDetector.OnScaleGestureListener {
-
- private static final Log.Tag TAG = new Log.Tag("ZoomRenderer");
-
- final private int mMinIndex = 0;
- private int mMaxIndex;
- // Discrete Zoom level [mMinIndex,mMaxIndex].
- private int mCurrentIndex;
- // Continuous Zoom level [0,1].
- private float mCurrentFraction;
- private double mFingerRadians;
- private OnZoomChangedListener mListener;
- private final ScaleGestureDetector mDetector;
- private final Paint mPaint;
- private int mCenterX;
- private int mCenterY;
- private float mOuterRadius;
- private float mInnerRadius;
- private final int mZoomStroke;
-
- public interface OnZoomChangedListener {
- void onZoomStart();
- void onZoomEnd();
- void onZoomValueChanged(int index);
- }
-
- public ZoomRenderer(Context ctx) {
- Resources res = ctx.getResources();
- mZoomStroke = res.getDimensionPixelSize(R.dimen.zoom_stroke);
- mPaint = new Paint();
- mPaint.setAntiAlias(true);
- mPaint.setColor(Color.WHITE);
- mPaint.setStyle(Paint.Style.STROKE);
- mPaint.setStrokeWidth(mZoomStroke);
- mPaint.setStrokeCap(Paint.Cap.ROUND);
- mDetector = new ScaleGestureDetector(ctx, this);
- setVisible(false);
- }
-
- // Set maximum Zoom Index from Module.
- public void setZoomMax(int zoomMaxIndex) {
- mMaxIndex = zoomMaxIndex;
- }
-
- // Set current Zoom Index from Module.
- public void setZoom(int index) {
- mCurrentIndex = index;
- mCurrentFraction = (float) index / (mMaxIndex - mMinIndex);
- }
-
- // Set Zoom Value to display from Module.
- public void setZoomValue(int centiValue) {
- // Do nothing.
- }
-
- public void setOnZoomChangeListener(OnZoomChangedListener listener) {
- mListener = listener;
- }
-
- @Override
- public void layout(int l, int t, int r, int b) {
- super.layout(l, t, r, b);
- mCenterX = (r - l) / 2;
- mCenterY = (b - t) / 2;
- // UI will extend from 20% to 80% of maximum inset circle.
- float insetCircleRadius = Math.min(getWidth(), getHeight());
- mInnerRadius = insetCircleRadius * 0.12f;
- mOuterRadius = insetCircleRadius * 0.38f;
- }
-
- public boolean isScaling() {
- return mDetector.isInProgress();
- }
-
- @Override
- public void onDraw(Canvas canvas) {
- // Draw background.
- mPaint.setAlpha(70);
- canvas.drawLine(mCenterX + mInnerRadius * (float) Math.cos(mFingerRadians),
- mCenterY - mInnerRadius * (float) Math.sin(mFingerRadians),
- mCenterX + mOuterRadius * (float) Math.cos(mFingerRadians),
- mCenterY - mOuterRadius * (float) Math.sin(mFingerRadians), mPaint);
- canvas.drawLine(mCenterX - mInnerRadius * (float) Math.cos(mFingerRadians),
- mCenterY + mInnerRadius * (float) Math.sin(mFingerRadians),
- mCenterX - mOuterRadius * (float) Math.cos(mFingerRadians),
- mCenterY + mOuterRadius * (float) Math.sin(mFingerRadians), mPaint);
- // Draw Zoom progress.
- mPaint.setAlpha(255);
- float zoomRadius = mInnerRadius + mCurrentFraction * (mOuterRadius - mInnerRadius);
- canvas.drawLine(mCenterX + mInnerRadius * (float) Math.cos(mFingerRadians),
- mCenterY - mInnerRadius * (float) Math.sin(mFingerRadians),
- mCenterX + zoomRadius * (float) Math.cos(mFingerRadians),
- mCenterY - zoomRadius * (float) Math.sin(mFingerRadians), mPaint);
- canvas.drawLine(mCenterX - mInnerRadius * (float) Math.cos(mFingerRadians),
- mCenterY + mInnerRadius * (float) Math.sin(mFingerRadians),
- mCenterX - zoomRadius * (float) Math.cos(mFingerRadians),
- mCenterY + zoomRadius * (float) Math.sin(mFingerRadians), mPaint);
- }
-
- @Override
- public boolean onScale(ScaleGestureDetector detector) {
- final float sf = detector.getScaleFactor();
- mCurrentFraction = (0.33f + mCurrentFraction) * sf * sf - 0.33f;
- if (mCurrentFraction < 0.0f) mCurrentFraction = 0.0f;
- if (mCurrentFraction > 1.0f) mCurrentFraction = 1.0f;
- int newIndex = mMinIndex + (int) (mCurrentFraction * (mMaxIndex - mMinIndex));
- if (mListener != null && newIndex != mCurrentIndex) {
- mListener.onZoomValueChanged(newIndex);
- mCurrentIndex = newIndex;
- }
- // mFingerRadians is currently constrained to [0,Pi/2].
- // TODO: Get actual touch coordinates to enable full [0,Pi] range.
- mFingerRadians = Math.atan2(detector.getCurrentSpanY(),detector.getCurrentSpanX());
- return true;
- }
-
- @Override
- public boolean onScaleBegin(ScaleGestureDetector detector) {
- setVisible(true);
- if (mListener != null) {
- mListener.onZoomStart();
- }
- update();
- return true;
- }
-
- @Override
- public void onScaleEnd(ScaleGestureDetector detector) {
- setVisible(false);
- if (mListener != null) {
- mListener.onZoomEnd();
- }
- }
-
-}
public static final boolean HAS_HIDEYBARS = isKitKatOrHigher();
+ public static final boolean IS_NEXUS_4 = "mako".equalsIgnoreCase(Build.DEVICE);
public static final boolean IS_NEXUS_5 = "LGE".equalsIgnoreCase(Build.MANUFACTURER)
&& "hammerhead".equalsIgnoreCase(Build.DEVICE);
public static final boolean IS_NEXUS_6 = "motorola".equalsIgnoreCase(Build.MANUFACTURER)
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Point;
+import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.camera2.CameraCharacteristics;
}
public static int nextPowerOf2(int n) {
+ // TODO: what happens if n is negative or already a power of 2?
n -= 1;
n |= n >>> 16;
n |= n >>> 8;
return (float) Math.sqrt(dx * dx + dy * dy);
}
+ /**
+ * Clamps x to between min and max (inclusive on both ends, x = min --> min,
+ * x = max --> max).
+ */
public static int clamp(int x, int min, int max) {
if (x > max) {
return max;
return x;
}
+ /**
+ * Clamps x to between min and max (inclusive on both ends, x = min --> min,
+ * x = max --> max).
+ */
public static float clamp(float x, float min, float max) {
if (x > max) {
return max;
}
/**
+ * Given (nx, ny) \in [0, 1]^2, in the display's portrait coordinate system,
+ * returns normalized sensor coordinates \in [0, 1]^2 depending on how
+ * the sensor's orientation \in {0, 90, 180, 270}.
+ *
+ * <p>
+ * Returns null if sensorOrientation is not one of the above.
+ * </p>
+ */
+ public static PointF normalizedSensorCoordsForNormalizedDisplayCoords(
+ float nx, float ny, int sensorOrientation) {
+ switch (sensorOrientation) {
+ case 0:
+ return new PointF(nx, ny);
+ case 90:
+ return new PointF(ny, 1.0f - nx);
+ case 180:
+ return new PointF(1.0f - nx, 1.0f - ny);
+ case 270:
+ return new PointF(1.0f - ny, nx);
+ default:
+ return null;
+ }
+ }
+
+ /**
* Given a size, return the largest size with the given aspectRatio that
* maximally fits into the bounding rectangle of the original Size.
*
}
private ViewItem buildItemFromData(int dataID) {
+ if (mActivity.isDestroyed()) {
+ // Loading item data is call from multiple AsyncTasks and the
+ // activity may be finished when buildItemFromData is called.
+ Log.d(TAG, "Activity destroyed, don't load data");
+ return null;
+ }
ImageData data = mDataAdapter.getImageData(dataID);
if (data == null) {
return null;
}
- int width = Math.round(mScale * getWidth());
- int height = Math.round(mScale * getHeight());
+ // Always scale by fixed filmstrip scale, since we only show items when
+ // in filmstrip. Preloading images with a different scale and bounds
+ // interferes with caching.
+ int width = Math.round(FILM_STRIP_SCALE * getWidth());
+ int height = Math.round(FILM_STRIP_SCALE * getHeight());
+ Log.v(TAG, "suggesting item bounds: " + width + "x" + height);
mDataAdapter.suggestViewSizeBound(width, height);
data.prepare();