diff options
Diffstat (limited to 'src/com/android/camera/ui')
29 files changed, 1964 insertions, 533 deletions
diff --git a/src/com/android/camera/ui/Arrows.java b/src/com/android/camera/ui/Arrows.java deleted file mode 100644 index 4923eb10e..000000000 --- a/src/com/android/camera/ui/Arrows.java +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright (c) 2016, The Linux Foundation. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of The Linux Foundation nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package com.android.camera.ui; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; -import android.util.AttributeSet; -import android.view.View; - -import java.util.ArrayList; - -public class Arrows extends View { - private static final int ARROW_COLOR = Color.WHITE; - private static final double ARROW_END_DEGREE = 15d; - private static final int ARROW_END_LENGTH = 50; - - private Paint mPaint; - private ArrayList<Path> mPaths; - - public Arrows(Context context, AttributeSet attrs) { - super(context, attrs); - mPaths = new ArrayList<Path>(); - mPaint = new Paint(); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setColor(ARROW_COLOR); - mPaint.setStrokeWidth(2f); - } - - @Override - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - if (mPaths != null) { - for(int i=0; i < mPaths.size(); i++) { - canvas.drawPath(mPaths.get(i), mPaint); - } - } - } - - public void addPath(float[] x, float[] y) { - Path path = new Path(); - path.reset(); - path.moveTo(x[0], y[0]); - for(int i=1; i < x.length; i++) { - if(i == x.length-1) { - path.lineTo(x[i], y[i]); - - double setha = Math.toDegrees(Math.atan2(y[i] - y[i - 1], x[i] - x[i - 1])); - setha = (setha + ARROW_END_DEGREE + 360) % 360; - path.lineTo(x[i]-(float)(ARROW_END_LENGTH*Math.cos(Math.toRadians(setha))), - y[i]-(float)(ARROW_END_LENGTH*Math.sin(Math.toRadians(setha)))); - path.lineTo(x[i], y[i]); - setha = (setha - ARROW_END_DEGREE*2 + 360) % 360; - path.lineTo(x[i]-(float)(ARROW_END_LENGTH*Math.cos(Math.toRadians(setha))), - y[i]-(float)(ARROW_END_LENGTH*Math.sin(Math.toRadians(setha)))); - } - else - path.quadTo(x[i],y[i], x[i+1], y[i+1]); - } - mPaths.add(path); - invalidate(); - } -} diff --git a/src/com/android/camera/ui/CameraControls.java b/src/com/android/camera/ui/CameraControls.java index b85d7f1ca..26a405727 100644 --- a/src/com/android/camera/ui/CameraControls.java +++ b/src/com/android/camera/ui/CameraControls.java @@ -59,7 +59,6 @@ public class CameraControls extends RotatableLayout { private View mFrontBackSwitcher; private View mHdrSwitcher; private View mTsMakeupSwitcher; - private View mIndicators; private View mPreview; private View mSceneModeSwitcher; private View mFilterModeSwitcher; @@ -135,7 +134,6 @@ public class CameraControls extends RotatableLayout { mMenu.setVisibility(View.INVISIBLE); mMute.setVisibility(View.INVISIBLE); mExitPanorama.setVisibility(View.INVISIBLE); - mIndicators.setVisibility(View.INVISIBLE); mPreview.setVisibility(View.INVISIBLE); isAnimating = false; enableTouch(true); @@ -160,7 +158,6 @@ public class CameraControls extends RotatableLayout { mMenu.setVisibility(View.INVISIBLE); mMute.setVisibility(View.INVISIBLE); mExitPanorama.setVisibility(View.INVISIBLE); - mIndicators.setVisibility(View.INVISIBLE); mPreview.setVisibility(View.INVISIBLE); isAnimating = false; enableTouch(true); @@ -276,8 +273,6 @@ public class CameraControls extends RotatableLayout { mViewList.add(mMute); if (mExitPanorama.getVisibility() == View.VISIBLE) mViewList.add(mExitPanorama); - if (mIndicators.getVisibility() == View.VISIBLE) - mViewList.add(mIndicators); } public void removeFromViewList(View view) { @@ -303,7 +298,6 @@ public class CameraControls extends RotatableLayout { mMute = findViewById(R.id.mute_button); mExitPanorama = findViewById(R.id.exit_panorama); mExitPanorama.setVisibility(View.GONE); - mIndicators = findViewById(R.id.on_screen_indicators); mPreview = findViewById(R.id.preview_thumb); mSceneModeSwitcher = findViewById(R.id.scene_mode_switcher); mFilterModeSwitcher = findViewById(R.id.filter_mode_switcher); @@ -349,6 +343,7 @@ public class CameraControls extends RotatableLayout { mReviewDoneButton = null; } layoutRemaingPhotos(); + mRemainingPhotos.setVisibility(View.VISIBLE); } @Override @@ -381,19 +376,18 @@ public class CameraControls extends RotatableLayout { int rotation = getUnifiedRotation(); toIndex(mSwitcher, w, h, rotation, 4, 6, SWITCHER_INDEX); toIndex(mVideoShutter, w, h, rotation, 3, 6, VIDEO_SHUTTER_INDEX); - toIndex(mMenu, w, h, rotation, 4, 0, MENU_INDEX); - toIndex(mMute, w, h, rotation, 3, 0, MUTE_INDEX); + toIndex(mSceneModeSwitcher, w, h, rotation, 4, 0, SCENE_MODE_INDEX); + toIndex(mMute, w, h, rotation, 4, 0, MUTE_INDEX); + toIndex(mFilterModeSwitcher, w, h, rotation, 3, 0, FILTER_MODE_INDEX); toIndex(mExitPanorama, w, h, rotation, 0, 0, EXIT_PANORAMA_INDEX); - toIndex(mIndicators, w, h, rotation, 0, 6, INDICATOR_INDEX); - toIndex(mFrontBackSwitcher, w, h, rotation, 2, 0, FRONT_BACK_INDEX); + toIndex(mFrontBackSwitcher, w, h, rotation, 1, 0, FRONT_BACK_INDEX); toIndex(mPreview, w, h, rotation, 0, 6, PREVIEW_INDEX); if(TsMakeupManager.HAS_TS_MAKEUP) { - toIndex(mTsMakeupSwitcher, w, h, rotation, 3, 0, TS_MAKEUP_INDEX); + toIndex(mTsMakeupSwitcher, w, h, rotation, 2, 0, TS_MAKEUP_INDEX); } else { - toIndex(mHdrSwitcher, w, h, rotation, 3, 0, HDR_INDEX); + toIndex(mHdrSwitcher, w, h, rotation, 2, 0, HDR_INDEX); } - toIndex(mFilterModeSwitcher, w, h, rotation, 1, 0, FILTER_MODE_INDEX); - toIndex(mSceneModeSwitcher, w, h, rotation, 0, 0, SCENE_MODE_INDEX); + toIndex(mMenu, w, h, rotation, 0, 0, MENU_INDEX); layoutToast(mRefocusToast, w, h, rotation); } @@ -498,7 +492,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.setX(mLocX[idx1][SWITCHER_INDEX] - x); mShutter.setX(mLocX[idx1][SHUTTER_INDEX] - x); mVideoShutter.setX(mLocX[idx1][VIDEO_SHUTTER_INDEX] - x); - mIndicators.setX(mLocX[idx1][INDICATOR_INDEX] - x); mPreview.setX(mLocX[idx1][PREVIEW_INDEX] - x); mFrontBackSwitcher.setY(mLocY[idx1][FRONT_BACK_INDEX] + y); @@ -515,7 +508,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.setY(mLocY[idx1][SWITCHER_INDEX] - y); mShutter.setY(mLocY[idx1][SHUTTER_INDEX] - y); mVideoShutter.setY(mLocY[idx1][VIDEO_SHUTTER_INDEX] - y); - mIndicators.setY(mLocY[idx1][INDICATOR_INDEX] - y); mPreview.setY(mLocY[idx1][PREVIEW_INDEX] - y); } @@ -550,7 +542,6 @@ public class CameraControls extends RotatableLayout { mMenu.animate().cancel(); mMute.animate().cancel(); mExitPanorama.animate().cancel(); - mIndicators.animate().cancel(); mPreview.animate().cancel(); mFrontBackSwitcher.animate().setListener(outlistener); ((ModuleSwitcher) mSwitcher).removePopup(); @@ -573,7 +564,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.animate().translationYBy(mSize).setDuration(ANIME_DURATION); mShutter.animate().translationYBy(mSize).setDuration(ANIME_DURATION); mVideoShutter.animate().translationYBy(mSize).setDuration(ANIME_DURATION); - mIndicators.animate().translationYBy(mSize).setDuration(ANIME_DURATION); mPreview.animate().translationYBy(mSize).setDuration(ANIME_DURATION); break; case 90: @@ -592,7 +582,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.animate().translationXBy(mSize).setDuration(ANIME_DURATION); mShutter.animate().translationXBy(mSize).setDuration(ANIME_DURATION); mVideoShutter.animate().translationXBy(mSize).setDuration(ANIME_DURATION); - mIndicators.animate().translationXBy(mSize).setDuration(ANIME_DURATION); mPreview.animate().translationXBy(mSize).setDuration(ANIME_DURATION); break; case 180: @@ -611,7 +600,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); mShutter.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); mVideoShutter.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); - mIndicators.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); mPreview.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); break; case 270: @@ -630,7 +618,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); mShutter.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); mVideoShutter.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); - mIndicators.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); mPreview.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); break; } @@ -657,7 +644,6 @@ public class CameraControls extends RotatableLayout { mMenu.animate().cancel(); mMute.animate().cancel(); mExitPanorama.animate().cancel(); - mIndicators.animate().cancel(); mPreview.animate().cancel(); if (mViewList != null) for (View v : mViewList) { @@ -669,7 +655,6 @@ public class CameraControls extends RotatableLayout { shutterAnim.stop(); mMenu.setVisibility(View.VISIBLE); - mIndicators.setVisibility(View.VISIBLE); mPreview.setVisibility(View.VISIBLE); mFrontBackSwitcher.animate().setListener(inlistener); @@ -692,7 +677,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); mShutter.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); mVideoShutter.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); - mIndicators.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); mPreview.animate().translationYBy(-mSize).setDuration(ANIME_DURATION); break; case 90: @@ -713,7 +697,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); mShutter.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); mVideoShutter.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); - mIndicators.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); mPreview.animate().translationXBy(-mSize).setDuration(ANIME_DURATION); break; case 180: @@ -734,7 +717,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.animate().translationYBy(mSize).setDuration(ANIME_DURATION); mShutter.animate().translationYBy(mSize).setDuration(ANIME_DURATION); mVideoShutter.animate().translationYBy(mSize).setDuration(ANIME_DURATION); - mIndicators.animate().translationYBy(mSize).setDuration(ANIME_DURATION); mPreview.animate().translationYBy(mSize).setDuration(ANIME_DURATION); break; case 270: @@ -755,7 +737,6 @@ public class CameraControls extends RotatableLayout { mSwitcher.animate().translationXBy(mSize).setDuration(ANIME_DURATION); mShutter.animate().translationXBy(mSize).setDuration(ANIME_DURATION); mVideoShutter.animate().translationXBy(mSize).setDuration(ANIME_DURATION); - mIndicators.animate().translationXBy(mSize).setDuration(ANIME_DURATION); mPreview.animate().translationXBy(mSize).setDuration(ANIME_DURATION); break; } @@ -1010,7 +991,7 @@ public class CameraControls extends RotatableLayout { mPaint.setColor(getResources().getColor(R.color.camera_control_bg_transparent)); } } - invalidate(); + requestLayout(); } public void showRefocusToast(boolean show) { @@ -1025,7 +1006,7 @@ public class CameraControls extends RotatableLayout { View[] views = { mSceneModeSwitcher, mFilterModeSwitcher, mFrontBackSwitcher, TsMakeupManager.HAS_TS_MAKEUP ? mTsMakeupSwitcher : mHdrSwitcher, - mMenu, mShutter, mPreview, mSwitcher, mMute, mReviewRetakeButton, + mMenu, mPreview, mSwitcher, mMute, mReviewRetakeButton, mReviewCancelButton, mReviewDoneButton, mExitPanorama }; for (View v : views) { diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java index e6e00ad56..6a767862f 100644 --- a/src/com/android/camera/ui/FilmStripView.java +++ b/src/com/android/camera/ui/FilmStripView.java @@ -2825,8 +2825,6 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { @Override public boolean onScroll(float x, float y, float dx, float dy) { - if (mPreviewGestures != null && mPreviewGestures.waitUntilNextDown()) - return false; ViewItem currItem = mViewItem[mCurrentItem]; if (currItem == null) { return false; @@ -2892,8 +2890,6 @@ public class FilmStripView extends ViewGroup implements BottomControlsListener { @Override public boolean onFling(float velocityX, float velocityY) { - if (mPreviewGestures != null && mPreviewGestures.waitUntilNextDown()) - return false; final ViewItem currItem = mViewItem[mCurrentItem]; if (currItem == null) { return false; diff --git a/src/com/android/camera/ui/ListMenu.java b/src/com/android/camera/ui/ListMenu.java index d83114cce..88ad8fe2f 100644 --- a/src/com/android/camera/ui/ListMenu.java +++ b/src/com/android/camera/ui/ListMenu.java @@ -120,8 +120,7 @@ public class ListMenu extends ListView + " position " + position); } if (position == mHighlighted) - view.setBackgroundColor(getContext().getResources() - .getColor(R.color.setting_color)); + view.setActivated(true); return view; } @@ -160,7 +159,6 @@ public class ListMenu extends ListView ArrayAdapter<ListPreference> mListItemAdapter = new MoreSettingAdapter(); setAdapter(mListItemAdapter); setOnItemClickListener(this); - setSelector(android.R.color.transparent); // Initialize mEnabled mEnabled = new boolean[mListItem.size()]; for (int i = 0; i < mEnabled.length; i++) { @@ -183,7 +181,6 @@ public class ListMenu extends ListView ArrayAdapter<ListPreference> mListItemAdapter = new MoreSettingAdapter(); setAdapter(mListItemAdapter); setOnItemClickListener(this); - setSelector(android.R.color.transparent); // Initialize mEnabled mEnabled = new boolean[mListItem.size()]; for (int i = 0; i < mEnabled.length; i++) { @@ -229,7 +226,7 @@ public class ListMenu extends ListView mEnabled[j] = enable; int offset = getFirstVisiblePosition(); if (offset >= 0) { - int indexInView = j - offset; + int indexInView = j + getHeaderViewsCount() - offset; if (getChildCount() > indexInView && indexInView >= 0) { getChildAt(indexInView).setEnabled(enable); } @@ -243,8 +240,7 @@ public class ListMenu extends ListView public void resetHighlight() { int count = getChildCount(); for (int i = 0; i < count; i++) { - View v = getChildAt(i); - v.setBackground(null); + getChildAt(i).setActivated(false); } mHighlighted = -1; } @@ -265,7 +261,7 @@ public class ListMenu extends ListView resetHighlight(); ListPreference pref = mListItem.get(position); mHighlighted = position; - view.setBackgroundColor(getContext().getResources().getColor(R.color.setting_color)); + view.setActivated(true); mListener.onPreferenceClicked(pref, (int) view.getY()); } @@ -274,11 +270,9 @@ public class ListMenu extends ListView public void reloadPreference() { int count = getChildCount(); for (int i = 0; i < count; i++) { - ListPreference pref = mListItem.get(i); - if (pref != null) { - ListMenuItem listMenuItem = - (ListMenuItem) getChildAt(i); - listMenuItem.reloadPreference(); + View view = getChildAt(i); + if (view instanceof ListMenuItem) { + ((ListMenuItem) view).reloadPreference(); } } } diff --git a/src/com/android/camera/ui/MenuHelp.java b/src/com/android/camera/ui/MenuHelp.java deleted file mode 100644 index 4adff0fd6..000000000 --- a/src/com/android/camera/ui/MenuHelp.java +++ /dev/null @@ -1,387 +0,0 @@ -/* -Copyright (c) 2016, The Linux Foundation. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of The Linux Foundation nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS -BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR -BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE -OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN -IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package com.android.camera.ui; - -import android.animation.Animator; -import android.animation.Animator.AnimatorListener; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Typeface; -import android.graphics.drawable.AnimationDrawable; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Paint; -import android.graphics.Path; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewPropertyAnimator; -import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.TableLayout; -import android.widget.TextView; -import java.util.ArrayList; - -import org.codeaurora.snapcam.R; -import com.android.camera.ui.ModuleSwitcher; -import com.android.camera.ui.RotateImageView; -import com.android.camera.ShutterButton; -import com.android.camera.Storage; -import com.android.camera.util.CameraUtil; -import com.android.camera.TsMakeupManager; - -public class MenuHelp extends RotatableLayout { - - private static final String TAG = "MenuHelp"; - private View mBackgroundView; - private Arrows mArrows; - private static int mTopMargin = 0; - private static int mBottomMargin = 0; - private static final int HELP_0_0_INDEX = 0; - private static final int HELP_1_0_INDEX = 1; - private static final int HELP_3_0_INDEX = 2; - private static final int HELP_4_6_INDEX = 3; - private static final int OK_2_4_INDEX = 4; - private static final int MAX_INDEX = 5; - private float[][] mLocX = new float[4][MAX_INDEX]; - private float[][] mLocY = new float[4][MAX_INDEX]; - private RotateLayout mHelp0_0; - private RotateLayout mHelp1_0; - private RotateLayout mHelp3_0; - private RotateLayout mHelp4_6; - private RotateLayout mOk2_4; - private Context mContext; - private int mOrientation; - private final static int POINT_MARGIN = 50; - private static final int WIDTH_GRID = 5; - private static final int HEIGHT_GRID = 7; - private Typeface mTypeface; - private boolean forCamera2 = false; - - public MenuHelp(Context context, AttributeSet attrs) { - super(context, attrs); - mContext = context; - mTypeface = Typeface.create(Typeface.SERIF, Typeface.NORMAL); - } - - public MenuHelp(Context context) { - this(context, null); - } - - public void setForCamera2(boolean forCamera2) { - this.forCamera2 = forCamera2; - } - - public void setMargins(int top, int bottom) { - mTopMargin = top; - mBottomMargin = bottom; - } - - @Override - public void onLayout(boolean changed, int l, int t, int r, int b) { - r = r - l; - b = b - t; - l = 0; - t = 0; - for (int i = 0; i < getChildCount(); i++) { - View v = getChildAt(i); - v.layout(l, t, r, b); - } - setLocation(r - l, b - t); - } - - private void setLocation(int w, int h) { - int rotation = getUnifiedRotation(); - toIndex(mHelp0_0, w, h, rotation, 1, 3, HELP_0_0_INDEX); - toIndex(mHelp1_0, w, h, rotation, 2, 2, HELP_1_0_INDEX); - if(TsMakeupManager.HAS_TS_MAKEUP) - toIndex(mHelp3_0, w, h, rotation, 3, 1, HELP_3_0_INDEX); - if (!forCamera2) { - toIndex(mHelp4_6, w, h, rotation, 3, 4, HELP_4_6_INDEX); - } else { - mHelp4_6.setVisibility(View.GONE); - } - toIndex(mOk2_4, w, h, rotation, 1, 5, OK_2_4_INDEX); - fillArrows(w, h, rotation); - } - - private void fillArrows(int w, int h, int rotation) { - View v1 = new View(mContext); - View v2 = new View(mContext); - View v3 = new View(mContext); - { - toIndex(v1, w, h, rotation, 1, 3, -1); - toIndex(v2, w, h, rotation, 0, 1, -1); - toIndex(v3, w, h, rotation, 0, 0, -1); - float[] x = {v1.getX()-POINT_MARGIN, v2.getX(), v3.getX()}; - float[] y = {v1.getY()-POINT_MARGIN, v2.getY(), v3.getY()+POINT_MARGIN}; - mArrows.addPath(x, y); - } - - { - toIndex(v1, w, h, rotation, 2, 2, -1); - toIndex(v2, w, h, rotation, 1, 1, -1); - toIndex(v3, w, h, rotation, 1, 0, -1); - float[] x = {v1.getX()-POINT_MARGIN, v2.getX(), v3.getX()}; - float[] y = {v1.getY()-POINT_MARGIN, v2.getY(), v3.getY()+POINT_MARGIN}; - mArrows.addPath(x, y); - } - - if(TsMakeupManager.HAS_TS_MAKEUP) { - toIndex(v1, w, h, rotation, 3, 1, -1); - toIndex(v2, w, h, rotation, 3, 0, -1); - float[] x = {v1.getX(), v2.getX()}; - float[] y = {v1.getY()-POINT_MARGIN*2, v2.getY()+POINT_MARGIN}; - mArrows.addPath(x, y); - } - - if (!forCamera2) { - toIndex(v1, w, h, rotation, 3, 4, -1); - toIndex(v2, w, h, rotation, 3, 5, -1); - toIndex(v3, w, h, rotation, 4, 6, -1); - float[] x = {v1.getX(), v2.getX(), v3.getX()}; - float[] y = {v1.getY()+POINT_MARGIN, v2.getY(), v3.getY()-POINT_MARGIN}; - mArrows.addPath(x, y); - } - } - - private void toIndex(View v, int w, int h, int rotation, int index, int index2, int index3) { - FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); - int tw = v.getMeasuredWidth(); - int th = v.getMeasuredHeight(); - int l = 0, r = 0, t = 0, b = 0; - - int wnumber = WIDTH_GRID; - int hnumber = HEIGHT_GRID; - int windex = 0; - int hindex = 0; - switch (rotation) { - case 0: - // portrait, to left of anchor at bottom - wnumber = WIDTH_GRID; - hnumber = HEIGHT_GRID; - windex = index; - hindex = index2; - break; - case 90: - // phone landscape: below anchor on right - wnumber = HEIGHT_GRID; - hnumber = WIDTH_GRID; - windex = index2; - hindex = hnumber - index - 1; - break; - case 180: - // phone upside down: right of anchor at top - wnumber = WIDTH_GRID; - hnumber = HEIGHT_GRID; - windex = wnumber - index - 1; - hindex = hnumber - index2 - 1; - break; - case 270: - // reverse landscape: above anchor on left - wnumber = HEIGHT_GRID; - hnumber = WIDTH_GRID; - windex = wnumber - index2 - 1; - hindex = index; - break; - } - int boxh = h / hnumber; - int boxw = w / wnumber; - int cx = (2 * windex + 1) * boxw / 2; - int cy = (2 * hindex + 1) * boxh / 2; - - if (index2 == 0 && mTopMargin != 0) { - switch (rotation) { - case 90: - cx = mTopMargin / 2; - break; - case 180: - cy = h - mTopMargin / 2; - break; - case 270: - cx = w - mTopMargin / 2; - break; - default: - cy = mTopMargin / 2; - break; - } - } - - l = cx - tw / 2; - r = cx + tw / 2; - t = cy - th / 2; - b = cy + th / 2; - - if (index3 != -1) { - int idx1 = rotation / 90; - int idx2 = index3; - mLocX[idx1][idx2] = l; - mLocY[idx1][idx2] = t; - } - v.layout(l, t, r, b); - } - - public void setOrientation(int orientation, boolean animation) { - mOrientation = orientation; - RotateLayout[] layouts = { - mHelp0_0, mHelp1_0, mHelp3_0, mHelp4_6, mOk2_4 - }; - for (RotateLayout l : layouts) { - l.setOrientation(orientation, animation); - } - } - - @Override - public void onFinishInflate() { - super.onFinishInflate(); - mBackgroundView = findViewById(R.id.background); - mBackgroundView.setBackgroundColor(Color.argb(200, 0, 0, 0)); - mHelp0_0 = (RotateLayout)findViewById(R.id.help_text_0_0); - fillHelp0_0(); - mHelp1_0 = (RotateLayout)findViewById(R.id.help_text_1_0); - fillHelp1_0(); - mHelp3_0 = (RotateLayout) findViewById(R.id.help_text_3_0); - fillHelp3_0(); - mHelp4_6 = (RotateLayout)findViewById(R.id.help_text_4_6); - fillHelp4_6(); - mOk2_4 = (RotateLayout)findViewById(R.id.help_ok_2_4); - fillOk2_4(); - mArrows = (Arrows)findViewById(R.id.arrows); - } - - private void fillOk2_4() { - LinearLayout linearLayout = new LinearLayout(mContext); - mOk2_4.addView(linearLayout); - linearLayout.setGravity(Gravity.CENTER); - linearLayout.setPadding(40, 20, 40, 20); - linearLayout.setBackgroundColor(Color.WHITE); - TextView text1 = new TextView(mContext); - text1.setText(getResources().getString(R.string.help_menu_ok)); - text1.setTextColor(Color.BLACK); - text1.setTypeface(mTypeface); - linearLayout.addView(text1); - } - - private void fillHelp0_0() { - TableLayout tableLayout = new TableLayout(mContext); - mHelp0_0.addView(tableLayout); - LinearLayout linearLayout = new LinearLayout(mContext); - TextView text1 = new TextView(mContext); - text1.setTextColor(getResources().getColor(R.color.help_menu_scene_mode_1)); - text1.setText(getResources().getString(R.string.help_menu_scene_mode_1)+" "); - text1.setTypeface(mTypeface); - linearLayout.addView(text1); - TextView text2 = new TextView(mContext); - text2.setText(getResources().getString(R.string.help_menu_scene_mode_2)); - text2.setTypeface(mTypeface); - linearLayout.addView(text2); - text2.setTextColor(getResources().getColor(R.color.help_menu_scene_mode_2)); - tableLayout.addView(linearLayout); - TextView text3 = new TextView(mContext); - text3.setText(getResources().getString(R.string.help_menu_scene_mode_3)); - text3.setTextColor(getResources().getColor(R.color.help_menu_scene_mode_3)); - text3.setTypeface(mTypeface); - tableLayout.addView(text3); - } - - private void fillHelp1_0() { - TableLayout tableLayout = new TableLayout(mContext); - mHelp1_0.addView(tableLayout); - LinearLayout linearLayout = new LinearLayout(mContext); - TextView text1 = new TextView(mContext); - text1.setText(getResources().getString(R.string.help_menu_color_filter_1)+" "); - text1.setTextColor(getResources().getColor(R.color.help_menu_color_filter_1)); - text1.setTypeface(mTypeface); - linearLayout.addView(text1); - TextView text2 = new TextView(mContext); - text2.setText(getResources().getString(R.string.help_menu_color_filter_2)+" "); - text2.setTextColor(getResources().getColor(R.color.help_menu_color_filter_2)); - text2.setTypeface(mTypeface); - linearLayout.addView(text2); - TextView text3 = new TextView(mContext); - text3.setText(getResources().getString(R.string.help_menu_color_filter_3)); - text3.setTextColor(getResources().getColor(R.color.help_menu_color_filter_3)); - text3.setTypeface(mTypeface); - linearLayout.addView(text3); - tableLayout.addView(linearLayout); - TextView text4 = new TextView(mContext); - text4.setText(getResources().getString(R.string.help_menu_color_filter_4)); - text4.setTextColor(getResources().getColor(R.color.help_menu_color_filter_4)); - text4.setTypeface(mTypeface); - tableLayout.addView(text4); - } - - private void fillHelp3_0() { - TableLayout tableLayout = new TableLayout(mContext); - mHelp3_0.addView(tableLayout); - if(TsMakeupManager.HAS_TS_MAKEUP) { - TextView text1 = new TextView(mContext); - text1.setText(getResources().getString(R.string.help_menu_beautify_1)); - text1.setTextColor(getResources().getColor(R.color.help_menu_beautify_1)); - text1.setTypeface(mTypeface); - tableLayout.addView(text1); - TextView text2 = new TextView(mContext); - text2.setText(getResources().getString(R.string.help_menu_beautify_2)); - text2.setTextColor(getResources().getColor(R.color.help_menu_beautify_2)); - text2.setTypeface(mTypeface); - tableLayout.addView(text2); - TextView text3 = new TextView(mContext); - text3.setText(getResources().getString(R.string.help_menu_beautify_3)); - text3.setTextColor(getResources().getColor(R.color.help_menu_beautify_3)); - text3.setTypeface(mTypeface); - tableLayout.addView(text3); - } - } - - private void fillHelp4_6() { - TableLayout tableLayout = new TableLayout(mContext); - mHelp4_6.addView(tableLayout); - LinearLayout linearLayout = new LinearLayout(mContext); - TextView text1 = new TextView(mContext); - text1.setText(getResources().getString(R.string.help_menu_switcher_1)+" "); - text1.setTextColor(Color.GREEN); - text1.setTypeface(mTypeface); - linearLayout.addView(text1); - TextView text2 = new TextView(mContext); - text2.setText(getResources().getString(R.string.help_menu_switcher_2)); - text2.setTextColor(Color.WHITE); - text2.setTypeface(mTypeface); - linearLayout.addView(text2); - tableLayout.addView(linearLayout); - TextView text3 = new TextView(mContext); - text3.setText(getResources().getString(R.string.help_menu_switcher_3)); - text3.setTextColor(Color.WHITE); - text3.setTypeface(mTypeface); - tableLayout.addView(text3); - } -} diff --git a/src/com/android/camera/ui/ModuleSwitcher.java b/src/com/android/camera/ui/ModuleSwitcher.java index d96775d14..058e64d7b 100644 --- a/src/com/android/camera/ui/ModuleSwitcher.java +++ b/src/com/android/camera/ui/ModuleSwitcher.java @@ -51,10 +51,11 @@ public class ModuleSwitcher extends RotateImageView public static final int PHOTO_MODULE_INDEX = 0; public static final int VIDEO_MODULE_INDEX = 1; public static final int WIDE_ANGLE_PANO_MODULE_INDEX = 2; - public static final int LIGHTCYCLE_MODULE_INDEX = 3; - public static final int GCAM_MODULE_INDEX = 4; - public static final int CAPTURE_MODULE_INDEX = 5; - public static final int PANOCAPTURE_MODULE_INDEX = 6; + public static final int QR_MODULE_INDEX = 3; + public static final int LIGHTCYCLE_MODULE_INDEX = 4; + public static final int GCAM_MODULE_INDEX = 5; + public static final int CAPTURE_MODULE_INDEX = 6; + public static final int PANOCAPTURE_MODULE_INDEX = 7; private boolean mTouchEnabled = true; private boolean mIsVisible = true; @@ -63,6 +64,7 @@ public class ModuleSwitcher extends RotateImageView R.drawable.ic_switch_camera, R.drawable.ic_switch_video, R.drawable.ic_switch_pan, + R.drawable.ic_cam_switcher_qr, R.drawable.ic_switch_photosphere, R.drawable.ic_switch_gcam, }; @@ -102,7 +104,7 @@ public class ModuleSwitcher extends RotateImageView private void init(Context context) { mItemSize = context.getResources().getDimensionPixelSize(R.dimen.switcher_size); - mIndicator = context.getResources().getDrawable(R.drawable.ic_switcher_menu_indicator); + mIndicator = context.getResources().getDrawable(R.color.transparent); initializeDrawables(context); } @@ -231,6 +233,10 @@ public class ModuleSwitcher extends RotateImageView item.setContentDescription(getContext().getResources().getString( R.string.accessibility_switch_to_panorama)); break; + case R.drawable.ic_cam_switcher_qr: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_qr)); + break; case R.drawable.ic_switch_photosphere: item.setContentDescription(getContext().getResources().getString( R.string.accessibility_switch_to_photo_sphere)); diff --git a/src/com/android/camera/ui/OneUICameraControls.java b/src/com/android/camera/ui/OneUICameraControls.java index 646596912..73cb68e16 100755 --- a/src/com/android/camera/ui/OneUICameraControls.java +++ b/src/com/android/camera/ui/OneUICameraControls.java @@ -232,7 +232,7 @@ public class OneUICameraControls extends RotatableLayout { mViews = new View[]{ mSceneModeSwitcher, mFilterModeSwitcher, mFrontBackSwitcher, - mTsMakeupSwitcher,mDeepportraitSwitcher, mFlashButton, mShutter, + mTsMakeupSwitcher, mDeepportraitSwitcher, mFlashButton, mPreview, mVideoShutter, mPauseButton, mCancelButton }; mBottomLargeSize = getResources().getDimensionPixelSize( @@ -501,7 +501,7 @@ public class OneUICameraControls extends RotatableLayout { View[] views = { mSceneModeSwitcher, mFilterModeSwitcher, mFrontBackSwitcher, mTsMakeupSwitcher, mFlashButton, mDeepportraitSwitcher, mPreview, mMute, - mShutter, mVideoShutter, mMakeupSeekBarLowText, mMakeupSeekBarHighText, + mMakeupSeekBarLowText, mMakeupSeekBarHighText, mPauseButton, mExitBestPhotpMode }; diff --git a/src/com/android/camera/ui/Switch.java b/src/com/android/camera/ui/Switch.java index 3e7b9bfb9..68fb36542 100644 --- a/src/com/android/camera/ui/Switch.java +++ b/src/com/android/camera/ui/Switch.java @@ -123,7 +123,7 @@ public class Switch extends CompoundButton { mSwitchMinWidth = res.getDimensionPixelSize(R.dimen.switch_min_width); mSwitchTextMaxWidth = res.getDimensionPixelSize(R.dimen.switch_text_max_width); mSwitchPadding = res.getDimensionPixelSize(R.dimen.switch_padding); - setSwitchTextAppearance(context, android.R.style.TextAppearance_Holo_Small); + setSwitchTextAppearance(context, android.R.style.TextAppearance_Material_Small); ViewConfiguration config = ViewConfiguration.get(context); mTouchSlop = config.getScaledTouchSlop(); diff --git a/src/com/android/camera/ui/ZoomRenderer.java b/src/com/android/camera/ui/ZoomRenderer.java index 48a565a6f..eb88decc2 100755 --- a/src/com/android/camera/ui/ZoomRenderer.java +++ b/src/com/android/camera/ui/ZoomRenderer.java @@ -16,12 +16,15 @@ package com.android.camera.ui; +import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Point; import android.graphics.Rect; +import android.os.Handler; import android.view.ScaleGestureDetector; import org.codeaurora.snapcam.R; @@ -35,6 +38,8 @@ public class ZoomRenderer extends OverlayRenderer private int mMinZoom; private OnZoomChangedListener mListener; + private final Handler mHandler = new Handler(); + private ScaleGestureDetector mDetector; private Paint mPaint; private Paint mTextPaint; @@ -47,12 +52,17 @@ public class ZoomRenderer extends OverlayRenderer private int mOuterStroke; private int mZoomSig; private int mZoomFraction; + private boolean mInZoom; private Rect mTextBounds; private int mOrientation; private boolean mCamera2 = false; private float mZoomMinValue; private float mZoomMaxValue; + private Point mDispSize; + private int mBottomMargin; + private int mTopMargin; + public interface OnZoomChangedListener { void onZoomStart(); void onZoomEnd(); @@ -77,6 +87,13 @@ public class ZoomRenderer extends OverlayRenderer mMinCircle = res.getDimensionPixelSize(R.dimen.zoom_ring_min); mTextBounds = new Rect(); setVisible(false); + mBottomMargin = + ctx.getResources().getDimensionPixelSize(R.dimen.preview_bottom_margin); + mTopMargin = + ctx.getResources().getDimensionPixelSize(R.dimen.preview_top_margin); + mDispSize = new Point(); + Activity activity = (Activity) ctx; + activity.getWindowManager().getDefaultDisplay().getRealSize(mDispSize); } // set from module @@ -115,9 +132,13 @@ public class ZoomRenderer extends OverlayRenderer @Override public void layout(int l, int t, int r, int b) { + l = 0; + t = mTopMargin; + r = mDispSize.x; + b = mDispSize.y - mBottomMargin; super.layout(l, t, r, b); mCenterX = (r - l) / 2; - mCenterY = (b - t) / 2; + mCenterY = ((b - t) / 2) + t; mMaxCircle = Math.min(getWidth(), getHeight()); mMaxCircle = (mMaxCircle - mMinCircle) / 2; } @@ -165,6 +186,48 @@ public class ZoomRenderer extends OverlayRenderer return true; } + public boolean onScaleStepResize(boolean direction) { + int zoom; + float circle; + float circleStep = (mMaxCircle - mMinCircle) / 18; + if (direction) { + circle = (int) (mCircleSize + circleStep); + } else { + circle = (int) (mCircleSize - circleStep); + } + circle = Math.max(mMinCircle, circle); + circle = Math.min(mMaxCircle, circle); + if (mListener != null && (int) circle != mCircleSize && (mMaxCircle - mMinCircle) > 0) { + mCircleSize = (int) circle; + zoom = mMinZoom + (int) ((mCircleSize - mMinCircle) + * (mMaxZoom - mMinZoom) / (mMaxCircle - mMinCircle)); + if (mListener != null) { + mHandler.removeCallbacks(mOnZoomEnd); + if (!mInZoom) { + mInZoom = true; + setVisible(true); + mListener.onZoomStart(); + update(); + } + mListener.onZoomValueChanged(zoom); + mHandler.postDelayed(mOnZoomEnd, 300); + } + return true; + } else { + return false; + } + } + + Runnable mOnZoomEnd = new Runnable() { + public void run() { + mInZoom = false; + setVisible(false); + if (mListener != null) { + mListener.onZoomEnd(); + } + } + }; + @Override public boolean onScaleBegin(ScaleGestureDetector detector) { setVisible(true); diff --git a/src/com/android/camera/ui/focus/AutoFocusRing.java b/src/com/android/camera/ui/focus/AutoFocusRing.java new file mode 100644 index 000000000..ff09ebc65 --- /dev/null +++ b/src/com/android/camera/ui/focus/AutoFocusRing.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 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.focus; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.android.camera.ui.motion.InterpolateUtils; +import com.android.camera.ui.motion.Invalidator; + +/** + * Passive focus ring animation renderer. + */ +class AutoFocusRing extends FocusRingRenderer { + private static final String TAG = "AutoFocusRing"; + + /** + * The auto focus ring encapsulates the animation logic for visualizing + * a focus event when triggered by the camera subsystem. + * + * @param invalidator the object to invalidate while running. + * @param ringPaint the paint to draw the ring with. + * @param enterDurationMillis the fade in time in milliseconds. + * @param exitDurationMillis the fade out time in milliseconds. + */ + public AutoFocusRing(Invalidator invalidator, Paint ringPaint, float enterDurationMillis, + float exitDurationMillis) { + super(invalidator, ringPaint, enterDurationMillis, exitDurationMillis); + } + + @Override + public void draw(long t, long dt, Canvas canvas) { + float ringRadius = mRingRadius.update(dt); + processStates(t); + + if (!isActive()) { + return; + } + + mInvalidator.invalidate(); + int ringAlpha = 255; + + if (mFocusState == FocusState.STATE_ENTER) { + float rFade = InterpolateUtils.unitRatio(t, mEnterStartMillis, mEnterDurationMillis); + ringAlpha = (int) InterpolateUtils + .lerp(0, 255, mEnterOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_FADE_OUT) { + float rFade = InterpolateUtils.unitRatio(t, mExitStartMillis, mExitDurationMillis); + ringAlpha = (int) InterpolateUtils + .lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_HARD_STOP) { + float rFade = InterpolateUtils + .unitRatio(t, mHardExitStartMillis, mHardExitDurationMillis); + ringAlpha = (int) InterpolateUtils + .lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_INACTIVE) { + ringAlpha = 0; + } + + mRingPaint.setAlpha(ringAlpha); + canvas.drawCircle(getCenterX(), getCenterY(), ringRadius, mRingPaint); + } + + private void processStates(long t) { + if (mFocusState == FocusState.STATE_INACTIVE) { + return; + } + + if (mFocusState == FocusState.STATE_ENTER && t > mEnterStartMillis + mEnterDurationMillis) { + mFocusState = FocusState.STATE_ACTIVE; + } + + if (mFocusState == FocusState.STATE_ACTIVE && !mRingRadius.isActive()) { + mFocusState = FocusState.STATE_FADE_OUT; + mExitStartMillis = t; + } + + if (mFocusState == FocusState.STATE_FADE_OUT && t > mExitStartMillis + mExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + + if (mFocusState == FocusState.STATE_HARD_STOP + && t > mHardExitStartMillis + mHardExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + } +} diff --git a/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java b/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java new file mode 100644 index 000000000..809503c9a --- /dev/null +++ b/src/com/android/camera/ui/focus/CameraCoordinateTransformer.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2014 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.focus; + +import android.graphics.Matrix; +import android.graphics.RectF; + +/** + * Transform coordinates to and from preview coordinate space and camera driver + * coordinate space. + */ +public class CameraCoordinateTransformer { + // http://developer.android.com/guide/topics/media/camera.html#metering-focus-areas + private static final RectF CAMERA_DRIVER_RECT = new RectF(-1000, -1000, 1000, 1000); + + private final Matrix mCameraToPreviewTransform; + private final Matrix mPreviewToCameraTransform; + + /** + * Convert rectangles to / from camera coordinate and preview coordinate space. + * + * @param mirrorX if the preview is mirrored along the X axis. + * @param displayOrientation orientation in degrees. + * @param previewRect the preview rectangle size and position. + */ + public CameraCoordinateTransformer(boolean mirrorX, int displayOrientation, + RectF previewRect) { + if (!hasNonZeroArea(previewRect)) { + throw new IllegalArgumentException("previewRect"); + } + + mCameraToPreviewTransform = cameraToPreviewTransform(mirrorX, displayOrientation, + previewRect); + mPreviewToCameraTransform = inverse(mCameraToPreviewTransform); + } + + /** + * Transform a rectangle in camera space into a new rectangle in preview + * view space. + * + * @param source the rectangle in camera space + * @return the rectangle in preview view space. + */ + public RectF toPreviewSpace(RectF source) { + RectF result = new RectF(); + mCameraToPreviewTransform.mapRect(result, source); + return result; + } + + /** + * Transform a rectangle in preview view space into a new rectangle in + * camera view space. + * + * @param source the rectangle in preview view space + * @return the rectangle in camera view space. + */ + public RectF toCameraSpace(RectF source) { + RectF result = new RectF(); + mPreviewToCameraTransform.mapRect(result, source); + return result; + } + + private Matrix cameraToPreviewTransform(boolean mirrorX, int displayOrientation, + RectF previewRect) { + Matrix transform = new Matrix(); + + // Need mirror for front camera. + transform.setScale(mirrorX ? -1 : 1, 1); + + // Apply a rotate transform. + // This is the value for android.hardware.Camera.setDisplayOrientation. + transform.postRotate(displayOrientation); + + // Map camera driver coordinates to preview rect coordinates + Matrix fill = new Matrix(); + fill.setRectToRect(CAMERA_DRIVER_RECT, + previewRect, + Matrix.ScaleToFit.FILL); + + // Concat the previous transform on top of the fill behavior. + transform.setConcat(fill, transform); + + return transform; + } + + private Matrix inverse(Matrix source) { + Matrix newMatrix = new Matrix(); + source.invert(newMatrix); + return newMatrix; + } + + private boolean hasNonZeroArea(RectF rect) { + return rect.width() != 0 && rect.height() != 0; + } +} diff --git a/src/com/android/camera/ui/focus/FocusController.java b/src/com/android/camera/ui/focus/FocusController.java new file mode 100644 index 000000000..a0c103443 --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusController.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2014 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.focus; + +import android.graphics.RectF; +import android.util.Log; + +import com.android.camera.async.MainThread; +import com.android.camera.ui.motion.LinearScale; + +/** + * The focus controller interacts with the focus ring UI element. + */ +public class FocusController { + private static final String TAG = "FocusController"; + + private final FocusRing mFocusRing; + private final FocusSound mFocusSound; + private final MainThread mMainThread; + + public FocusController(FocusRing focusRing, FocusSound focusSound, MainThread mainThread) { + mFocusRing = focusRing; + mFocusSound = focusSound; + mMainThread = mainThread; + } + + /** + * Show a passive focus animation at the center of the active area. + * This will likely be different than the view bounds due to varying image + * ratios and dimensions. + */ + public void showPassiveFocusAtCenter() { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "Running showPassiveFocusAtCenter()"); + mFocusRing.startPassiveFocus(); + mFocusRing.centerFocusLocation(); + } + }); + } + + /** + * Show a passive focus animation at the given viewX and viewY position. + * This is usually indicates the camera subsystem kicked off an auto-focus + * at the given screen position. + * + * @param viewX the view's x coordinate + * @param viewY the view's y coordinate + */ + public void showPassiveFocusAt(final int viewX, final int viewY) { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "Running showPassiveFocusAt(" + viewX + ", " + viewY + ")"); + mFocusRing.startPassiveFocus(); + mFocusRing.setFocusLocation(viewX, viewY); + } + }); + } + + /** + * Show an active focus animation at the given viewX and viewY position. + * This is normally initiated by the user touching the screen at a given + * point. + * + * @param viewX the view's x coordinate + * @param viewY the view's y coordinate + */ + public void showActiveFocusAt(final int viewX, final int viewY) { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "showActiveFocusAt(" + viewX + ", " + viewY + ")"); + mFocusRing.startActiveFocus(); + mFocusRing.setFocusLocation(viewX, viewY); + + // TODO: Enable focus sound when better audio controls exist. + // mFocusSound.play(); + } + }); + } + + /** + * Computing the correct location for the focus ring requires knowing + * the screen position and size of the preview area so the drawing + * operations can be clipped correctly. + */ + public void configurePreviewDimensions(final RectF previewArea) { + mMainThread.execute(new Runnable() { + @Override + public void run() { + Log.v(TAG, "configurePreviewDimensions(" + previewArea + ")"); + mFocusRing.configurePreviewDimensions(previewArea); + } + }); + } + + /** + * Set the radius of the focus ring as a radius between 0 and 1. + * This will map to the min and max values computed for the UI. + */ + public void setFocusRatio(final float ratio) { + mMainThread.execute(new Runnable() { + @Override + public void run() { + if (mFocusRing.isPassiveFocusRunning() || + mFocusRing.isActiveFocusRunning()) { + mFocusRing.setRadiusRatio(ratio); + } + } + }); + } +} diff --git a/src/com/android/camera/ui/focus/FocusRing.java b/src/com/android/camera/ui/focus/FocusRing.java new file mode 100644 index 000000000..89de357ad --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRing.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 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.focus; + +import android.graphics.RectF; + +/** + * Primary interface for interacting with the focus ring UI. + */ +public interface FocusRing { + /** + * Check the state of the passive focus ring animation. + * + * @return whether the passive focus animation is running. + */ + public boolean isPassiveFocusRunning(); + /** + * Check the state of the active focus ring animation. + * + * @return whether the active focus animation is running. + */ + public boolean isActiveFocusRunning(); + /** + * Start a passive focus animation. + */ + public void startPassiveFocus(); + /** + * Start an active focus animation. + */ + public void startActiveFocus(); + /** + * Stop any currently running focus animations. + */ + public void stopFocusAnimations(); + /** + * Set the location of the focus ring animation center. + */ + public void setFocusLocation(float viewX, float viewY); + + /** + * Set the location of the focus ring animation center. + */ + public void centerFocusLocation(); + + /** + * Set the target radius as a ratio of min to max visible radius + * which will internally convert and clamp the value to the + * correct pixel radius. + */ + public void setRadiusRatio(float ratio); + + /** + * The physical size of preview can vary and does not map directly + * to the size of the view. This allows for conversions between view + * and preview space for values that are provided in preview space. + */ + void configurePreviewDimensions(RectF previewArea); +}
\ No newline at end of file diff --git a/src/com/android/camera/ui/focus/FocusRingRenderer.java b/src/com/android/camera/ui/focus/FocusRingRenderer.java new file mode 100644 index 000000000..264af2ace --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRingRenderer.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2014 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.focus; + +import android.graphics.Paint; +import android.util.Log; + +import com.android.camera.ui.motion.DampedSpring; +import com.android.camera.ui.motion.DynamicAnimation; +import com.android.camera.ui.motion.Invalidator; +import com.android.camera.ui.motion.UnitCurve; +import com.android.camera.ui.motion.UnitCurves; + +/** + * Base class for defining the focus ring states, enter and exit durations, and + * positioning logic. + */ +abstract class FocusRingRenderer implements DynamicAnimation { + private static final String TAG = "FocusRingRenderer"; + + /** + * Primary focus states that a focus ring renderer can go through. + */ + protected static enum FocusState { + STATE_INACTIVE, + STATE_ENTER, + STATE_ACTIVE, + STATE_FADE_OUT, + STATE_HARD_STOP, + } + + protected final Invalidator mInvalidator; + protected final Paint mRingPaint; + protected final DampedSpring mRingRadius; + protected final UnitCurve mEnterOpacityCurve; + protected final UnitCurve mExitOpacityCurve; + protected final UnitCurve mHardExitOpacityCurve; + protected final float mEnterDurationMillis; + protected final float mExitDurationMillis; + protected final float mHardExitDurationMillis = 64; + + private int mCenterX; + private int mCenterY; + protected long mEnterStartMillis = 0; + protected long mExitStartMillis = 0; + protected long mHardExitStartMillis = 0; + + protected FocusState mFocusState = FocusState.STATE_INACTIVE; + + /** + * A dynamic, configurable, self contained ring render that will inform + * via invalidation if it should continue to be receive updates + * and re-draws. + * + * @param invalidator the object to inform if it requires more draw calls. + * @param ringPaint the paint to use to draw the ring. + * @param enterDurationMillis the fade in duration in milliseconds + * @param exitDurationMillis the fade out duration in milliseconds. + */ + FocusRingRenderer(Invalidator invalidator, Paint ringPaint, float enterDurationMillis, + float exitDurationMillis) { + mInvalidator = invalidator; + mRingPaint = ringPaint; + mEnterDurationMillis = enterDurationMillis; + mExitDurationMillis = exitDurationMillis; + + mEnterOpacityCurve = UnitCurves.FAST_OUT_SLOW_IN; + mExitOpacityCurve = UnitCurves.FAST_OUT_LINEAR_IN; + mHardExitOpacityCurve = UnitCurves.FAST_OUT_LINEAR_IN; + + mRingRadius = new DampedSpring(); + } + + /** + * Set the centerX position for this focus ring renderer. + * + * @param value the x position + */ + public void setCenterX(int value) { + mCenterX = value; + } + + protected int getCenterX() { + return mCenterX; + } + + /** + * Set the centerY position for this focus ring renderer. + * + * @param value the y position + */ + public void setCenterY(int value) { + mCenterY = value; + } + + protected int getCenterY() { + return mCenterY; + } + + /** + * Set the physical radius of this ring. + * + * @param value the radius of the ring. + */ + public void setRadius(long tMs, float value) { + if (mFocusState == FocusState.STATE_FADE_OUT + && Math.abs(mRingRadius.getTarget() - value) > 0.1) { + Log.v(TAG, "FOCUS STATE ENTER VIA setRadius(" + tMs + ", " + value + ")"); + mFocusState = FocusState.STATE_ENTER; + mEnterStartMillis = computeEnterStartTimeMillis(tMs, mEnterDurationMillis); + } + + mRingRadius.setTarget(value); + } + + /** + * returns true if the renderer is not in an inactive state. + */ + @Override + public boolean isActive() { + return mFocusState != FocusState.STATE_INACTIVE; + } + + /** + * returns true if the renderer is in an exit state. + */ + public boolean isExiting() { + return mFocusState == FocusState.STATE_FADE_OUT + || mFocusState == FocusState.STATE_HARD_STOP; + } + + /** + * returns true if the renderer is in an enter state. + */ + public boolean isEntering() { + return mFocusState == FocusState.STATE_ENTER; + } + + /** + * Initialize and start the animation with the given start and + * target radius. + */ + public void start(long startMs, float initialRadius, float targetRadius) { + if (mFocusState != FocusState.STATE_INACTIVE) { + Log.w(TAG, "start() called while the ring was still focusing!"); + } + mRingRadius.stop(); + mRingRadius.setValue(initialRadius); + mRingRadius.setTarget(targetRadius); + mEnterStartMillis = startMs; + + mFocusState = FocusState.STATE_ENTER; + mInvalidator.invalidate(); + } + + /** + * Put the animation in the exit state regardless of the current + * dynamic transition. If the animation is currently in an enter state + * this will compute an exit start time such that the exit time lines + * up with the enter time at the current transition value. + * + * @param t the current animation time. + */ + public void exit(long t) { + if (mRingRadius.isActive()) { + mRingRadius.stop(); + } + + mFocusState = FocusState.STATE_FADE_OUT; + mExitStartMillis = computeExitStartTimeMs(t, mExitDurationMillis); + } + + /** + * Put the animation in the hard stop state regardless of the current + * dynamic transition. If the animation is currently in an enter state + * this will compute an exit start time such that the exit time lines + * up with the enter time at the current transition value. + * + * @param tMillis the current animation time in milliseconds. + */ + public void stop(long tMillis) { + if (mRingRadius.isActive()) { + mRingRadius.stop(); + } + + mFocusState = FocusState.STATE_HARD_STOP; + mHardExitStartMillis = computeExitStartTimeMs(tMillis, mHardExitDurationMillis); + } + + private long computeExitStartTimeMs(long tMillis, float exitDuration) { + if (mEnterStartMillis + mEnterDurationMillis <= tMillis) { + return tMillis; + } + + // Compute the current progress on the enter animation. + float enterT = (tMillis - mEnterStartMillis) / mEnterDurationMillis; + + // Find a time on the exit curve such that it will produce the same value. + float exitT = UnitCurves.mapEnterCurveToExitCurveAtT(mEnterOpacityCurve, mExitOpacityCurve, + enterT); + + // Compute the a start time before tMs such that the ratio of time completed + // equals the computed exit curve animation position. + return tMillis - (long) (exitT * exitDuration); + } + + private long computeEnterStartTimeMillis(long tMillis, float enterDuration) { + if (mExitStartMillis + mExitDurationMillis <= tMillis) { + return tMillis; + } + + // Compute the current progress on the enter animation. + float exitT = (tMillis - mExitStartMillis) / mExitDurationMillis; + + // Find a time on the exit curve such that it will produce the same value. + float enterT = UnitCurves.mapEnterCurveToExitCurveAtT(mExitOpacityCurve, mEnterOpacityCurve, + exitT); + + // Compute the a start time before tMs such that the ratio of time completed + // equals the computed exit curve animation position. + return tMillis - (long) (enterT * enterDuration); + } +} diff --git a/src/com/android/camera/ui/focus/FocusRingView.java b/src/com/android/camera/ui/focus/FocusRingView.java new file mode 100644 index 000000000..14a7f6cc9 --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusRingView.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2014 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.focus; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.RectF; +import android.graphics.Region; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.camera.ui.motion.AnimationClock.SystemTimeClock; +import com.android.camera.ui.motion.DynamicAnimator; +import com.android.camera.ui.motion.Invalidator; +import com.android.camera.ui.motion.LinearScale; + +import org.codeaurora.snapcam.R; + +/** + * Custom view for running the focus ring animations. + */ +public class FocusRingView extends View implements Invalidator, FocusRing { + private static final String TAG = "FocusRingView"; + private static final float FADE_IN_DURATION_MILLIS = 1000f; + private static final float FADE_OUT_DURATION_MILLIS = 250f; + + private final AutoFocusRing mAutoFocusRing; + private final ManualFocusRing mManualFocusRing; + private final DynamicAnimator mAnimator; + private final LinearScale mRatioScale; + private final float mDefaultRadiusPx; + + private FocusRingRenderer currentFocusAnimation; + private boolean isFirstDraw; + private float mLastRadiusPx; + + private RectF mPreviewSize; + + public FocusRingView(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources res = getResources(); + Paint paint = makePaint(res, R.color.focus_color); + + float focusCircleMinSize = res.getDimensionPixelSize(R.dimen.focus_circle_min_size); + float focusCircleMaxSize = res.getDimensionPixelSize(R.dimen.focus_circle_max_size); + mDefaultRadiusPx = res.getDimensionPixelSize(R.dimen.focus_circle_initial_size); + + mRatioScale = new LinearScale(0, 1, focusCircleMinSize, focusCircleMaxSize); + mAnimator = new DynamicAnimator(this, new SystemTimeClock()); + + mAutoFocusRing = new AutoFocusRing(mAnimator, paint, + FADE_IN_DURATION_MILLIS, + FADE_OUT_DURATION_MILLIS); + mManualFocusRing = new ManualFocusRing(mAnimator, paint, + FADE_OUT_DURATION_MILLIS); + + mAnimator.animations.add(mAutoFocusRing); + mAnimator.animations.add(mManualFocusRing); + + isFirstDraw = true; + mLastRadiusPx = mDefaultRadiusPx; + } + + @Override + public boolean isPassiveFocusRunning() { + return mAutoFocusRing.isActive(); + } + + @Override + public boolean isActiveFocusRunning() { + return mManualFocusRing.isActive(); + } + + @Override + public void startPassiveFocus() { + mAnimator.invalidate(); + long tMs = mAnimator.getTimeMillis(); + + if (mManualFocusRing.isActive() && !mManualFocusRing.isExiting()) { + mManualFocusRing.stop(tMs); + } + + mAutoFocusRing.start(tMs, mLastRadiusPx, mLastRadiusPx); + currentFocusAnimation = mAutoFocusRing; + } + + @Override + public void startActiveFocus() { + mAnimator.invalidate(); + long tMs = mAnimator.getTimeMillis(); + + if (mAutoFocusRing.isActive() && !mAutoFocusRing.isExiting()) { + mAutoFocusRing.stop(tMs); + } + + mManualFocusRing.start(tMs, 0.0f, mLastRadiusPx); + currentFocusAnimation = mManualFocusRing; + } + + @Override + public void stopFocusAnimations() { + long tMs = mAnimator.getTimeMillis(); + if (mManualFocusRing.isActive() && !mManualFocusRing.isExiting() + && !mManualFocusRing.isEntering()) { + mManualFocusRing.exit(tMs); + } + + if (mAutoFocusRing.isActive() && !mAutoFocusRing.isExiting()) { + mAutoFocusRing.exit(tMs); + } + } + + @Override + public void setFocusLocation(float viewX, float viewY) { + mAutoFocusRing.setCenterX((int) viewX); + mAutoFocusRing.setCenterY((int) viewY); + mManualFocusRing.setCenterX((int) viewX); + mManualFocusRing.setCenterY((int) viewY); + } + + @Override + public void centerFocusLocation() { + Point center = computeCenter(); + mAutoFocusRing.setCenterX(center.x); + mAutoFocusRing.setCenterY(center.y); + mManualFocusRing.setCenterX(center.x); + mManualFocusRing.setCenterY(center.y); + } + + @Override + public void setRadiusRatio(float ratio) { + setRadius(mRatioScale.scale(mRatioScale.clamp(ratio))); + } + + @Override + public void configurePreviewDimensions(RectF previewArea) { + mPreviewSize = previewArea; + mLastRadiusPx = mDefaultRadiusPx; + + if (!isFirstDraw) { + centerAutofocusRing(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (isFirstDraw) { + isFirstDraw = false; + centerAutofocusRing(); + } + + if (mPreviewSize != null) { + canvas.clipRect(mPreviewSize, Region.Op.REPLACE); + } + + mAnimator.draw(canvas); + } + + private void setRadius(float radiusPx) { + long tMs = mAnimator.getTimeMillis(); + // Some devices return zero for invalid or "unknown" diopter values. + if (currentFocusAnimation != null && radiusPx > 0.1f) { + currentFocusAnimation.setRadius(tMs, radiusPx); + mLastRadiusPx = radiusPx; + } + } + + private void centerAutofocusRing() { + Point center = computeCenter(); + mAutoFocusRing.setCenterX(center.x); + mAutoFocusRing.setCenterY(center.y); + } + + private Point computeCenter() { + if (mPreviewSize != null && (mPreviewSize.width() * mPreviewSize.height() > 0.01f)) { + Log.i(TAG, "Computing center via preview size."); + return new Point((int) mPreviewSize.centerX(), (int) mPreviewSize.centerY()); + } + Log.i(TAG, "Computing center via view bounds."); + return new Point(getWidth() / 2, getHeight() / 2); + } + + private Paint makePaint(Resources res, int color) { + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(res.getColor(color)); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(res.getDimension(R.dimen.focus_circle_stroke)); + return paint; + } +} diff --git a/src/com/android/camera/ui/focus/FocusSound.java b/src/com/android/camera/ui/focus/FocusSound.java new file mode 100644 index 000000000..c3ff0107d --- /dev/null +++ b/src/com/android/camera/ui/focus/FocusSound.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 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.focus; + +import com.android.camera.SoundPlayer; + +/** + * Wraps the focus sound and the player into a single object that can + * be played on demand. + * + * TODO: This needs some way to better manage the sound lifetimes + */ +public class FocusSound { + private static final float DEFAULT_VOLUME = 0.6f; + private final SoundPlayer mPlayer; + private final int mSoundId; + public FocusSound(SoundPlayer player, int soundId) { + mPlayer = player; + mSoundId = soundId; + + mPlayer.loadSound(mSoundId); + } + + /** + * Play the focus sound with the sound player at the default + * volume. + */ + public void play() { + if(!mPlayer.isReleased()) { + mPlayer.play(mSoundId, DEFAULT_VOLUME); + } + } +} diff --git a/src/com/android/camera/ui/focus/LensRangeCalculator.java b/src/com/android/camera/ui/focus/LensRangeCalculator.java new file mode 100644 index 000000000..ef9cbaec7 --- /dev/null +++ b/src/com/android/camera/ui/focus/LensRangeCalculator.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2015 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.focus; + +import android.annotation.TargetApi; +import android.hardware.camera2.CameraCharacteristics; +import android.os.Build.VERSION_CODES; + +import com.android.camera.ui.motion.LinearScale; + +/** + * Compute diopter range scale to convert lens focus distances into + * a ratio value. + */ +@TargetApi(VERSION_CODES.LOLLIPOP) +public class LensRangeCalculator { + + /** + * A NoOp linear scale for computing diopter values will always return 0 + */ + public static LinearScale getNoOp() { + return new LinearScale(0, 0, 0, 0); + } + + /** + * Compute the focus range from the camera characteristics and build + * a linear scale model that maps a focus distance to a ratio between + * the min and max range. + */ + public static LinearScale getDiopterToRatioCalculator(CameraCharacteristics characteristics) { + // From the android documentation: + // + // 0.0f represents farthest focus, and LENS_INFO_MINIMUM_FOCUS_DISTANCE + // represents the nearest focus the device can achieve. + // + // Example: + // + // Infinity Hyperfocal Minimum Camera + // <----------|-----------------------------| | + // [0.0] [0.31] [14.29] + Float nearest = characteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + Float hyperfocal = characteristics.get(CameraCharacteristics.LENS_INFO_HYPERFOCAL_DISTANCE); + + if (nearest == null && hyperfocal == null) { + return getNoOp(); + } + + nearest = (nearest == null) ? 0.0f : nearest; + hyperfocal = (hyperfocal == null) ? 0.0f : hyperfocal; + + if (nearest > hyperfocal) { + return new LinearScale(hyperfocal, nearest, 0, 1); + } + + return new LinearScale(nearest, hyperfocal, 0, 1); + } +} diff --git a/src/com/android/camera/ui/focus/ManualFocusRing.java b/src/com/android/camera/ui/focus/ManualFocusRing.java new file mode 100644 index 000000000..0133d8e09 --- /dev/null +++ b/src/com/android/camera/ui/focus/ManualFocusRing.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 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.focus; + +import android.graphics.Canvas; +import android.graphics.Paint; + +import com.android.camera.ui.motion.InterpolateUtils; +import com.android.camera.ui.motion.Invalidator; + +/** + * Manual focus ring animation renderer. + */ +class ManualFocusRing extends FocusRingRenderer { + /** + * The manual focus ring encapsulates the animation logic for visualizing + * a focus event when triggered by a physical screen touch. + * + * @param invalidator the object to invalidate while running. + * @param ringPaint the paint to draw the ring with. + * @param exitDurationMillis the fade out time in milliseconds. + */ + public ManualFocusRing(Invalidator invalidator, Paint ringPaint, + float exitDurationMillis) { + super(invalidator, ringPaint, 0.0f, exitDurationMillis); + } + + @Override + public void draw(long t, long dt, Canvas canvas) { + float ringRadius = mRingRadius.update(dt); + processStates(t); + + if (!isActive()) { + return; + } + + mInvalidator.invalidate(); + int ringAlpha = 255; + + if (mFocusState == FocusState.STATE_FADE_OUT) { + float rFade = InterpolateUtils.unitRatio(t, mExitStartMillis, mExitDurationMillis); + ringAlpha = (int) InterpolateUtils.lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_HARD_STOP) { + float rFade = InterpolateUtils.unitRatio(t, mHardExitStartMillis, + mHardExitDurationMillis); + ringAlpha = (int) InterpolateUtils.lerp(255, 0, mExitOpacityCurve.valueAt(rFade)); + } else if (mFocusState == FocusState.STATE_INACTIVE) { + ringAlpha = 0; + } + + mRingPaint.setAlpha(ringAlpha); + canvas.drawCircle(getCenterX(), getCenterY(), ringRadius, mRingPaint); + } + + private void processStates(long t) { + if (mFocusState == FocusState.STATE_INACTIVE) { + return; + } + + if (mFocusState == FocusState.STATE_ENTER + && (t > mEnterStartMillis + mEnterDurationMillis)) { + mFocusState = FocusState.STATE_ACTIVE; + } + + if (mFocusState == FocusState.STATE_ACTIVE && !mRingRadius.isActive()) { + mFocusState = FocusState.STATE_FADE_OUT; + mExitStartMillis = t; + } + + if (mFocusState == FocusState.STATE_FADE_OUT && t > mExitStartMillis + mExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + + if (mFocusState == FocusState.STATE_HARD_STOP + && t > mHardExitStartMillis + mHardExitDurationMillis) { + mFocusState = FocusState.STATE_INACTIVE; + } + } +} diff --git a/src/com/android/camera/ui/motion/AnimationClock.java b/src/com/android/camera/ui/motion/AnimationClock.java new file mode 100644 index 000000000..d2504de6b --- /dev/null +++ b/src/com/android/camera/ui/motion/AnimationClock.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 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.motion; + +import android.os.SystemClock; + +/** + * Wraps the SystemClock static time methods so they can be exercised in tests. + */ +public abstract class AnimationClock { + + public abstract long getTimeMillis(); + + /** + * Forwards calls to SystemClock.uptimeMillis() since it is the most consistent clock for + * animations. + */ + public static class SystemTimeClock extends AnimationClock { + + @Override + public long getTimeMillis() { + return SystemClock.uptimeMillis(); + } + } +} diff --git a/src/com/android/camera/ui/motion/DampedSpring.java b/src/com/android/camera/ui/motion/DampedSpring.java new file mode 100644 index 000000000..84cbfa6f8 --- /dev/null +++ b/src/com/android/camera/ui/motion/DampedSpring.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2014 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.motion; + +/** + * This models a value after the behavior of a spring. The value tracks the current value, a target + * value, and the current velocity and applies both a directional force and a damping force to the + * value on each update call. + */ +public class DampedSpring { + public static final float DEFAULT_TIME_TO_90_PERCENT_MILLIS = 200.0f; + public static final float DEFAULT_SPRING_STIFFNESS = 3.75f; + public static final float EPSILON = 0.01f; + + private final float mSpringStiffness; + private final float mTimeTo90PercentMs; + + private float mTarget = 0f; + private float mVelocity = 0f; + private float mValue = 0f; + + public DampedSpring() { + this(DEFAULT_TIME_TO_90_PERCENT_MILLIS, DEFAULT_SPRING_STIFFNESS); + } + + public DampedSpring(float timeTo90PercentMs) { + this(timeTo90PercentMs, DEFAULT_SPRING_STIFFNESS); + } + + public DampedSpring(float timeTo90PercentMs, float springStiffness) { + // TODO: Assert timeTo90PercentMs >= 1ms, it might behave badly at low values. + // TODO: Assert springStiffness > 2.0f + + mTimeTo90PercentMs = timeTo90PercentMs; + mSpringStiffness = springStiffness; + + if (springStiffness > timeTo90PercentMs) { + throw new IllegalArgumentException("Creating a spring value with " + + "excessive stiffness will oscillate endlessly."); + } + } + + /** + * @return the current value. + */ + public float getValue() { + return mValue; + } + + /** + * @param value the value to set this instance's current state too. + */ + public void setValue(float value) { + mValue = value; + } + + /** + * @return the current target value. + */ + public float getTarget() { + return mTarget; + } + + /** + * Set a target value. The current value will maintain any existing velocity values and will + * move towards the new target value. To forcibly stopAt the value use the stopAt() method. + * + * @param value the new value to move the current value towards. + */ + public void setTarget(float value) { + mTarget = value; + } + + /** + * Update the current value, moving it towards the actual value over the given + * time delta (in milliseconds) since the last update. This works off of the + * principle of a critically damped spring such that any given current value + * will move elastically towards the target value. The current value maintains + * and applies velocity, acceleration, and a damping force to give a continuous, + * smooth transition towards the target value. + * + * @param dtMs the time since the last update, or zero. + * @return the current value after the update occurs. + */ + public float update(float dtMs) { + float dt = dtMs / mTimeTo90PercentMs; + float dts = dt * mSpringStiffness; + + // If the dts > 1, and the velocity is zero, the force will exceed the + // distance to the target value and it will overshoot the value, causing + // weird behavior and unintended oscillation. since a critically damped + // spring should never overshoot the value, simply the current value to the + // target value. + if (dts > 1.0f || dts < 0.0f) { + stop(); + return mValue; + } + + float delta = (mTarget - mValue); + float force = delta - 2.0f * mVelocity; + + mVelocity += force * dts; + mValue += mVelocity * dts; + + // If we get close enough to the actual value, simply set the current value + // to the current target value and stop. + if (!isActive()) { + stop(); + } + + return mValue; + } + + /** + * @return true if this instance has velocity or it is not at the target value. + */ + public boolean isActive() { + boolean hasVelocity = Math.abs(mVelocity) >= EPSILON; + boolean atTarget = Math.abs(mTarget - mValue) < EPSILON; + return hasVelocity || !atTarget; + } + + /** + * Stop the spring motion wherever it is currently at. Sets target to the + * current value and sets the velocity to zero. + */ + public void stop() { + mTarget = mValue; + mVelocity = 0.0f; + } +} diff --git a/src/com/android/camera/ui/motion/DynamicAnimation.java b/src/com/android/camera/ui/motion/DynamicAnimation.java new file mode 100644 index 000000000..57d5a1021 --- /dev/null +++ b/src/com/android/camera/ui/motion/DynamicAnimation.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 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.motion; + +import android.graphics.Canvas; + +/** + * Rendering object that can be driven by an animator instance. + */ +public interface DynamicAnimation { + + /** + * Check to determine if this animation is currently in a stable state. + * + * @return true if the animation is stable, false if it should continue to be redrawn. + */ + boolean isActive(); + + /** + * Update and draw the animation onto the given canvas. + * + * @param t current animation frame time. + * @param dt delta since the last update. + * @param canvas the canvas to draw the animation onto. + */ + void draw(long t, long dt, Canvas canvas); +} diff --git a/src/com/android/camera/ui/motion/DynamicAnimator.java b/src/com/android/camera/ui/motion/DynamicAnimator.java new file mode 100644 index 000000000..542ac1e37 --- /dev/null +++ b/src/com/android/camera/ui/motion/DynamicAnimator.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2014 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.motion; + +import android.graphics.Canvas; + +import java.util.ArrayList; +import java.util.List; + +/** + * Designed to handle the lifecycle of a view that needs a continuous update / + * redraw cycle that does not have a defined start / end time. + * + * Fixed length animations should NOT use this class. + */ +public class DynamicAnimator implements Invalidator { + + public final List<DynamicAnimation> animations = new ArrayList<>(); + + private final Invalidator mInvalidator; + private final AnimationClock mClock; + + private boolean mUpdateRequested = false; + private boolean mIsDrawing = false; + private long mLastDrawTimeMillis = 0; + private long mDrawTimeMillis = 0; + + public DynamicAnimator(Invalidator invalidator, AnimationClock clock) { + mInvalidator = invalidator; + mClock = clock; + } + + public void draw(Canvas canvas) { + mIsDrawing = true; + mUpdateRequested = false; + + mDrawTimeMillis = mClock.getTimeMillis(); + + if (mLastDrawTimeMillis <= 0) { + mLastDrawTimeMillis = mDrawTimeMillis; // On the initial draw, dt is zero. + } + + long dt = mDrawTimeMillis - mLastDrawTimeMillis; + mLastDrawTimeMillis = mDrawTimeMillis; + + // Run the animation + for (DynamicAnimation renderer : animations) { + if (renderer.isActive()) { + renderer.draw(mDrawTimeMillis, dt, canvas); + } + } + + // If either the update or the draw methods requested new frames, then + // invalidate the view which should give us another frame to work with. + // Otherwise, stopAt the last update time. + if (mUpdateRequested) { + mInvalidator.invalidate(); + } else { + mLastDrawTimeMillis = -1; + } + + mIsDrawing = false; + } + + /** + * If a scheduleNewFrame request comes in outside of the animation loop, + * and we didn't schedule a frame after the previous loop (or it's the + * first time we've used this instance), invalidate the view and set the + * last update time to the current time. Theoretically, a few milliseconds + * have elapsed before the view gets updated. + */ + @Override + public void invalidate() { + if (!mIsDrawing && !mUpdateRequested) { + mInvalidator.invalidate(); + mLastDrawTimeMillis = mClock.getTimeMillis(); + } + + mUpdateRequested = true; + } + + /** + * This will return the "best guess" for the most current animation frame + * time. If the loop is currently drawing, then it will return the time the + * draw began, and if an update is currently requested it will return the + * time that the update was requested at, and if neither of these are true + * it will return the current system clock time. + * + * This method will not trigger a new update. + */ + public long getTimeMillis() { + if (mIsDrawing) { + return mDrawTimeMillis; + } + + if (mUpdateRequested) { + return mLastDrawTimeMillis; + } + + return mClock.getTimeMillis(); + } +} diff --git a/src/com/android/camera/ui/motion/InterpolateUtils.java b/src/com/android/camera/ui/motion/InterpolateUtils.java new file mode 100644 index 000000000..3c3cd532f --- /dev/null +++ b/src/com/android/camera/ui/motion/InterpolateUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 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.motion; + +/** + * Various static helper functions for interpolating between values. + */ +public class InterpolateUtils { + + private InterpolateUtils() { + } + + /** + * Linear interpolation from v0 to v1 as t goes from 0...1 + * + * @param v0 the value at t=0 + * @param v1 the value at t=1 + * @param t value in the range of 0 to 1. + * @return the value between v0 and v1 as a ratio between 0 and 1 defined by t. + */ + public static float lerp(float v0, float v1, float t) { + return v0 + t * (v1 - v0); + } + + /** + * Project a value that is within the in(Min/Max) number space into the to(Min/Max) number + * space. + * + * @param v value to scale into the 'to' number space. + * @param vMin min value of the values number space. + * @param vMax max value of the values number space. + * @param pMin min value of the projection number space. + * @param pMax max value of the projection number space. + * @return the ratio of the value in the source number space as a value in the to(Min/Max) + * number space. + */ + public static float scale(float v, float vMin, float vMax, float pMin, float pMax) { + return (pMax - pMin) * (v - vMin) / (vMax - vMin) + pMin; + } + + /** + * Value between 0 and 1 as a ratio between tBegin over tDuration + * with no upper bound. + */ + public static float unitRatio(long t, long tBegin, float tDuration) { + if (t <= tBegin) { + return 0.0f; + } + + return (t - tBegin) / tDuration; + } +} diff --git a/src/com/android/camera/ui/motion/InterpolatorHelper.java b/src/com/android/camera/ui/motion/InterpolatorHelper.java new file mode 100644 index 000000000..84114cb03 --- /dev/null +++ b/src/com/android/camera/ui/motion/InterpolatorHelper.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 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.motion; + +import android.content.Context; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; + +import com.android.camera.util.ApiHelper; + +public class InterpolatorHelper { + private static Interpolator LINEAR_OUT_SLOW_IN = null; + + public static Interpolator getLinearOutSlowInInterpolator(final Context context) { + if (LINEAR_OUT_SLOW_IN != null) { + return LINEAR_OUT_SLOW_IN; + } + + LINEAR_OUT_SLOW_IN = AnimationUtils.loadInterpolator( + context, android.R.interpolator.linear_out_slow_in); + return LINEAR_OUT_SLOW_IN; + } +} diff --git a/src/com/android/camera/ui/motion/Invalidator.java b/src/com/android/camera/ui/motion/Invalidator.java new file mode 100644 index 000000000..fdb548748 --- /dev/null +++ b/src/com/android/camera/ui/motion/Invalidator.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 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.motion; + +/** + * Basic interface for objects that can be invalidated. + */ +public interface Invalidator { + /** + * Request that the object should be redrawn whenever it gets + * the chance. + */ + void invalidate(); +} diff --git a/src/com/android/camera/ui/motion/LinearScale.java b/src/com/android/camera/ui/motion/LinearScale.java new file mode 100644 index 000000000..5886f6882 --- /dev/null +++ b/src/com/android/camera/ui/motion/LinearScale.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2015 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.motion; + +/** + * Represents a discrete linear scale function. + */ +public final class LinearScale { + private final float mDomainA; + private final float mDomainB; + private final float mRangeA; + private final float mRangeB; + + private final float mScale; + + public LinearScale(float domainA, float domainB, float rangeA, float rangeB) { + mDomainA = domainA; + mDomainB = domainB; + mRangeA = rangeA; + mRangeB = rangeB; + + // Precomputed ratio between input domain and output range. + float scale = (mRangeB - mRangeA) / (mDomainB - mDomainA); + mScale = Float.isNaN(scale) ? 0.0f : scale; + } + + /** + * Clamp a given domain value to the given domain. + */ + public float clamp(float domainValue) { + if (mDomainA > mDomainB) { + return Math.max(mDomainB, Math.min(mDomainA, domainValue)); + } + + return Math.max(mDomainA, Math.min(mDomainB, domainValue)); + } + + /** + * Returns true if the value is within the domain. + */ + public boolean isInDomain(float domainValue) { + if (mDomainA > mDomainB) { + return domainValue <= mDomainA && domainValue >= mDomainB; + } + return domainValue >= mDomainA && domainValue <= mDomainB; + } + + /** + * Linearly scale a given domain value into the output range. + */ + public float scale(float domainValue) { + return mRangeA + (domainValue - mDomainA) * mScale; + } + + /** + * For the current domain and range parameters produce a new scale function + * that is the inverse of the current scale function. + */ + public LinearScale inverse() { + return new LinearScale(mRangeA, mRangeB, mDomainA, mDomainB); + } + + @Override + public String toString() { + return "LinearScale{" + + "mDomainA=" + mDomainA + + ", mDomainB=" + mDomainB + + ", mRangeA=" + mRangeA + + ", mRangeB=" + mRangeB + "}"; + } +}
\ No newline at end of file diff --git a/src/com/android/camera/ui/motion/UnitBezier.java b/src/com/android/camera/ui/motion/UnitBezier.java new file mode 100644 index 000000000..242f54556 --- /dev/null +++ b/src/com/android/camera/ui/motion/UnitBezier.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2014 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.motion; + +/** + * This represents is a precomputed cubic bezier curve starting at (0,0) and + * going to (1,1) with two configurable control points. Once the instance is + * created, the control points cannot be modified. + * + * Generally, this will be used for computing timing curves for with control + * points where an x value will be provide from 0.0 - 1.0, and the y value will + * be solved for where y is used as the timing value in some linear + * interpolation of a value. + */ +public class UnitBezier implements UnitCurve { + + private static final float EPSILON = 1e-6f; + + private final DerivableFloatFn mXFn; + private final DerivableFloatFn mYFn; + + /** + * Build and pre-compute a unit bezier. This assumes a starting point of + * (0, 0) and end point of (1.0, 1.0). + * + * @param c0x control point x value for p0 + * @param c0y control point y value for p0 + * @param c1x control point x value for p1 + * @param c1y control point y value for p1 + */ + public UnitBezier(float c0x, float c0y, float c1x, float c1y) { + mXFn = new CubicBezierFn(c0x, c1x); + mYFn = new CubicBezierFn(c0y, c1y); + } + + /** + * Given a unit bezier curve find the height of the curve at t (which is + * internally represented as the xAxis). + * + * @param t the x position between 0 and 1 to solve for y. + * @return the closest approximate height of the curve at x. + */ + @Override + public float valueAt(float t) { + return mYFn.value(solve(t, mXFn)); + } + + /** + * Given a unit bezier curve find a value along the x axis such that + * valueAt(result) produces the input value. + * + * @param value the y position between 0 and 1 to solve for x + * @return the closest approximate input that will produce value when provided + * to the valueAt function. + */ + @Override + public float tAt(float value) { + return mXFn.value(solve(value, mYFn)); + } + + private float solve(float target, DerivableFloatFn fn) { + // For a linear fn, t = value. This makes value a good starting guess. + float input = target; + + // Newton's method (Faster than bisection) + for (int i = 0; i < 8; i++) { + float value = fn.value(input) - target; + if (Math.abs(value) < EPSILON) { + return input; + } + float derivative = fn.derivative(input); + if (Math.abs(derivative) < EPSILON) { + break; + } + input = input - value / derivative; + } + + // Fallback on bi-section + float min = 0.0f; + float max = 1.0f; + input = target; + + if (input < min) { + return min; + } + if (input > max) { + return max; + } + + while (min < max) { + float value = fn.value(input); + if (Math.abs(value - target) < EPSILON) { + return input; + } + + if (target > value) { + min = input; + } else { + max = input; + } + + input = (max - min) * .5f + min; + } + + // Give up, return the closest match we got too. + return input; + } + + private interface DerivableFloatFn { + float value(float x); + float derivative(float x); + } + + /** + * Precomputed constants for a given set of control points along a given + * cubic bezier axis. + */ + private static class CubicBezierFn implements DerivableFloatFn { + private final float c; + private final float a; + private final float b; + + /** + * Build and pre-compute a single axis for a unit bezier. This assumes p0 + * is 0 and p1 is 1. + * + * @param c0 start control point. + * @param c1 end control point. + */ + public CubicBezierFn(float c0, float c1) { + c = 3.0f * c0; + b = 3.0f * (c1 - c0) - c; + a = 1.0f - c - b; + } + + public float value(float x) { + return ((a * x + b) * x + c) * x; + } + public float derivative(float x) { + return (3.0f * a * x + 2.0f * b) * x + c; + } + } +} diff --git a/src/com/android/camera/ui/motion/UnitCurve.java b/src/com/android/camera/ui/motion/UnitCurve.java new file mode 100644 index 000000000..d89f1fa4d --- /dev/null +++ b/src/com/android/camera/ui/motion/UnitCurve.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 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.motion; + +/** + * Simple functions that produce values along a curve for any given input and can compute input + * times for a given output value. + */ +public interface UnitCurve { + + /** + * Produce a unit value of this curve at time t. The function should always return a valid + * return value for any valid t input. + * + * @param t ratio of time passed from (0..1) + * @return the unit value at t. + */ + float valueAt(float t); + + /** + * If possible, find a value for t such that valueAt(t) == value or best guess. + * + * @param value to match to the output of valueAt(t) + * @return t where valueAt(t) == value or throw. + */ + float tAt(float value); +} diff --git a/src/com/android/camera/ui/motion/UnitCurves.java b/src/com/android/camera/ui/motion/UnitCurves.java new file mode 100644 index 000000000..a1117fa96 --- /dev/null +++ b/src/com/android/camera/ui/motion/UnitCurves.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 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.motion; + +/** + * Predefined material curves and animations. + */ +public class UnitCurves { + public static final UnitCurve FAST_OUT_SLOW_IN = new UnitBezier(0.4f, 0.0f, 0.2f, 1.0f); + public static final UnitCurve LINEAR_OUT_SLOW_IN = new UnitBezier(0.0f, 0.0f, 0.2f, 1.0f); + public static final UnitCurve FAST_OUT_LINEAR_IN = new UnitBezier(0.4f, 0.0f, 1.0f, 1.0f); + public static final UnitCurve LINEAR = new UnitBezier(0.0f, 0.0f, 1.0f, 1.0f); + + /** + * Given two curves (from and to) and a time along the from curve, compute + * the time at t, and find a t along the 'toCurve' that will produce the + * same output. This is useful when interpolating between two different curves + * when the animation is not at the beginning or end. + * + * @param enterCurve the curve to compute the value from + * @param exitCurve the curve to find a time t on that matches output of + * enterCurve at T. + * @param t the time at which to compute the value (0..1) + * @return the time along the exitCurve. + */ + public static float mapEnterCurveToExitCurveAtT(UnitCurve enterCurve, UnitCurve exitCurve, + float t) { + return exitCurve.tAt(1 - enterCurve.valueAt(t)); + } +} |