diff options
Diffstat (limited to 'src/com/android/camera/ui')
38 files changed, 7597 insertions, 0 deletions
diff --git a/src/com/android/camera/ui/AbstractSettingPopup.java b/src/com/android/camera/ui/AbstractSettingPopup.java new file mode 100644 index 000000000..783b6c771 --- /dev/null +++ b/src/com/android/camera/ui/AbstractSettingPopup.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.gallery3d.R; + +// A popup window that shows one or more camera settings. +abstract public class AbstractSettingPopup extends RotateLayout { + protected ViewGroup mSettingList; + protected TextView mTitle; + + public AbstractSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mTitle = (TextView) findViewById(R.id.title); + mSettingList = (ViewGroup) findViewById(R.id.settingList); + } + + abstract public void reloadPreference(); +} diff --git a/src/com/android/camera/ui/CameraControls.java b/src/com/android/camera/ui/CameraControls.java new file mode 100644 index 000000000..7fa6890a7 --- /dev/null +++ b/src/com/android/camera/ui/CameraControls.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.camera.Util; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +public class CameraControls extends RotatableLayout { + + private static final String TAG = "CAM_Controls"; + + private View mBackgroundView; + private View mShutter; + private View mSwitcher; + private View mMenu; + private View mIndicators; + private View mPreview; + + public CameraControls(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CameraControls(Context context) { + super(context); + } + + @Override + public void onFinishInflate() { + super.onFinishInflate(); + mBackgroundView = findViewById(R.id.blocker); + mSwitcher = findViewById(R.id.camera_switcher); + mShutter = findViewById(R.id.shutter_button); + mMenu = findViewById(R.id.menu); + mIndicators = findViewById(R.id.on_screen_indicators); + mPreview = findViewById(R.id.preview_thumb); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + int orientation = getResources().getConfiguration().orientation; + int size = getResources().getDimensionPixelSize(R.dimen.camera_controls_size); + int rotation = getUnifiedRotation(); + adjustBackground(); + // As l,t,r,b are positions relative to parents, we need to convert them + // to child's coordinates + 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); + } + Rect shutter = new Rect(); + topRight(mPreview, l, t, r, b); + if (size > 0) { + // restrict controls to size + switch (rotation) { + case 0: + case 180: + l = (l + r - size) / 2; + r = l + size; + break; + case 90: + case 270: + t = (t + b - size) / 2; + b = t + size; + break; + } + } + center(mShutter, l, t, r, b, orientation, rotation, shutter); + center(mBackgroundView, l, t, r, b, orientation, rotation, new Rect()); + toLeft(mSwitcher, shutter, rotation); + toRight(mMenu, shutter, rotation); + toRight(mIndicators, shutter, rotation); + View retake = findViewById(R.id.btn_retake); + if (retake != null) { + center(retake, shutter, rotation); + View cancel = findViewById(R.id.btn_cancel); + toLeft(cancel, shutter, rotation); + View done = findViewById(R.id.btn_done); + toRight(done, shutter, rotation); + } + } + + private void center(View v, int l, int t, int r, int b, int orientation, int rotation, Rect result) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin; + switch (rotation) { + case 0: + // phone portrait; controls bottom + result.left = (r + l) / 2 - tw / 2 + lp.leftMargin; + result.right = (r + l) / 2 + tw / 2 - lp.rightMargin; + result.bottom = b - lp.bottomMargin; + result.top = b - th + lp.topMargin; + break; + case 90: + // phone landscape: controls right + result.right = r - lp.rightMargin; + result.left = r - tw + lp.leftMargin; + result.top = (b + t) / 2 - th / 2 + lp.topMargin; + result.bottom = (b + t) / 2 + th / 2 - lp.bottomMargin; + break; + case 180: + // phone upside down: controls top + result.left = (r + l) / 2 - tw / 2 + lp.leftMargin; + result.right = (r + l) / 2 + tw / 2 - lp.rightMargin; + result.top = t + lp.topMargin; + result.bottom = t + th - lp.bottomMargin; + break; + case 270: + // reverse landscape: controls left + result.left = l + lp.leftMargin; + result.right = l + tw - lp.rightMargin; + result.top = (b + t) / 2 - th / 2 + lp.topMargin; + result.bottom = (b + t) / 2 + th / 2 - lp.bottomMargin; + break; + } + v.layout(result.left, result.top, result.right, result.bottom); + } + + private void center(View v, Rect other, int rotation) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin; + int cx = (other.left + other.right) / 2; + int cy = (other.top + other.bottom) / 2; + v.layout(cx - tw / 2 + lp.leftMargin, + cy - th / 2 + lp.topMargin, + cx + tw / 2 - lp.rightMargin, + cy + th / 2 - lp.bottomMargin); + } + + private void toLeft(View v, Rect other, int rotation) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin; + int cx = (other.left + other.right) / 2; + int cy = (other.top + other.bottom) / 2; + int l = 0, r = 0, t = 0, b = 0; + switch (rotation) { + case 0: + // portrait, to left of anchor at bottom + l = other.left - tw + lp.leftMargin; + r = other.left - lp.rightMargin; + t = cy - th / 2 + lp.topMargin; + b = cy + th / 2 - lp.bottomMargin; + break; + case 90: + // phone landscape: below anchor on right + l = cx - tw / 2 + lp.leftMargin; + r = cx + tw / 2 - lp.rightMargin; + t = other.bottom + lp.topMargin; + b = other.bottom + th - lp.bottomMargin; + break; + case 180: + // phone upside down: right of anchor at top + l = other.right + lp.leftMargin; + r = other.right + tw - lp.rightMargin; + t = cy - th / 2 + lp.topMargin; + b = cy + th / 2 - lp.bottomMargin; + break; + case 270: + // reverse landscape: above anchor on left + l = cx - tw / 2 + lp.leftMargin; + r = cx + tw / 2 - lp.rightMargin; + t = other.top - th + lp.topMargin; + b = other.top - lp.bottomMargin; + break; + } + v.layout(l, t, r, b); + } + + private void toRight(View v, Rect other, int rotation) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams(); + int tw = lp.leftMargin + v.getMeasuredWidth() + lp.rightMargin; + int th = lp.topMargin + v.getMeasuredHeight() + lp.bottomMargin; + int cx = (other.left + other.right) / 2; + int cy = (other.top + other.bottom) / 2; + int l = 0, r = 0, t = 0, b = 0; + switch (rotation) { + case 0: + l = other.right + lp.leftMargin; + r = other.right + tw - lp.rightMargin; + t = cy - th / 2 + lp.topMargin; + b = cy + th / 2 - lp.bottomMargin; + break; + case 90: + l = cx - tw / 2 + lp.leftMargin; + r = cx + tw / 2 - lp.rightMargin; + t = other.top - th + lp.topMargin; + b = other.top - lp.bottomMargin; + break; + case 180: + l = other.left - tw + lp.leftMargin; + r = other.left - lp.rightMargin; + t = cy - th / 2 + lp.topMargin; + b = cy + th / 2 - lp.bottomMargin; + break; + case 270: + l = cx - tw / 2 + lp.leftMargin; + r = cx + tw / 2 - lp.rightMargin; + t = other.bottom + lp.topMargin; + b = other.bottom + th - lp.bottomMargin; + break; + } + v.layout(l, t, r, b); + } + + private void topRight(View v, int l, int t, int r, int b) { + // layout using the specific margins; the rotation code messes up the others + int mt = getContext().getResources().getDimensionPixelSize(R.dimen.capture_margin_top); + int mr = getContext().getResources().getDimensionPixelSize(R.dimen.capture_margin_right); + v.layout(r - v.getMeasuredWidth() - mr, t + mt, r - mr, t + mt + v.getMeasuredHeight()); + } + + private void adjustBackground() { + int rotation = getUnifiedRotation(); + // remove current drawable and reset rotation + mBackgroundView.setBackgroundDrawable(null); + mBackgroundView.setRotationX(0); + mBackgroundView.setRotationY(0); + // if the switcher background is top aligned we need to flip the background + // drawable vertically; if left aligned, flip horizontally + switch (rotation) { + case 180: + mBackgroundView.setRotationX(180); + break; + case 270: + mBackgroundView.setRotationY(180); + break; + default: + break; + } + mBackgroundView.setBackgroundResource(R.drawable.switcher_bg); + } + +} diff --git a/src/com/android/camera/ui/CameraRootView.java b/src/com/android/camera/ui/CameraRootView.java new file mode 100644 index 000000000..adda70697 --- /dev/null +++ b/src/com/android/camera/ui/CameraRootView.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.DisplayListener; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.camera.Util; +import com.android.gallery3d.common.ApiHelper; + +public class CameraRootView extends FrameLayout { + + private int mTopMargin = 0; + private int mBottomMargin = 0; + private int mLeftMargin = 0; + private int mRightMargin = 0; + private Rect mCurrentInsets; + private int mOffset = 0; + private Object mDisplayListener; + private MyDisplayListener mListener; + public interface MyDisplayListener { + public void onDisplayChanged(); + } + + public CameraRootView(Context context, AttributeSet attrs) { + super(context, attrs); + initDisplayListener(); + setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + super.fitSystemWindows(insets); + mCurrentInsets = insets; + // insets include status bar, navigation bar, etc + // In this case, we are only concerned with the size of nav bar + if (mOffset > 0) return true; + + if (insets.bottom > 0) { + mOffset = insets.bottom; + } else if (insets.right > 0) { + mOffset = insets.right; + } + return true; + } + + public void initDisplayListener() { + if (ApiHelper.HAS_DISPLAY_LISTENER) { + mDisplayListener = new DisplayListener() { + + @Override + public void onDisplayAdded(int arg0) {} + + @Override + public void onDisplayChanged(int arg0) { + mListener.onDisplayChanged(); + } + + @Override + public void onDisplayRemoved(int arg0) {} + }; + } + } + + public void setDisplayChangeListener(MyDisplayListener listener) { + mListener = listener; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + if (ApiHelper.HAS_DISPLAY_LISTENER) { + ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) + .registerDisplayListener((DisplayListener) mDisplayListener, null); + } + } + + @Override + public void onDetachedFromWindow () { + super.onDetachedFromWindow(); + if (ApiHelper.HAS_DISPLAY_LISTENER) { + ((DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE)) + .unregisterDisplayListener((DisplayListener) mDisplayListener); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int rotation = Util.getDisplayRotation((Activity) getContext()); + // all the layout code assumes camera device orientation to be portrait + // adjust rotation for landscape + int orientation = getResources().getConfiguration().orientation; + int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT + : Configuration.ORIENTATION_LANDSCAPE; + if (camOrientation != orientation) { + rotation = (rotation + 90) % 360; + } + // calculate margins + mLeftMargin = 0; + mRightMargin = 0; + mBottomMargin = 0; + mTopMargin = 0; + switch (rotation) { + case 0: + mBottomMargin += mOffset; + break; + case 90: + mRightMargin += mOffset; + break; + case 180: + mTopMargin += mOffset; + break; + case 270: + mLeftMargin += mOffset; + break; + } + if (mCurrentInsets != null) { + if (mCurrentInsets.right > 0) { + // navigation bar on the right + mRightMargin = mRightMargin > 0 ? mRightMargin : mCurrentInsets.right; + } else { + // navigation bar on the bottom + mBottomMargin = mBottomMargin > 0 ? mBottomMargin : mCurrentInsets.bottom; + } + } + // make sure all the children are resized + super.onMeasure(widthMeasureSpec - mLeftMargin - mRightMargin, + heightMeasureSpec - mTopMargin - mBottomMargin); + setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + r -= l; + b -= t; + l = 0; + t = 0; + int orientation = getResources().getConfiguration().orientation; + // Lay out children + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + if (v instanceof CameraControls) { + // Lay out camera controls to center on the short side of the screen + // so that they stay in place during rotation + int width = v.getMeasuredWidth(); + int height = v.getMeasuredHeight(); + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + int left = (l + r - width) / 2; + v.layout(left, t + mTopMargin, left + width, b - mBottomMargin); + } else { + int top = (t + b - height) / 2; + v.layout(l + mLeftMargin, top, r - mRightMargin, top + height); + } + } else { + v.layout(l + mLeftMargin, t + mTopMargin, r - mRightMargin, b - mBottomMargin); + } + } + } +} diff --git a/src/com/android/camera/ui/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java new file mode 100644 index 000000000..6e4321571 --- /dev/null +++ b/src/com/android/camera/ui/CameraSwitcher.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.widget.FrameLayout.LayoutParams; +import android.widget.LinearLayout; + +import com.android.camera.Util; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.LightCycleHelper; +import com.android.gallery3d.util.UsageStatistics; + +public class CameraSwitcher extends RotateImageView + implements OnClickListener, OnTouchListener { + + private static final String TAG = "CAM_Switcher"; + private static final int SWITCHER_POPUP_ANIM_DURATION = 200; + + public static final int PHOTO_MODULE_INDEX = 0; + public static final int VIDEO_MODULE_INDEX = 1; + public static final int LIGHTCYCLE_MODULE_INDEX = 2; + public static final int REFOCUS_MODULE_INDEX = 3; + private static final int[] DRAW_IDS = { + R.drawable.ic_switch_camera, + R.drawable.ic_switch_video, + R.drawable.ic_switch_photosphere, + R.drawable.ic_switch_refocus + }; + public interface CameraSwitchListener { + public void onCameraSelected(int i); + public void onShowSwitcherPopup(); + } + + private CameraSwitchListener mListener; + private int mCurrentIndex; + private int[] mModuleIds; + private int[] mDrawIds; + private int mItemSize; + private View mPopup; + private View mParent; + private boolean mShowingPopup; + private boolean mNeedsAnimationSetup; + private Drawable mIndicator; + + private float mTranslationX = 0; + private float mTranslationY = 0; + + private AnimatorListener mHideAnimationListener; + private AnimatorListener mShowAnimationListener; + + public CameraSwitcher(Context context) { + super(context); + init(context); + } + + public CameraSwitcher(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + private void init(Context context) { + mItemSize = context.getResources().getDimensionPixelSize(R.dimen.switcher_size); + setOnClickListener(this); + mIndicator = context.getResources().getDrawable(R.drawable.ic_switcher_menu_indicator); + initializeDrawables(context); + } + + public void initializeDrawables(Context context) { + int totaldrawid = (LightCycleHelper.hasLightCycleCapture(context) + ? DRAW_IDS.length : DRAW_IDS.length - 1); + + int[] drawids = new int[totaldrawid]; + int[] moduleids = new int[totaldrawid]; + int ix = 0; + for (int i = 0; i < DRAW_IDS.length; i++) { + if (i == LIGHTCYCLE_MODULE_INDEX && !LightCycleHelper.hasLightCycleCapture(context)) { + continue; // not enabled, so don't add to UI + } + moduleids[ix] = i; + drawids[ix++] = DRAW_IDS[i]; + } + setIds(moduleids, drawids); + } + + public void setIds(int[] moduleids, int[] drawids) { + mDrawIds = drawids; + mModuleIds = moduleids; + } + + public void setCurrentIndex(int i) { + mCurrentIndex = i; + setImageResource(mDrawIds[i]); + } + + public void setSwitchListener(CameraSwitchListener l) { + mListener = l; + } + + @Override + public void onClick(View v) { + showSwitcher(); + mListener.onShowSwitcherPopup(); + } + + private void onCameraSelected(int ix) { + hidePopup(); + if ((ix != mCurrentIndex) && (mListener != null)) { + UsageStatistics.onEvent("CameraModeSwitch", null, null); + UsageStatistics.setPendingTransitionCause( + UsageStatistics.TRANSITION_MENU_TAP); + setCurrentIndex(ix); + mListener.onCameraSelected(mModuleIds[ix]); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + mIndicator.setBounds(getDrawable().getBounds()); + mIndicator.draw(canvas); + } + + private void initPopup() { + mParent = LayoutInflater.from(getContext()).inflate(R.layout.switcher_popup, + (ViewGroup) getParent()); + LinearLayout content = (LinearLayout) mParent.findViewById(R.id.content); + mPopup = content; + // Set the gravity of the popup, so that it shows up at the right position + // on screen + LayoutParams lp = ((LayoutParams) mPopup.getLayoutParams()); + lp.gravity = ((LayoutParams) mParent.findViewById(R.id.camera_switcher) + .getLayoutParams()).gravity; + mPopup.setLayoutParams(lp); + + mPopup.setVisibility(View.INVISIBLE); + mNeedsAnimationSetup = true; + for (int i = mDrawIds.length - 1; i >= 0; i--) { + RotateImageView item = new RotateImageView(getContext()); + item.setImageResource(mDrawIds[i]); + item.setBackgroundResource(R.drawable.bg_pressed); + final int index = i; + item.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (showsPopup()) onCameraSelected(index); + } + }); + switch (mDrawIds[i]) { + case R.drawable.ic_switch_camera: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_camera)); + break; + case R.drawable.ic_switch_video: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_video)); + break; + case R.drawable.ic_switch_photosphere: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_new_panorama)); + break; + case R.drawable.ic_switch_refocus: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_refocus)); + break; + default: + break; + } + content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize)); + } + mPopup.measure(MeasureSpec.makeMeasureSpec(mParent.getWidth(), MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(mParent.getHeight(), MeasureSpec.AT_MOST)); + } + + public boolean showsPopup() { + return mShowingPopup; + } + + public boolean isInsidePopup(MotionEvent evt) { + if (!showsPopup()) return false; + int topLeft[] = new int[2]; + mPopup.getLocationOnScreen(topLeft); + int left = topLeft[0]; + int top = topLeft[1]; + int bottom = top + mPopup.getHeight(); + int right = left + mPopup.getWidth(); + return evt.getX() >= left && evt.getX() < right + && evt.getY() >= top && evt.getY() < bottom; + } + + private void hidePopup() { + mShowingPopup = false; + setVisibility(View.VISIBLE); + if (mPopup != null && !animateHidePopup()) { + mPopup.setVisibility(View.INVISIBLE); + } + mParent.setOnTouchListener(null); + } + + @Override + public void onConfigurationChanged(Configuration config) { + if (showsPopup()) { + ((ViewGroup) mParent).removeView(mPopup); + mPopup = null; + initPopup(); + mPopup.setVisibility(View.VISIBLE); + } + } + + private void showSwitcher() { + mShowingPopup = true; + if (mPopup == null) { + initPopup(); + } + layoutPopup(); + mPopup.setVisibility(View.VISIBLE); + if (!animateShowPopup()) { + setVisibility(View.INVISIBLE); + } + mParent.setOnTouchListener(this); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + closePopup(); + return true; + } + + public void closePopup() { + if (showsPopup()) { + hidePopup(); + } + } + + @Override + public void setOrientation(int degree, boolean animate) { + super.setOrientation(degree, animate); + ViewGroup content = (ViewGroup) mPopup; + if (content == null) return; + for (int i = 0; i < content.getChildCount(); i++) { + RotateImageView iv = (RotateImageView) content.getChildAt(i); + iv.setOrientation(degree, animate); + } + } + + private void layoutPopup() { + int orientation = Util.getDisplayRotation((Activity) getContext()); + int w = mPopup.getMeasuredWidth(); + int h = mPopup.getMeasuredHeight(); + if (orientation == 0) { + mPopup.layout(getRight() - w, getBottom() - h, getRight(), getBottom()); + mTranslationX = 0; + mTranslationY = h / 3; + } else if (orientation == 90) { + mTranslationX = w / 3; + mTranslationY = - h / 3; + mPopup.layout(getRight() - w, getTop(), getRight(), getTop() + h); + } else if (orientation == 180) { + mTranslationX = - w / 3; + mTranslationY = - h / 3; + mPopup.layout(getLeft(), getTop(), getLeft() + w, getTop() + h); + } else { + mTranslationX = - w / 3; + mTranslationY = h - getHeight(); + mPopup.layout(getLeft(), getBottom() - h, getLeft() + w, getBottom()); + } + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mPopup != null) { + layoutPopup(); + } + } + + private void popupAnimationSetup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return; + } + layoutPopup(); + mPopup.setScaleX(0.3f); + mPopup.setScaleY(0.3f); + mPopup.setTranslationX(mTranslationX); + mPopup.setTranslationY(mTranslationY); + mNeedsAnimationSetup = false; + } + + private boolean animateHidePopup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return false; + } + if (mHideAnimationListener == null) { + mHideAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Verify that we weren't canceled + if (!showsPopup() && mPopup != null) { + mPopup.setVisibility(View.INVISIBLE); + ((ViewGroup) mParent).removeView(mPopup); + mPopup = null; + } + } + }; + } + mPopup.animate() + .alpha(0f) + .scaleX(0.3f).scaleY(0.3f) + .translationX(mTranslationX) + .translationY(mTranslationY) + .setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(mHideAnimationListener); + animate().alpha(1f).setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(null); + return true; + } + + private boolean animateShowPopup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return false; + } + if (mNeedsAnimationSetup) { + popupAnimationSetup(); + } + if (mShowAnimationListener == null) { + mShowAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + // Verify that we weren't canceled + if (showsPopup()) { + setVisibility(View.INVISIBLE); + // request layout to make sure popup is laid out correctly on ICS + mPopup.requestLayout(); + } + } + }; + } + mPopup.animate() + .alpha(1f) + .scaleX(1f).scaleY(1f) + .translationX(0) + .translationY(0) + .setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(null); + animate().alpha(0f).setDuration(SWITCHER_POPUP_ANIM_DURATION) + .setListener(mShowAnimationListener); + return true; + } +} diff --git a/src/com/android/camera/ui/CheckedLinearLayout.java b/src/com/android/camera/ui/CheckedLinearLayout.java new file mode 100644 index 000000000..4e7750499 --- /dev/null +++ b/src/com/android/camera/ui/CheckedLinearLayout.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Checkable; +import android.widget.LinearLayout; + +public class CheckedLinearLayout extends LinearLayout implements Checkable { + private static final int[] CHECKED_STATE_SET = { + android.R.attr.state_checked + }; + private boolean mChecked; + + public CheckedLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public boolean isChecked() { + return mChecked; + } + + @Override + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + refreshDrawableState(); + } + } + + @Override + public void toggle() { + setChecked(!mChecked); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + if (mChecked) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } +} diff --git a/src/com/android/camera/ui/CountDownView.java b/src/com/android/camera/ui/CountDownView.java new file mode 100644 index 000000000..907d33508 --- /dev/null +++ b/src/com/android/camera/ui/CountDownView.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import java.util.Locale; + +import android.content.Context; +import android.media.AudioManager; +import android.media.SoundPool; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.gallery3d.R; + +public class CountDownView extends FrameLayout { + + private static final String TAG = "CAM_CountDownView"; + private static final int SET_TIMER_TEXT = 1; + private TextView mRemainingSecondsView; + private int mRemainingSecs = 0; + private OnCountDownFinishedListener mListener; + private Animation mCountDownAnim; + private SoundPool mSoundPool; + private int mBeepTwice; + private int mBeepOnce; + private boolean mPlaySound; + private final Handler mHandler = new MainHandler(); + + public CountDownView(Context context, AttributeSet attrs) { + super(context, attrs); + mCountDownAnim = AnimationUtils.loadAnimation(context, R.anim.count_down_exit); + // Load the beeps + mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0); + mBeepOnce = mSoundPool.load(context, R.raw.beep_once, 1); + mBeepTwice = mSoundPool.load(context, R.raw.beep_twice, 1); + } + + public boolean isCountingDown() { + return mRemainingSecs > 0; + }; + + public interface OnCountDownFinishedListener { + public void onCountDownFinished(); + } + + private void remainingSecondsChanged(int newVal) { + mRemainingSecs = newVal; + if (newVal == 0) { + // Countdown has finished + setVisibility(View.INVISIBLE); + mListener.onCountDownFinished(); + } else { + Locale locale = getResources().getConfiguration().locale; + String localizedValue = String.format(locale, "%d", newVal); + mRemainingSecondsView.setText(localizedValue); + // Fade-out animation + mCountDownAnim.reset(); + mRemainingSecondsView.clearAnimation(); + mRemainingSecondsView.startAnimation(mCountDownAnim); + + // Play sound effect for the last 3 seconds of the countdown + if (mPlaySound) { + if (newVal == 1) { + mSoundPool.play(mBeepTwice, 1.0f, 1.0f, 0, 0, 1.0f); + } else if (newVal <= 3) { + mSoundPool.play(mBeepOnce, 1.0f, 1.0f, 0, 0, 1.0f); + } + } + // Schedule the next remainingSecondsChanged() call in 1 second + mHandler.sendEmptyMessageDelayed(SET_TIMER_TEXT, 1000); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mRemainingSecondsView = (TextView) findViewById(R.id.remaining_seconds); + } + + public void setCountDownFinishedListener(OnCountDownFinishedListener listener) { + mListener = listener; + } + + public void startCountDown(int sec, boolean playSound) { + if (sec <= 0) { + Log.w(TAG, "Invalid input for countdown timer: " + sec + " seconds"); + return; + } + setVisibility(View.VISIBLE); + mPlaySound = playSound; + remainingSecondsChanged(sec); + } + + public void cancelCountDown() { + if (mRemainingSecs > 0) { + mRemainingSecs = 0; + mHandler.removeMessages(SET_TIMER_TEXT); + setVisibility(View.INVISIBLE); + } + } + + private class MainHandler extends Handler { + @Override + public void handleMessage(Message message) { + if (message.what == SET_TIMER_TEXT) { + remainingSecondsChanged(mRemainingSecs -1); + } + } + } +}
\ No newline at end of file diff --git a/src/com/android/camera/ui/CountdownTimerPopup.java b/src/com/android/camera/ui/CountdownTimerPopup.java new file mode 100644 index 000000000..7c3572b55 --- /dev/null +++ b/src/com/android/camera/ui/CountdownTimerPopup.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.NumberPicker; +import android.widget.NumberPicker.OnValueChangeListener; + +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +import java.util.Locale; + +/** + * This is a popup window that allows users to specify a countdown timer + */ + +public class CountdownTimerPopup extends AbstractSettingPopup { + private static final String TAG = "TimerSettingPopup"; + private NumberPicker mNumberSpinner; + private String[] mDurations; + private ListPreference mTimer; + private ListPreference mBeep; + private Listener mListener; + private Button mConfirmButton; + private View mPickerTitle; + private CheckBox mTimerSound; + private View mSoundTitle; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public CountdownTimerPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize(ListPreference timer, ListPreference beep) { + mTimer = timer; + mBeep = beep; + // Set title. + mTitle.setText(mTimer.getTitle()); + + // Duration + CharSequence[] entries = mTimer.getEntryValues(); + mDurations = new String[entries.length]; + Locale locale = getResources().getConfiguration().locale; + mDurations[0] = getResources().getString(R.string.setting_off); // Off + for (int i = 1; i < entries.length; i++) + mDurations[i] = String.format(locale, "%d", Integer.parseInt(entries[i].toString())); + int durationCount = mDurations.length; + mNumberSpinner = (NumberPicker) findViewById(R.id.duration); + mNumberSpinner.setMinValue(0); + mNumberSpinner.setMaxValue(durationCount - 1); + mNumberSpinner.setDisplayedValues(mDurations); + mNumberSpinner.setWrapSelectorWheel(false); + mNumberSpinner.setOnValueChangedListener(new OnValueChangeListener() { + @Override + public void onValueChange(NumberPicker picker, int oldValue, int newValue) { + setTimeSelectionEnabled(newValue != 0); + } + }); + mConfirmButton = (Button) findViewById(R.id.timer_set_button); + mPickerTitle = findViewById(R.id.set_time_interval_title); + + // Disable focus on the spinners to prevent keyboard from coming up + mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + mConfirmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + updateInputState(); + } + }); + mTimerSound = (CheckBox) findViewById(R.id.sound_check_box); + mSoundTitle = findViewById(R.id.beep_title); + } + + private void restoreSetting() { + int index = mTimer.findIndexOfValue(mTimer.getValue()); + if (index == -1) { + Log.e(TAG, "Invalid preference value."); + mTimer.print(); + throw new IllegalArgumentException(); + } else { + setTimeSelectionEnabled(index != 0); + mNumberSpinner.setValue(index); + } + boolean checked = mBeep.findIndexOfValue(mBeep.getValue()) != 0; + mTimerSound.setChecked(checked); + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Set the number pickers and on/off switch to be consistent + // with the preference + restoreSetting(); + } + } + super.setVisibility(visibility); + } + + protected void setTimeSelectionEnabled(boolean enabled) { + mPickerTitle.setVisibility(enabled ? VISIBLE : INVISIBLE); + mTimerSound.setEnabled(enabled); + mSoundTitle.setEnabled(enabled); + } + + @Override + public void reloadPreference() { + } + + private void updateInputState() { + mTimer.setValueIndex(mNumberSpinner.getValue()); + mBeep.setValueIndex(mTimerSound.isChecked() ? 1 : 0); + if (mListener != null) { + mListener.onListPrefChanged(mTimer); + mListener.onListPrefChanged(mBeep); + } + } +} diff --git a/src/com/android/camera/ui/EffectSettingPopup.java b/src/com/android/camera/ui/EffectSettingPopup.java new file mode 100644 index 000000000..568781a01 --- /dev/null +++ b/src/com/android/camera/ui/EffectSettingPopup.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.SimpleAdapter; + +import com.android.camera.IconListPreference; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.ArrayList; +import java.util.HashMap; + +// A popup window that shows video effect setting. It has two grid view. +// One shows the goofy face effects. The other shows the background replacer +// effects. +public class EffectSettingPopup extends AbstractSettingPopup implements + AdapterView.OnItemClickListener, View.OnClickListener { + private static final String TAG = "EffectSettingPopup"; + private String mNoEffect; + private IconListPreference mPreference; + private Listener mListener; + private View mClearEffects; + private GridView mSillyFacesGrid; + private GridView mBackgroundGrid; + + // Data for silly face items. (text, image, and preference value) + ArrayList<HashMap<String, Object>> mSillyFacesItem = + new ArrayList<HashMap<String, Object>>(); + + // Data for background replacer items. (text, image, and preference value) + ArrayList<HashMap<String, Object>> mBackgroundItem = + new ArrayList<HashMap<String, Object>>(); + + + static public interface Listener { + public void onSettingChanged(); + } + + public EffectSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + mNoEffect = context.getString(R.string.pref_video_effect_default); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mClearEffects = findViewById(R.id.clear_effects); + mClearEffects.setOnClickListener(this); + mSillyFacesGrid = (GridView) findViewById(R.id.effect_silly_faces); + mBackgroundGrid = (GridView) findViewById(R.id.effect_background); + } + + public void initialize(IconListPreference preference) { + mPreference = preference; + Context context = getContext(); + CharSequence[] entries = mPreference.getEntries(); + CharSequence[] entryValues = mPreference.getEntryValues(); + int[] iconIds = mPreference.getImageIds(); + if (iconIds == null) { + iconIds = mPreference.getLargeIconIds(); + } + + // Set title. + mTitle.setText(mPreference.getTitle()); + + for(int i = 0; i < entries.length; ++i) { + String value = entryValues[i].toString(); + if (value.equals(mNoEffect)) continue; // no effect, skip it. + HashMap<String, Object> map = new HashMap<String, Object>(); + map.put("value", value); + map.put("text", entries[i].toString()); + if (iconIds != null) map.put("image", iconIds[i]); + if (value.startsWith("goofy_face")) { + mSillyFacesItem.add(map); + } else if (value.startsWith("backdropper")) { + mBackgroundItem.add(map); + } + } + + boolean hasSillyFaces = mSillyFacesItem.size() > 0; + boolean hasBackground = mBackgroundItem.size() > 0; + + // Initialize goofy face if it is supported. + if (hasSillyFaces) { + findViewById(R.id.effect_silly_faces_title).setVisibility(View.VISIBLE); + findViewById(R.id.effect_silly_faces_title_separator).setVisibility(View.VISIBLE); + mSillyFacesGrid.setVisibility(View.VISIBLE); + SimpleAdapter sillyFacesItemAdapter = new SimpleAdapter(context, + mSillyFacesItem, R.layout.effect_setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + mSillyFacesGrid.setAdapter(sillyFacesItemAdapter); + mSillyFacesGrid.setOnItemClickListener(this); + } + + if (hasSillyFaces && hasBackground) { + findViewById(R.id.effect_background_separator).setVisibility(View.VISIBLE); + } + + // Initialize background replacer if it is supported. + if (hasBackground) { + findViewById(R.id.effect_background_title).setVisibility(View.VISIBLE); + findViewById(R.id.effect_background_title_separator).setVisibility(View.VISIBLE); + mBackgroundGrid.setVisibility(View.VISIBLE); + SimpleAdapter backgroundItemAdapter = new SimpleAdapter(context, + mBackgroundItem, R.layout.effect_setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + mBackgroundGrid.setAdapter(backgroundItemAdapter); + mBackgroundGrid.setOnItemClickListener(this); + } + + reloadPreference(); + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Do not show or hide "Clear effects" button when the popup + // is already visible. Otherwise it looks strange. + boolean noEffect = mPreference.getValue().equals(mNoEffect); + mClearEffects.setVisibility(noEffect ? View.GONE : View.VISIBLE); + } + reloadPreference(); + } + super.setVisibility(visibility); + } + + // The value of the preference may have changed. Update the UI. + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public void reloadPreference() { + mBackgroundGrid.setItemChecked(mBackgroundGrid.getCheckedItemPosition(), false); + mSillyFacesGrid.setItemChecked(mSillyFacesGrid.getCheckedItemPosition(), false); + + String value = mPreference.getValue(); + if (value.equals(mNoEffect)) return; + + for (int i = 0; i < mSillyFacesItem.size(); i++) { + if (value.equals(mSillyFacesItem.get(i).get("value"))) { + mSillyFacesGrid.setItemChecked(i, true); + return; + } + } + + for (int i = 0; i < mBackgroundItem.size(); i++) { + if (value.equals(mBackgroundItem.get(i).get("value"))) { + mBackgroundGrid.setItemChecked(i, true); + return; + } + } + + Log.e(TAG, "Invalid preference value: " + value); + mPreference.print(); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, + int index, long id) { + String value; + if (parent == mSillyFacesGrid) { + value = (String) mSillyFacesItem.get(index).get("value"); + } else if (parent == mBackgroundGrid) { + value = (String) mBackgroundItem.get(index).get("value"); + } else { + return; + } + + // Tapping the selected effect will deselect it (clear effects). + if (value.equals(mPreference.getValue())) { + mPreference.setValue(mNoEffect); + } else { + mPreference.setValue(value); + } + reloadPreference(); + if (mListener != null) mListener.onSettingChanged(); + } + + @Override + public void onClick(View v) { + // Clear the effect. + mPreference.setValue(mNoEffect); + reloadPreference(); + if (mListener != null) mListener.onSettingChanged(); + } +} diff --git a/src/com/android/camera/ui/ExpandedGridView.java b/src/com/android/camera/ui/ExpandedGridView.java new file mode 100644 index 000000000..13cf58f34 --- /dev/null +++ b/src/com/android/camera/ui/ExpandedGridView.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +public class ExpandedGridView extends GridView { + public ExpandedGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // If UNSPECIFIED is passed to GridView, it will show only one row. + // Here GridView is put in a ScrollView, so pass it a very big size with + // AT_MOST to show all the rows. + heightMeasureSpec = MeasureSpec.makeMeasureSpec(65536, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/src/com/android/camera/ui/FaceView.java b/src/com/android/camera/ui/FaceView.java new file mode 100644 index 000000000..7d66dc079 --- /dev/null +++ b/src/com/android/camera/ui/FaceView.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.hardware.Camera.Face; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.android.camera.PhotoUI; +import com.android.camera.Util; +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +@TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) +public class FaceView extends View + implements FocusIndicator, Rotatable, + PhotoUI.SurfaceTextureSizeChangedListener { + private static final String TAG = "CAM FaceView"; + private final boolean LOGV = false; + // The value for android.hardware.Camera.setDisplayOrientation. + private int mDisplayOrientation; + // The orientation compensation for the face indicator to make it look + // correctly in all device orientations. Ex: if the value is 90, the + // indicator should be rotated 90 degrees counter-clockwise. + private int mOrientation; + private boolean mMirror; + private boolean mPause; + private Matrix mMatrix = new Matrix(); + private RectF mRect = new RectF(); + // As face detection can be flaky, we add a layer of filtering on top of it + // to avoid rapid changes in state (eg, flickering between has faces and + // not having faces) + private Face[] mFaces; + private Face[] mPendingFaces; + private int mColor; + private final int mFocusingColor; + private final int mFocusedColor; + private final int mFailColor; + private Paint mPaint; + private volatile boolean mBlocked; + + private int mUncroppedWidth; + private int mUncroppedHeight; + private static final int MSG_SWITCH_FACES = 1; + private static final int SWITCH_DELAY = 70; + private boolean mStateSwitchPending = false; + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SWITCH_FACES: + mStateSwitchPending = false; + mFaces = mPendingFaces; + invalidate(); + break; + } + } + }; + + public FaceView(Context context, AttributeSet attrs) { + super(context, attrs); + Resources res = getResources(); + mFocusingColor = res.getColor(R.color.face_detect_start); + mFocusedColor = res.getColor(R.color.face_detect_success); + mFailColor = res.getColor(R.color.face_detect_fail); + mColor = mFocusingColor; + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setStyle(Style.STROKE); + mPaint.setStrokeWidth(res.getDimension(R.dimen.face_circle_stroke)); + } + + @Override + public void onSurfaceTextureSizeChanged(int uncroppedWidth, int uncroppedHeight) { + mUncroppedWidth = uncroppedWidth; + mUncroppedHeight = uncroppedHeight; + } + + public void setFaces(Face[] faces) { + if (LOGV) Log.v(TAG, "Num of faces=" + faces.length); + if (mPause) return; + if (mFaces != null) { + if ((faces.length > 0 && mFaces.length == 0) + || (faces.length == 0 && mFaces.length > 0)) { + mPendingFaces = faces; + if (!mStateSwitchPending) { + mStateSwitchPending = true; + mHandler.sendEmptyMessageDelayed(MSG_SWITCH_FACES, SWITCH_DELAY); + } + return; + } + } + if (mStateSwitchPending) { + mStateSwitchPending = false; + mHandler.removeMessages(MSG_SWITCH_FACES); + } + mFaces = faces; + invalidate(); + } + + public void setDisplayOrientation(int orientation) { + mDisplayOrientation = orientation; + if (LOGV) Log.v(TAG, "mDisplayOrientation=" + orientation); + } + + @Override + public void setOrientation(int orientation, boolean animation) { + mOrientation = orientation; + invalidate(); + } + + public void setMirror(boolean mirror) { + mMirror = mirror; + if (LOGV) Log.v(TAG, "mMirror=" + mirror); + } + + public boolean faceExists() { + return (mFaces != null && mFaces.length > 0); + } + + @Override + public void showStart() { + mColor = mFocusingColor; + invalidate(); + } + + // Ignore the parameter. No autofocus animation for face detection. + @Override + public void showSuccess(boolean timeout) { + mColor = mFocusedColor; + invalidate(); + } + + // Ignore the parameter. No autofocus animation for face detection. + @Override + public void showFail(boolean timeout) { + mColor = mFailColor; + invalidate(); + } + + @Override + public void clear() { + // Face indicator is displayed during preview. Do not clear the + // drawable. + mColor = mFocusingColor; + mFaces = null; + invalidate(); + } + + public void pause() { + mPause = true; + } + + public void resume() { + mPause = false; + } + + public void setBlockDraw(boolean block) { + mBlocked = block; + } + + @Override + protected void onDraw(Canvas canvas) { + if (!mBlocked && (mFaces != null) && (mFaces.length > 0)) { + int rw, rh; + rw = mUncroppedWidth; + rh = mUncroppedHeight; + // Prepare the matrix. + if (((rh > rw) && ((mDisplayOrientation == 0) || (mDisplayOrientation == 180))) + || ((rw > rh) && ((mDisplayOrientation == 90) || (mDisplayOrientation == 270)))) { + int temp = rw; + rw = rh; + rh = temp; + } + Util.prepareMatrix(mMatrix, mMirror, mDisplayOrientation, rw, rh); + int dx = (getWidth() - rw) / 2; + int dy = (getHeight() - rh) / 2; + + // Focus indicator is directional. Rotate the matrix and the canvas + // so it looks correctly in all orientations. + canvas.save(); + mMatrix.postRotate(mOrientation); // postRotate is clockwise + canvas.rotate(-mOrientation); // rotate is counter-clockwise (for canvas) + for (int i = 0; i < mFaces.length; i++) { + // Filter out false positives. + if (mFaces[i].score < 50) continue; + + // Transform the coordinates. + mRect.set(mFaces[i].rect); + if (LOGV) Util.dumpRect(mRect, "Original rect"); + mMatrix.mapRect(mRect); + if (LOGV) Util.dumpRect(mRect, "Transformed rect"); + mPaint.setColor(mColor); + mRect.offset(dx, dy); + canvas.drawOval(mRect, mPaint); + } + canvas.restore(); + } + super.onDraw(canvas); + } +} diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java new file mode 100644 index 000000000..f870b5829 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector. +public class FilmStripGestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripGestureRecognizer"; + + public interface Listener { + boolean onSingleTapUp(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onScroll(float x, float y, float dx, float dy); + boolean onFling(float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + boolean onDown(float x, float y); + boolean onUp(float x, float y); + void onScaleEnd(); + } + + private final GestureDetector mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final Listener mListener; + + public FilmStripGestureRecognizer(Context context, Listener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + } + + public void onTouchEvent(MotionEvent event) { + mGestureDetector.onTouchEvent(event); + mScaleDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP) { + mListener.onUp(event.getX(), event.getY()); + } + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mListener.onSingleTapUp(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + return mListener.onScroll(e2.getX(), e2.getY(), dx, dy); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(velocityX, velocityY); + } + + @Override + public boolean onDown(MotionEvent e) { + mListener.onDown(e.getX(), e.getY()); + return super.onDown(e); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } +} diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java new file mode 100644 index 000000000..8a1a85a55 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripView.java @@ -0,0 +1,1720 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.animation.Animator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.Scroller; + +import com.android.camera.ui.FilmStripView.ImageData.PanoramaSupportCallback; +import com.android.gallery3d.R; +import com.android.gallery3d.util.LightCycleHelper.PanoramaViewHelper; + +public class FilmStripView extends ViewGroup { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripView"; + + private static final int BUFFER_SIZE = 5; + private static final int DURATION_GEOMETRY_ADJUST = 200; + private static final float FILM_STRIP_SCALE = 0.6f; + private static final float FULLSCREEN_SCALE = 1f; + // Only check for intercepting touch events within first 500ms + private static final int SWIPE_TIME_OUT = 500; + + private Context mContext; + private FilmStripGestureRecognizer mGestureRecognizer; + private DataAdapter mDataAdapter; + private int mViewGap; + private final Rect mDrawArea = new Rect(); + + private final int mCurrentInfo = (BUFFER_SIZE - 1) / 2; + private float mScale; + private MyController mController; + private int mCenterX = -1; + private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE]; + + private Listener mListener; + + private MotionEvent mDown; + private boolean mCheckToIntercept = true; + private View mCameraView; + private int mSlop; + private TimeInterpolator mViewAnimInterpolator; + + private ImageButton mViewPhotoSphereButton; + private PanoramaViewHelper mPanoramaViewHelper; + private long mLastItemId = -1; + + // This is used to resolve the misalignment problem when the device + // orientation is changed. If the current item is in fullscreen, it might + // be shifted because mCenterX is not adjusted with the orientation. + // Set this to true when onSizeChanged is called to make sure we adjust + // mCenterX accordingly. + private boolean mAnchorPending; + + /** + * Common interface for all images in the filmstrip. + */ + public interface ImageData { + /** + * Interface that is used to tell the caller whether an image is a photo + * sphere. + */ + public static interface PanoramaSupportCallback { + /** + * Called then photo sphere info has been loaded. + * + * @param isPanorama whether the image is a valid photo sphere + * @param isPanorama360 whether the photo sphere is a full 360 + * degree horizontal panorama + */ + void panoramaInfoAvailable(boolean isPanorama, + boolean isPanorama360); + } + + // Image data types. + public static final int TYPE_NONE = 0; + public static final int TYPE_CAMERA_PREVIEW = 1; + public static final int TYPE_PHOTO = 2; + public static final int TYPE_VIDEO = 3; + + // Actions allowed to be performed on the image data. + // The actions are defined bit-wise so we can use bit operations like + // | and &. + public static final int ACTION_NONE = 0; + public static final int ACTION_PROMOTE = 1; + public static final int ACTION_DEMOTE = (1 << 1); + + /** + * SIZE_FULL can be returned by {@link ImageData#getWidth()} and + * {@link ImageData#getHeight()}. + * When SIZE_FULL is returned for width/height, it means the the + * width or height will be disregarded when deciding the view size + * of this ImageData, just use full screen size. + */ + public static final int SIZE_FULL = -2; + + /** + * Returns the width of the image. The final layout of the view returned + * by {@link DataAdapter#getView(android.content.Context, int)} will + * preserve the aspect ratio of + * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and + * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}. + */ + public int getWidth(); + + + /** + * Returns the width of the image. The final layout of the view returned + * by {@link DataAdapter#getView(android.content.Context, int)} will + * preserve the aspect ratio of + * {@link com.android.camera.ui.FilmStripView.ImageData#getWidth()} and + * {@link com.android.camera.ui.FilmStripView.ImageData#getHeight()}. + */ + public int getHeight(); + + /** Returns the image data type */ + public int getType(); + + /** + * Checks if the UI action is supported. + * + * @param action The UI actions to check. + * @return {@code false} if at least one of the actions is not + * supported. {@code true} otherwise. + */ + public boolean isUIActionSupported(int action); + + /** + * Gives the data a hint when its view is going to be displayed. + * {@code FilmStripView} should always call this function before + * showing its corresponding view every time. + */ + public void prepare(); + + /** + * Gives the data a hint when its view is going to be removed from the + * view hierarchy. {@code FilmStripView} should always call this + * function after its corresponding view is removed from the view + * hierarchy. + */ + public void recycle(); + + /** + * Asynchronously checks if the image is a photo sphere. Notified the + * callback when the results are available. + */ + public void isPhotoSphere(Context context, PanoramaSupportCallback callback); + + /** + * If the item is a valid photo sphere panorama, this method will launch + * the viewer. + */ + public void viewPhotoSphere(PanoramaViewHelper helper); + } + + /** + * An interfaces which defines the interactions between the + * {@link ImageData} and the {@link FilmStripView}. + */ + public interface DataAdapter { + /** + * An interface which defines the update report used to return to + * the {@link com.android.camera.ui.FilmStripView.Listener}. + */ + public interface UpdateReporter { + /** Checks if the data of dataID is removed. */ + public boolean isDataRemoved(int dataID); + + /** Checks if the data of dataID is updated. */ + public boolean isDataUpdated(int dataID); + } + + /** + * An interface which defines the listener for UI actions over + * {@link ImageData}. + */ + public interface Listener { + // Called when the whole data loading is done. No any assumption + // on previous data. + public void onDataLoaded(); + + // Only some of the data is changed. The listener should check + // if any thing needs to be updated. + public void onDataUpdated(UpdateReporter reporter); + + public void onDataInserted(int dataID, ImageData data); + + public void onDataRemoved(int dataID, ImageData data); + } + + /** Returns the total number of image data */ + public int getTotalNumber(); + + /** + * Returns the view to visually present the image data. + * + * @param context The {@link Context} to create the view. + * @param dataID The ID of the image data to be presented. + * @return The view representing the image data. Null if + * unavailable or the {@code dataID} is out of range. + */ + public View getView(Context context, int dataID); + + /** + * Returns the {@link ImageData} specified by the ID. + * + * @param dataID The ID of the {@link ImageData}. + * @return The specified {@link ImageData}. Null if not available. + */ + public ImageData getImageData(int dataID); + + /** + * Suggests the data adapter the maximum possible size of the layout + * so the {@link DataAdapter} can optimize the view returned for the + * {@link ImageData}. + * + * @param w Maximum width. + * @param h Maximum height. + */ + public void suggestViewSizeBound(int w, int h); + + /** + * Sets the listener for FilmStripView UI actions over the ImageData. + * + * @param listener The listener to use. + */ + public void setListener(Listener listener); + + /** + * The callback when the item enters/leaves full-screen. + * TODO: Call this function actually. + * + * @param dataID The ID of the image data. + * @param fullScreen {@code true} if the data is entering full-screen. + * {@code false} otherwise. + */ + public void onDataFullScreen(int dataID, boolean fullScreen); + + /** + * The callback when the item is centered/off-centered. + * TODO: Calls this function actually. + * + * @param dataID The ID of the image data. + * @param centered {@code true} if the data is centered. + * {@code false} otherwise. + */ + public void onDataCentered(int dataID, boolean centered); + + /** + * Returns {@code true} if the view of the data can be moved by swipe + * gesture when in full-screen. + * + * @param dataID The ID of the data. + * @return {@code true} if the view can be moved, + * {@code false} otherwise. + */ + public boolean canSwipeInFullScreen(int dataID); + } + + /** + * An interface which defines the FilmStripView UI action listener. + */ + public interface Listener { + /** + * Callback when the data is promoted. + * + * @param dataID The ID of the promoted data. + */ + public void onDataPromoted(int dataID); + + /** + * Callback when the data is demoted. + * + * @param dataID The ID of the demoted data. + */ + public void onDataDemoted(int dataID); + + public void onDataFullScreenChange(int dataID, boolean full); + + /** + * Callback when entering/leaving camera mode. + * + * @param toCamera {@code true} if entering camera mode. Otherwise, + * {@code false} + */ + public void onSwitchMode(boolean toCamera); + } + + /** + * An interface which defines the controller of {@link FilmStripView}. + */ + public interface Controller { + public boolean isScalling(); + + public void scroll(float deltaX); + + public void fling(float velocity); + + public void scrollTo(int position, int duration, boolean interruptible); + + public boolean stopScrolling(); + + public boolean isScrolling(); + + public void lockAtCurrentView(); + + public void unlockPosition(); + + public void gotoCameraFullScreen(); + + public void gotoFilmStrip(); + + public void gotoFullScreen(); + } + + /** + * A helper class to tract and calculate the view coordination. + */ + private static class ViewInfo { + private int mDataID; + /** The position of the left of the view in the whole filmstrip. */ + private int mLeftPosition; + private View mView; + private RectF mViewArea; + + public ViewInfo(int id, View v) { + v.setPivotX(0f); + v.setPivotY(0f); + mDataID = id; + mView = v; + mLeftPosition = -1; + mViewArea = new RectF(); + } + + public int getID() { + return mDataID; + } + + public void setID(int id) { + mDataID = id; + } + + public void setLeftPosition(int pos) { + mLeftPosition = pos; + } + + public int getLeftPosition() { + return mLeftPosition; + } + + public float getTranslationY(float scale) { + return mView.getTranslationY() / scale; + } + + public float getTranslationX(float scale) { + return mView.getTranslationX(); + } + + public void setTranslationY(float transY, float scale) { + mView.setTranslationY(transY * scale); + } + + public void setTranslationX(float transX, float scale) { + mView.setTranslationX(transX * scale); + } + + public void translateXBy(float transX, float scale) { + mView.setTranslationX(mView.getTranslationX() + transX * scale); + } + + public int getCenterX() { + return mLeftPosition + mView.getWidth() / 2; + } + + public View getView() { + return mView; + } + + private void layoutAt(int left, int top) { + mView.layout(left, top, left + mView.getMeasuredWidth(), + top + mView.getMeasuredHeight()); + } + + public void layoutIn(Rect drawArea, int refCenter, float scale) { + // drawArea is where to layout in. + // refCenter is the absolute horizontal position of the center of + // drawArea. + int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale); + int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale); + layoutAt(left, top); + mView.setScaleX(scale); + mView.setScaleY(scale); + + // update mViewArea for touch detection. + int l = mView.getLeft(); + int t = mView.getTop(); + mViewArea.set(l, t, + l + mView.getWidth() * scale, + t + mView.getHeight() * scale); + } + + public boolean areaContains(float x, float y) { + return mViewArea.contains(x, y); + } + } + + /** Constructor. */ + public FilmStripView(Context context) { + super(context); + init(context); + } + + /** Constructor. */ + public FilmStripView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + /** Constructor. */ + public FilmStripView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + // This is for positioning camera controller at the same place in + // different orientations. + setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); + + setWillNotDraw(false); + mContext = context; + mScale = 1.0f; + mController = new MyController(context); + mViewAnimInterpolator = new DecelerateInterpolator(); + mGestureRecognizer = + new FilmStripGestureRecognizer(context, new MyGestureReceiver()); + mSlop = (int) getContext().getResources().getDimension(R.dimen.pie_touch_slop); + } + + /** + * Returns the controller. + * + * @return The {@code Controller}. + */ + public Controller getController() { + return mController; + } + + public void setListener(Listener l) { + mListener = l; + } + + public void setViewGap(int viewGap) { + mViewGap = viewGap; + } + + /** + * Sets the helper that's to be used to open photo sphere panoramas. + */ + public void setPanoramaViewHelper(PanoramaViewHelper helper) { + mPanoramaViewHelper = helper; + } + + public float getScale() { + return mScale; + } + + public boolean isAnchoredTo(int id) { + if (mViewInfo[mCurrentInfo].getID() == id + && mViewInfo[mCurrentInfo].getCenterX() == mCenterX) { + return true; + } + return false; + } + + public int getCurrentType() { + if (mDataAdapter == null) { + return ImageData.TYPE_NONE; + } + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) { + return ImageData.TYPE_NONE; + } + return mDataAdapter.getImageData(curr.getID()).getType(); + } + + @Override + public void onDraw(Canvas c) { + if (mController.hasNewGeometry()) { + layoutChildren(); + } + } + + /** Returns [width, height] preserving image aspect ratio. */ + private int[] calculateChildDimension( + int imageWidth, int imageHeight, + int boundWidth, int boundHeight) { + + if (imageWidth == ImageData.SIZE_FULL + || imageHeight == ImageData.SIZE_FULL) { + imageWidth = boundWidth; + imageHeight = boundHeight; + } + + int[] ret = new int[2]; + ret[0] = boundWidth; + ret[1] = boundHeight; + + if (imageWidth * ret[1] > ret[0] * imageHeight) { + ret[1] = imageHeight * ret[0] / imageWidth; + } else { + ret[0] = imageWidth * ret[1] / imageHeight; + } + + return ret; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int boundWidth = MeasureSpec.getSize(widthMeasureSpec); + int boundHeight = MeasureSpec.getSize(heightMeasureSpec); + if (boundWidth == 0 || boundHeight == 0) { + // Either width or height is unknown, can't measure children yet. + return; + } + + if (mDataAdapter != null) { + mDataAdapter.suggestViewSizeBound(boundWidth / 2, boundHeight / 2); + } + + for (ViewInfo info : mViewInfo) { + if (info == null) continue; + + int id = info.getID(); + int[] dim = calculateChildDimension( + mDataAdapter.getImageData(id).getWidth(), + mDataAdapter.getImageData(id).getHeight(), + boundWidth, boundHeight); + + info.getView().measure( + MeasureSpec.makeMeasureSpec( + dim[0], MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + dim[1], MeasureSpec.EXACTLY)); + } + } + + @Override + protected boolean fitSystemWindows(Rect insets) { + if (mViewPhotoSphereButton != null) { + // Set the position of the "View Photo Sphere" button. + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) mViewPhotoSphereButton + .getLayoutParams(); + params.bottomMargin = insets.bottom; + mViewPhotoSphereButton.setLayoutParams(params); + } + + return super.fitSystemWindows(insets); + } + + private int findTheNearestView(int pointX) { + + int nearest = 0; + // Find the first non-null ViewInfo. + while (nearest < BUFFER_SIZE + && (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1)) { + nearest++; + } + // No existing available ViewInfo + if (nearest == BUFFER_SIZE) { + return -1; + } + int min = Math.abs(pointX - mViewInfo[nearest].getCenterX()); + + for (int infoID = nearest + 1; infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) { + // Not measured yet. + if (mViewInfo[infoID].getLeftPosition() == -1) + continue; + + int c = mViewInfo[infoID].getCenterX(); + int dist = Math.abs(pointX - c); + if (dist < min) { + min = dist; + nearest = infoID; + } + } + return nearest; + } + + private ViewInfo buildInfoFromData(int dataID) { + ImageData data = mDataAdapter.getImageData(dataID); + if (data == null) { + return null; + } + data.prepare(); + View v = mDataAdapter.getView(mContext, dataID); + if (v == null) { + return null; + } + ViewInfo info = new ViewInfo(dataID, v); + v = info.getView(); + if (v != mCameraView) { + addView(info.getView()); + } else { + v.setVisibility(View.VISIBLE); + } + return info; + } + + private void removeInfo(int infoID) { + if (infoID >= mViewInfo.length || mViewInfo[infoID] == null) { + return; + } + + ImageData data = mDataAdapter.getImageData(mViewInfo[infoID].getID()); + checkForRemoval(data, mViewInfo[infoID].getView()); + mViewInfo[infoID] = null; + } + + /** + * We try to keep the one closest to the center of the screen at position + * mCurrentInfo. + */ + private void stepIfNeeded() { + if (!inFilmStrip() && !inFullScreen()) { + // The good timing to step to the next view is when everything is + // not in + // transition. + return; + } + int nearest = findTheNearestView(mCenterX); + // no change made. + if (nearest == -1 || nearest == mCurrentInfo) + return; + + int adjust = nearest - mCurrentInfo; + if (adjust > 0) { + for (int k = 0; k < adjust; k++) { + removeInfo(k); + } + for (int k = 0; k + adjust < BUFFER_SIZE; k++) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) { + mViewInfo[k] = null; + if (mViewInfo[k - 1] != null) { + mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1); + } + } + } else { + for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { + removeInfo(k); + } + for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = -1 - adjust; k >= 0; k--) { + mViewInfo[k] = null; + if (mViewInfo[k + 1] != null) { + mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1); + } + } + } + } + + /** Don't go beyond the bound. */ + private void clampCenterX() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) { + return; + } + + if (curr.getID() == 0 && mCenterX < curr.getCenterX()) { + mCenterX = curr.getCenterX(); + if (mController.isScrolling()) { + mController.stopScrolling(); + } + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW + && !mController.isScalling() + && mScale != FULLSCREEN_SCALE) { + mController.gotoFullScreen(); + } + } + if (curr.getID() == mDataAdapter.getTotalNumber() - 1 + && mCenterX > curr.getCenterX()) { + mCenterX = curr.getCenterX(); + if (!mController.isScrolling()) { + mController.stopScrolling(); + } + } + } + + private void adjustChildZOrder() { + for (int i = BUFFER_SIZE - 1; i >= 0; i--) { + if (mViewInfo[i] == null) + continue; + bringChildToFront(mViewInfo[i].getView()); + } + } + + /** + * If the current photo is a photo sphere, this will launch the Photo Sphere + * panorama viewer. + */ + private void showPhotoSphere() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr != null) { + mDataAdapter.getImageData(curr.getID()).viewPhotoSphere(mPanoramaViewHelper); + } + } + + /** + * @return The ID of the current item, or -1. + */ + private int getCurrentId() { + ViewInfo current = mViewInfo[mCurrentInfo]; + if (current == null) { + return -1; + } + return current.getID(); + } + + /** + * Updates the visibility of the View Photo Sphere button. + */ + private void updatePhotoSphereViewButton() { + if (mViewPhotoSphereButton == null) { + mViewPhotoSphereButton = (ImageButton) ((View) getParent()) + .findViewById(R.id.filmstrip_bottom_control_panorama); + mViewPhotoSphereButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + showPhotoSphere(); + } + }); + } + final int requestId = getCurrentId(); + + // Check if the item has changed since the last time we updated the + // visibility status. Only then check of the current image is a photo + // sphere. + if (requestId == mLastItemId || requestId < 0) { + return; + } + + ImageData data = mDataAdapter.getImageData(requestId); + data.isPhotoSphere(mContext, new PanoramaSupportCallback() { + @Override + public void panoramaInfoAvailable(final boolean isPanorama, + boolean isPanorama360) { + // Make sure the returned data is for the current image. + if (requestId == getCurrentId()) { + mViewPhotoSphereButton.post(new Runnable() { + @Override + public void run() { + mViewPhotoSphereButton.setVisibility(isPanorama ? View.VISIBLE + : View.GONE); + } + }); + } + } + }); + } + + private void layoutChildren() { + if (mAnchorPending) { + mCenterX = mViewInfo[mCurrentInfo].getCenterX(); + mAnchorPending = false; + } + + if (mController.hasNewGeometry()) { + mCenterX = mController.getNewPosition(); + mScale = mController.getNewScale(); + } + + clampCenterX(); + + mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterX, mScale); + + int currentViewLeft = mViewInfo[mCurrentInfo].getLeftPosition(); + int currentViewCenter = mViewInfo[mCurrentInfo].getCenterX(); + int fullScreenWidth = mDrawArea.width() + mViewGap; + float scaleFraction = mViewAnimInterpolator.getInterpolation( + (mScale - FILM_STRIP_SCALE) / (FULLSCREEN_SCALE - FILM_STRIP_SCALE)); + + // images on the left + for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) { + ViewInfo curr = mViewInfo[infoID]; + if (curr == null) { + continue; + } + + ViewInfo next = mViewInfo[infoID + 1]; + int myLeft = + next.getLeftPosition() - curr.getView().getMeasuredWidth() - mViewGap; + curr.setLeftPosition(myLeft); + curr.layoutIn(mDrawArea, mCenterX, mScale); + curr.getView().setAlpha(1f); + int infoDiff = mCurrentInfo - infoID; + curr.setTranslationX( + (currentViewCenter + - fullScreenWidth * infoDiff - curr.getCenterX()) * scaleFraction, + mScale); + } + + // images on the right + for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) { + ViewInfo curr = mViewInfo[infoID]; + if (curr == null) { + continue; + } + + ViewInfo prev = mViewInfo[infoID - 1]; + int myLeft = + prev.getLeftPosition() + prev.getView().getMeasuredWidth() + mViewGap; + curr.setLeftPosition(myLeft); + curr.layoutIn(mDrawArea, mCenterX, mScale); + if (infoID == mCurrentInfo + 1) { + curr.getView().setAlpha(1f - scaleFraction); + } else { + if (scaleFraction == 0f) { + curr.getView().setAlpha(1f); + } else { + curr.getView().setAlpha(0f); + } + } + curr.setTranslationX((currentViewLeft - myLeft) * scaleFraction, mScale); + } + + stepIfNeeded(); + adjustChildZOrder(); + invalidate(); + updatePhotoSphereViewButton(); + mLastItemId = getCurrentId(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mViewInfo[mCurrentInfo] == null) { + return; + } + + mDrawArea.left = l; + mDrawArea.top = t; + mDrawArea.right = r; + mDrawArea.bottom = b; + + layoutChildren(); + } + + // Keeps the view in the view hierarchy if it's camera preview. + // Remove from the hierarchy otherwise. + private void checkForRemoval(ImageData data, View v) { + if (data.getType() != ImageData.TYPE_CAMERA_PREVIEW) { + removeView(v); + data.recycle(); + } else { + v.setVisibility(View.INVISIBLE); + if (mCameraView != null && mCameraView != v) { + removeView(mCameraView); + } + mCameraView = v; + } + } + + private void slideViewBack(View v) { + v.animate().translationX(0) + .alpha(1f) + .setDuration(DURATION_GEOMETRY_ADJUST) + .setInterpolator(mViewAnimInterpolator) + .start(); + } + + private void updateRemoval(int dataID, final ImageData data) { + int removedInfo = findInfoByDataID(dataID); + + // adjust the data id to be consistent + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null || mViewInfo[i].getID() <= dataID) { + continue; + } + mViewInfo[i].setID(mViewInfo[i].getID() - 1); + } + if (removedInfo == -1) { + return; + } + + final View removedView = mViewInfo[removedInfo].getView(); + final int offsetX = removedView.getMeasuredWidth() + mViewGap; + + for (int i = removedInfo + 1; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null) { + mViewInfo[i].setLeftPosition(mViewInfo[i].getLeftPosition() - offsetX); + } + } + + if (removedInfo >= mCurrentInfo + && mViewInfo[removedInfo].getID() < mDataAdapter.getTotalNumber()) { + // Fill the removed info by left shift when the current one or + // anyone on the right is removed, and there's more data on the + // right available. + for (int i = removedInfo; i < BUFFER_SIZE - 1; i++) { + mViewInfo[i] = mViewInfo[i + 1]; + } + + // pull data out from the DataAdapter for the last one. + int curr = BUFFER_SIZE - 1; + int prev = curr - 1; + if (mViewInfo[prev] != null) { + mViewInfo[curr] = buildInfoFromData(mViewInfo[prev].getID() + 1); + } + + // Translate the views to their original places. + for (int i = removedInfo; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null) { + mViewInfo[i].setTranslationX(offsetX, mScale); + } + } + + // The end of the filmstrip might have been changed. + // The mCenterX might be out of the bound. + ViewInfo currInfo = mViewInfo[mCurrentInfo]; + if (currInfo.getID() == mDataAdapter.getTotalNumber() - 1 + && mCenterX > currInfo.getCenterX()) { + int adjustDiff = currInfo.getCenterX() - mCenterX; + mCenterX = currInfo.getCenterX(); + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null) { + mViewInfo[i].translateXBy(adjustDiff, mScale); + } + } + } + } else { + // fill the removed place by right shift + mCenterX -= offsetX; + + for (int i = removedInfo; i > 0; i--) { + mViewInfo[i] = mViewInfo[i - 1]; + } + + // pull data out from the DataAdapter for the first one. + int curr = 0; + int next = curr + 1; + if (mViewInfo[next] != null) { + mViewInfo[curr] = buildInfoFromData(mViewInfo[next].getID() - 1); + } + + // Translate the views to their original places. + for (int i = removedInfo; i >= 0; i--) { + if (mViewInfo[i] != null) { + mViewInfo[i].setTranslationX(-offsetX, mScale); + } + } + } + + // Now, slide every one back. + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null + && mViewInfo[i].getTranslationX(mScale) != 0f) { + slideViewBack(mViewInfo[i].getView()); + } + } + + int transY = getHeight() / 8; + if (removedView.getTranslationY() < 0) { + transY = -transY; + } + removedView.animate() + .alpha(0f) + .translationYBy(transY) + .setInterpolator(mViewAnimInterpolator) + .setDuration(DURATION_GEOMETRY_ADJUST) + .withEndAction(new Runnable() { + @Override + public void run() { + checkForRemoval(data, removedView); + } + }) + .start(); + layoutChildren(); + } + + // returns -1 on failure. + private int findInfoByDataID(int dataID) { + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] != null + && mViewInfo[i].getID() == dataID) { + return i; + } + } + return -1; + } + + private void updateInsertion(int dataID) { + int insertedInfo = findInfoByDataID(dataID); + if (insertedInfo == -1) { + // Not in the current info buffers. Check if it's inserted + // at the end. + if (dataID == mDataAdapter.getTotalNumber() - 1) { + int prev = findInfoByDataID(dataID - 1); + if (prev >= 0 && prev < BUFFER_SIZE - 1) { + // The previous data is in the buffer and we still + // have room for the inserted data. + insertedInfo = prev + 1; + } + } + } + + // adjust the data id to be consistent + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null || mViewInfo[i].getID() < dataID) { + continue; + } + mViewInfo[i].setID(mViewInfo[i].getID() + 1); + } + if (insertedInfo == -1) { + return; + } + + final ImageData data = mDataAdapter.getImageData(dataID); + int[] dim = calculateChildDimension( + data.getWidth(), data.getHeight(), + getMeasuredWidth(), getMeasuredHeight()); + final int offsetX = dim[0] + mViewGap; + ViewInfo viewInfo = buildInfoFromData(dataID); + + if (insertedInfo >= mCurrentInfo) { + if (insertedInfo == mCurrentInfo) { + viewInfo.setLeftPosition(mViewInfo[mCurrentInfo].getLeftPosition()); + } + // Shift right to make rooms for newly inserted item. + removeInfo(BUFFER_SIZE - 1); + for (int i = BUFFER_SIZE - 1; i > insertedInfo; i--) { + mViewInfo[i] = mViewInfo[i - 1]; + if (mViewInfo[i] != null) { + mViewInfo[i].setTranslationX(-offsetX, mScale); + slideViewBack(mViewInfo[i].getView()); + } + } + } else { + // Shift left. Put the inserted data on the left instead of the + // found position. + --insertedInfo; + if (insertedInfo < 0) { + return; + } + removeInfo(0); + for (int i = 1; i <= insertedInfo; i++) { + if (mViewInfo[i] != null) { + mViewInfo[i].setTranslationX(offsetX, mScale); + slideViewBack(mViewInfo[i].getView()); + mViewInfo[i - 1] = mViewInfo[i]; + } + } + } + + mViewInfo[insertedInfo] = viewInfo; + View insertedView = mViewInfo[insertedInfo].getView(); + insertedView.setAlpha(0f); + insertedView.setTranslationY(getHeight() / 8); + insertedView.animate() + .alpha(1f) + .translationY(0f) + .setInterpolator(mViewAnimInterpolator) + .setDuration(DURATION_GEOMETRY_ADJUST) + .start(); + invalidate(); + } + + public void setDataAdapter(DataAdapter adapter) { + mDataAdapter = adapter; + mDataAdapter.suggestViewSizeBound(getMeasuredWidth(), getMeasuredHeight()); + mDataAdapter.setListener(new DataAdapter.Listener() { + @Override + public void onDataLoaded() { + reload(); + } + + @Override + public void onDataUpdated(DataAdapter.UpdateReporter reporter) { + update(reporter); + } + + @Override + public void onDataInserted(int dataID, ImageData data) { + if (mViewInfo[mCurrentInfo] == null) { + // empty now, simply do a reload. + reload(); + return; + } + updateInsertion(dataID); + } + + @Override + public void onDataRemoved(int dataID, ImageData data) { + updateRemoval(dataID, data); + } + }); + } + + public boolean inFilmStrip() { + return (mScale == FILM_STRIP_SCALE); + } + + public boolean inFullScreen() { + return (mScale == FULLSCREEN_SCALE); + } + + public boolean inCameraFullscreen() { + return isAnchoredTo(0) && inFullScreen() + && (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (inFilmStrip()) { + return true; + } + + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + mCheckToIntercept = true; + mDown = MotionEvent.obtain(ev); + ViewInfo viewInfo = mViewInfo[mCurrentInfo]; + // Do not intercept touch if swipe is not enabled + if (viewInfo != null && !mDataAdapter.canSwipeInFullScreen(viewInfo.getID())) { + mCheckToIntercept = false; + } + return false; + } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { + // Do not intercept touch once child is in zoom mode + mCheckToIntercept = false; + return false; + } else { + if (!mCheckToIntercept) { + return false; + } + if (ev.getEventTime() - ev.getDownTime() > SWIPE_TIME_OUT) { + return false; + } + int deltaX = (int) (ev.getX() - mDown.getX()); + int deltaY = (int) (ev.getY() - mDown.getY()); + if (ev.getActionMasked() == MotionEvent.ACTION_MOVE + && deltaX < mSlop * (-1)) { + // intercept left swipe + if (Math.abs(deltaX) >= Math.abs(deltaY) * 2) { + return true; + } + } + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mGestureRecognizer.onTouchEvent(ev); + return true; + } + + private void updateViewInfo(int infoID) { + ViewInfo info = mViewInfo[infoID]; + removeView(info.getView()); + mViewInfo[infoID] = buildInfoFromData(info.getID()); + } + + /** Some of the data is changed. */ + private void update(DataAdapter.UpdateReporter reporter) { + // No data yet. + if (mViewInfo[mCurrentInfo] == null) { + reload(); + return; + } + + // Check the current one. + ViewInfo curr = mViewInfo[mCurrentInfo]; + int dataID = curr.getID(); + if (reporter.isDataRemoved(dataID)) { + mCenterX = -1; + reload(); + return; + } + if (reporter.isDataUpdated(dataID)) { + updateViewInfo(mCurrentInfo); + } + + // Check left + for (int i = mCurrentInfo - 1; i >= 0; i--) { + curr = mViewInfo[i]; + if (curr != null) { + dataID = curr.getID(); + if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) { + updateViewInfo(i); + } + } else { + ViewInfo next = mViewInfo[i + 1]; + if (next != null) { + mViewInfo[i] = buildInfoFromData(next.getID() - 1); + } + } + } + + // Check right + for (int i = mCurrentInfo + 1; i < BUFFER_SIZE; i++) { + curr = mViewInfo[i]; + if (curr != null) { + dataID = curr.getID(); + if (reporter.isDataRemoved(dataID) || reporter.isDataUpdated(dataID)) { + updateViewInfo(i); + } + } else { + ViewInfo prev = mViewInfo[i - 1]; + if (prev != null) { + mViewInfo[i] = buildInfoFromData(prev.getID() + 1); + } + } + } + } + + /** + * The whole data might be totally different. Flush all and load from the + * start. + */ + private void reload() { + removeAllViews(); + int dataNumber = mDataAdapter.getTotalNumber(); + if (dataNumber == 0) { + return; + } + + mViewInfo[mCurrentInfo] = buildInfoFromData(0); + mViewInfo[mCurrentInfo].setLeftPosition(0); + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) { + // we are in camera mode by default. + mController.lockAtCurrentView(); + } + for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) { + int infoID = mCurrentInfo + i; + if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) { + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1); + } + infoID = mCurrentInfo - i; + if (infoID >= 0 && mViewInfo[infoID + 1] != null) { + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1); + } + } + layoutChildren(); + } + + private void promoteData(int infoID, int dataID) { + if (mListener != null) { + mListener.onDataPromoted(dataID); + } + } + + private void demoteData(int infoID, int dataID) { + if (mListener != null) { + mListener.onDataDemoted(dataID); + } + } + + /** + * MyController controls all the geometry animations. It passively tells the + * geometry information on demand. + */ + private class MyController implements + Controller, + ValueAnimator.AnimatorUpdateListener, + Animator.AnimatorListener { + + private ValueAnimator mScaleAnimator; + private boolean mHasNewScale; + private float mNewScale; + + private Scroller mScroller; + private boolean mHasNewPosition; + private DecelerateInterpolator mDecelerateInterpolator; + + private boolean mCanStopScroll; + + private boolean mIsPositionLocked; + private int mLockedViewInfo; + + MyController(Context context) { + mScroller = new Scroller(context); + mHasNewPosition = false; + mScaleAnimator = new ValueAnimator(); + mScaleAnimator.addUpdateListener(MyController.this); + mScaleAnimator.addListener(MyController.this); + mDecelerateInterpolator = new DecelerateInterpolator(); + mCanStopScroll = true; + mHasNewScale = false; + } + + @Override + public boolean isScrolling() { + return !mScroller.isFinished(); + } + + @Override + public boolean isScalling() { + return mScaleAnimator.isRunning(); + } + + boolean hasNewGeometry() { + mHasNewPosition = mScroller.computeScrollOffset(); + if (!mHasNewPosition) { + mCanStopScroll = true; + } + // If the position is locked, then we always return true to force + // the position value to use the locked value. + return (mHasNewPosition || mHasNewScale || mIsPositionLocked); + } + + /** + * Always call {@link #hasNewGeometry()} before getting the new scale + * value. + */ + float getNewScale() { + if (!mHasNewScale) { + return mScale; + } + mHasNewScale = false; + return mNewScale; + } + + /** + * Always call {@link #hasNewGeometry()} before getting the new position + * value. + */ + int getNewPosition() { + if (mIsPositionLocked) { + if (mViewInfo[mLockedViewInfo] == null) + return mCenterX; + return mViewInfo[mLockedViewInfo].getCenterX(); + } + if (!mHasNewPosition) + return mCenterX; + return mScroller.getCurrX(); + } + + @Override + public void lockAtCurrentView() { + mIsPositionLocked = true; + mLockedViewInfo = mCurrentInfo; + } + + @Override + public void unlockPosition() { + if (mIsPositionLocked) { + // only when the position is previously locked we set the + // current position to make it consistent. + if (mViewInfo[mLockedViewInfo] != null) { + mCenterX = mViewInfo[mLockedViewInfo].getCenterX(); + } + mIsPositionLocked = false; + } + } + + private int estimateMinX(int dataID, int leftPos, int viewWidth) { + return leftPos - (dataID + 100) * (viewWidth + mViewGap); + } + + private int estimateMaxX(int dataID, int leftPos, int viewWidth) { + return leftPos + + (mDataAdapter.getTotalNumber() - dataID + 100) + * (viewWidth + mViewGap); + } + + @Override + public void scroll(float deltaX) { + if (mController.isScrolling()) { + return; + } + mCenterX += deltaX; + } + + @Override + public void fling(float velocityX) { + if (!stopScrolling() || mIsPositionLocked) { + return; + } + ViewInfo info = mViewInfo[mCurrentInfo]; + if (info == null) { + return; + } + + float scaledVelocityX = velocityX / mScale; + if (inCameraFullscreen() && scaledVelocityX < 0) { + // Swipe left in camera preview. + gotoFilmStrip(); + } + + int w = getWidth(); + // Estimation of possible length on the left. To ensure the + // velocity doesn't become too slow eventually, we add a huge number + // to the estimated maximum. + int minX = estimateMinX(info.getID(), info.getLeftPosition(), w); + // Estimation of possible length on the right. Likewise, exaggerate + // the possible maximum too. + int maxX = estimateMaxX(info.getID(), info.getLeftPosition(), w); + mScroller.fling(mCenterX, 0, (int) -velocityX, 0, minX, maxX, 0, 0); + + layoutChildren(); + } + + @Override + public boolean stopScrolling() { + if (!mCanStopScroll) + return false; + mScroller.forceFinished(true); + mHasNewPosition = false; + return true; + } + + private void stopScale() { + mScaleAnimator.cancel(); + mHasNewScale = false; + } + + @Override + public void scrollTo(int position, int duration, boolean interruptible) { + if (!stopScrolling() || mIsPositionLocked) + return; + mCanStopScroll = interruptible; + stopScrolling(); + mScroller.startScroll(mCenterX, 0, position - mCenterX, + 0, duration); + invalidate(); + } + + private void scaleTo(float scale, int duration) { + stopScale(); + mScaleAnimator.setDuration(duration); + mScaleAnimator.setFloatValues(mScale, scale); + mScaleAnimator.setInterpolator(mDecelerateInterpolator); + mScaleAnimator.start(); + mHasNewScale = true; + layoutChildren(); + } + + @Override + public void gotoFilmStrip() { + unlockPosition(); + scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST); + if (mListener != null) { + mListener.onSwitchMode(false); + } + } + + @Override + public void gotoFullScreen() { + if (mViewInfo[mCurrentInfo] != null) { + mController.scrollTo(mViewInfo[mCurrentInfo].getCenterX(), + DURATION_GEOMETRY_ADJUST, false); + } + enterFullScreen(); + } + + private void enterFullScreen() { + if (mListener != null) { + // TODO: After full size images snapping to fill the screen at + // the end of a scroll/fling is implemented, we should only make + // this call when the view on the center of the screen is + // camera preview + mListener.onSwitchMode(true); + } + if (inFullScreen()) { + return; + } + scaleTo(1f, DURATION_GEOMETRY_ADJUST); + } + + @Override + public void gotoCameraFullScreen() { + if (mDataAdapter.getImageData(0).getType() + != ImageData.TYPE_CAMERA_PREVIEW) { + return; + } + gotoFullScreen(); + scrollTo( + estimateMinX(mViewInfo[mCurrentInfo].getID(), + mViewInfo[mCurrentInfo].getLeftPosition(), + getWidth()), + DURATION_GEOMETRY_ADJUST, false); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mHasNewScale = true; + mNewScale = (Float) animation.getAnimatedValue(); + layoutChildren(); + } + + @Override + public void onAnimationStart(Animator anim) { + } + + @Override + public void onAnimationEnd(Animator anim) { + ViewInfo info = mViewInfo[mCurrentInfo]; + if (info != null && mCenterX == info.getCenterX()) { + if (inFullScreen()) { + lockAtCurrentView(); + } else if (inFilmStrip()) { + unlockPosition(); + } + } + } + + @Override + public void onAnimationCancel(Animator anim) { + } + + @Override + public void onAnimationRepeat(Animator anim) { + } + } + + private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener { + // Indicating the current trend of scaling is up (>1) or down (<1). + private float mScaleTrend; + + @Override + public boolean onSingleTapUp(float x, float y) { + if (inFilmStrip()) { + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) { + continue; + } + + if (mViewInfo[i].areaContains(x, y)) { + mController.scrollTo(mViewInfo[i].getCenterX(), + DURATION_GEOMETRY_ADJUST, false); + return true; + } + } + } + return false; + } + + @Override + public boolean onDoubleTap(float x, float y) { + if (inFilmStrip()) { + ViewInfo centerInfo = mViewInfo[mCurrentInfo]; + if (centerInfo != null && centerInfo.areaContains(x, y)) { + mController.gotoFullScreen(); + return true; + } + } else if (inFullScreen()) { + mController.gotoFilmStrip(); + return true; + } + return false; + } + + @Override + public boolean onDown(float x, float y) { + if (mController.isScrolling()) { + mController.stopScrolling(); + } + return true; + } + + @Override + public boolean onUp(float x, float y) { + float halfH = getHeight() / 2; + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) { + continue; + } + float transY = mViewInfo[i].getTranslationY(mScale); + if (transY == 0) { + continue; + } + int id = mViewInfo[i].getID(); + + if (mDataAdapter.getImageData(id) + .isUIActionSupported(ImageData.ACTION_DEMOTE) + && transY > halfH) { + demoteData(i, id); + } else if (mDataAdapter.getImageData(id) + .isUIActionSupported(ImageData.ACTION_PROMOTE) + && transY < -halfH) { + promoteData(i, id); + } else { + // put the view back. + mViewInfo[i].getView().animate() + .translationY(0f) + .alpha(1f) + .setDuration(DURATION_GEOMETRY_ADJUST) + .start(); + } + } + return false; + } + + @Override + public boolean onScroll(float x, float y, float dx, float dy) { + int deltaX = (int) (dx / mScale); + if (inFilmStrip()) { + if (Math.abs(dx) > Math.abs(dy)) { + if (deltaX > 0 && inCameraFullscreen()) { + mController.gotoFilmStrip(); + } + mController.scroll(deltaX); + } else { + // Vertical part. Promote or demote. + // int scaledDeltaY = (int) (dy * mScale); + int hit = 0; + Rect hitRect = new Rect(); + for (; hit < BUFFER_SIZE; hit++) { + if (mViewInfo[hit] == null) { + continue; + } + mViewInfo[hit].getView().getHitRect(hitRect); + if (hitRect.contains((int) x, (int) y)) { + break; + } + } + if (hit == BUFFER_SIZE) { + return false; + } + + ImageData data = mDataAdapter.getImageData(mViewInfo[hit].getID()); + float transY = mViewInfo[hit].getTranslationY(mScale) - dy / mScale; + if (!data.isUIActionSupported(ImageData.ACTION_DEMOTE) && transY > 0f) { + transY = 0f; + } + if (!data.isUIActionSupported(ImageData.ACTION_PROMOTE) && transY < 0f) { + transY = 0f; + } + mViewInfo[hit].setTranslationY(transY, mScale); + } + } else if (inFullScreen()) { + if (deltaX > 0 && inCameraFullscreen()) { + mController.gotoFilmStrip(); + } + mController.scroll(deltaX); + } + layoutChildren(); + + return true; + } + + @Override + public boolean onFling(float velocityX, float velocityY) { + if (Math.abs(velocityX) > Math.abs(velocityY)) { + mController.fling(velocityX); + } else { + // ignore vertical fling. + } + return true; + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + if (inCameraFullscreen()) { + return false; + } + mScaleTrend = 1f; + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (inCameraFullscreen()) { + return false; + } + + mScaleTrend = mScaleTrend * 0.3f + scale * 0.7f; + mScale *= scale; + if (mScale <= FILM_STRIP_SCALE) { + mScale = FILM_STRIP_SCALE; + } + if (mScale >= FULLSCREEN_SCALE) { + mScale = FULLSCREEN_SCALE; + } + layoutChildren(); + return true; + } + + @Override + public void onScaleEnd() { + if (mScaleTrend >= 1f) { + mController.gotoFullScreen(); + } else { + mController.gotoFilmStrip(); + } + mScaleTrend = 1f; + } + } +} diff --git a/src/com/android/camera/ui/FocusIndicator.java b/src/com/android/camera/ui/FocusIndicator.java new file mode 100644 index 000000000..e06057041 --- /dev/null +++ b/src/com/android/camera/ui/FocusIndicator.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2011 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; + +public interface FocusIndicator { + public void showStart(); + public void showSuccess(boolean timeout); + public void showFail(boolean timeout); + public void clear(); +} diff --git a/src/com/android/camera/ui/InLineSettingCheckBox.java b/src/com/android/camera/ui/InLineSettingCheckBox.java new file mode 100644 index 000000000..c1aa5a91c --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingCheckBox.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; + + +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +/* A check box setting control which turns on/off the setting. */ +public class InLineSettingCheckBox extends InLineSettingItem { + private CheckBox mCheckBox; + + OnCheckedChangeListener mCheckedChangeListener = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean desiredState) { + changeIndex(desiredState ? 1 : 0); + } + }; + + public InLineSettingCheckBox(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mCheckBox = (CheckBox) findViewById(R.id.setting_check_box); + mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener); + } + + @Override + public void initialize(ListPreference preference) { + super.initialize(preference); + // Add content descriptions for the increment and decrement buttons. + mCheckBox.setContentDescription(getContext().getResources().getString( + R.string.accessibility_check_box, mPreference.getTitle())); + } + + @Override + protected void updateView() { + mCheckBox.setOnCheckedChangeListener(null); + if (mOverrideValue == null) { + mCheckBox.setChecked(mIndex == 1); + } else { + int index = mPreference.findIndexOfValue(mOverrideValue); + mCheckBox.setChecked(index == 1); + } + mCheckBox.setOnCheckedChangeListener(mCheckedChangeListener); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + event.getText().add(mPreference.getTitle()); + return true; + } + + @Override + public void setEnabled(boolean enable) { + if (mTitle != null) mTitle.setEnabled(enable); + if (mCheckBox != null) mCheckBox.setEnabled(enable); + } +} diff --git a/src/com/android/camera/ui/InLineSettingItem.java b/src/com/android/camera/ui/InLineSettingItem.java new file mode 100644 index 000000000..839a77fd0 --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingItem.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.accessibility.AccessibilityEvent; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +/** + * A one-line camera setting could be one of three types: knob, switch or restore + * preference button. The setting includes a title for showing the preference + * title which is initialized in the SimpleAdapter. A knob also includes + * (ex: Picture size), a previous button, the current value (ex: 5MP), + * and a next button. A switch, i.e. the preference RecordLocationPreference, + * has only two values on and off which will be controlled in a switch button. + * Other setting popup window includes several InLineSettingItem items with + * different types if possible. + */ +public abstract class InLineSettingItem extends LinearLayout { + private Listener mListener; + protected ListPreference mPreference; + protected int mIndex; + // Scene mode can override the original preference value. + protected String mOverrideValue; + protected TextView mTitle; + + static public interface Listener { + public void onSettingChanged(ListPreference pref); + } + + public InLineSettingItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + + protected void setTitle(ListPreference preference) { + mTitle = ((TextView) findViewById(R.id.title)); + mTitle.setText(preference.getTitle()); + } + + public void initialize(ListPreference preference) { + setTitle(preference); + if (preference == null) return; + mPreference = preference; + reloadPreference(); + } + + protected abstract void updateView(); + + protected boolean changeIndex(int index) { + if (index >= mPreference.getEntryValues().length || index < 0) return false; + mIndex = index; + mPreference.setValueIndex(mIndex); + if (mListener != null) { + mListener.onSettingChanged(mPreference); + } + updateView(); + sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + return true; + } + + // The value of the preference may have changed. Update the UI. + public void reloadPreference() { + mIndex = mPreference.findIndexOfValue(mPreference.getValue()); + updateView(); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public void overrideSettings(String value) { + mOverrideValue = value; + updateView(); + } +} diff --git a/src/com/android/camera/ui/InLineSettingMenu.java b/src/com/android/camera/ui/InLineSettingMenu.java new file mode 100644 index 000000000..8e45c3e38 --- /dev/null +++ b/src/com/android/camera/ui/InLineSettingMenu.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +/* Setting menu item that will bring up a menu when you click on it. */ +public class InLineSettingMenu extends InLineSettingItem { + private static final String TAG = "InLineSettingMenu"; + // The view that shows the current selected setting. Ex: 5MP + private TextView mEntry; + + public InLineSettingMenu(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mEntry = (TextView) findViewById(R.id.current_setting); + } + + @Override + public void initialize(ListPreference preference) { + super.initialize(preference); + //TODO: add contentDescription + } + + @Override + protected void updateView() { + if (mOverrideValue == null) { + mEntry.setText(mPreference.getEntry()); + } else { + int index = mPreference.findIndexOfValue(mOverrideValue); + if (index != -1) { + mEntry.setText(mPreference.getEntries()[index]); + } else { + // Avoid the crash if camera driver has bugs. + Log.e(TAG, "Fail to find override value=" + mOverrideValue); + mPreference.print(); + } + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + event.getText().add(mPreference.getTitle() + mPreference.getEntry()); + return true; + } + + @Override + public void setEnabled(boolean enable) { + super.setEnabled(enable); + if (mTitle != null) mTitle.setEnabled(enable); + if (mEntry != null) mEntry.setEnabled(enable); + } +} diff --git a/src/com/android/camera/ui/LayoutChangeHelper.java b/src/com/android/camera/ui/LayoutChangeHelper.java new file mode 100644 index 000000000..ef4eb6a7a --- /dev/null +++ b/src/com/android/camera/ui/LayoutChangeHelper.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.view.View; + +public class LayoutChangeHelper implements LayoutChangeNotifier { + private LayoutChangeNotifier.Listener mListener; + private boolean mFirstTimeLayout; + private View mView; + + public LayoutChangeHelper(View v) { + mView = v; + mFirstTimeLayout = true; + } + + @Override + public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener) { + mListener = listener; + } + + public void onLayout(boolean changed, int l, int t, int r, int b) { + if (mListener == null) return; + if (mFirstTimeLayout || changed) { + mFirstTimeLayout = false; + mListener.onLayoutChange(mView, l, t, r, b); + } + } +} diff --git a/src/com/android/camera/ui/LayoutChangeNotifier.java b/src/com/android/camera/ui/LayoutChangeNotifier.java new file mode 100644 index 000000000..6261d34f6 --- /dev/null +++ b/src/com/android/camera/ui/LayoutChangeNotifier.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.view.View; + +public interface LayoutChangeNotifier { + public interface Listener { + // Invoked only when the layout has changed or it is the first layout. + public void onLayoutChange(View v, int l, int t, int r, int b); + } + + public void setOnLayoutChangeListener(LayoutChangeNotifier.Listener listener); +} diff --git a/src/com/android/camera/ui/LayoutNotifyView.java b/src/com/android/camera/ui/LayoutNotifyView.java new file mode 100644 index 000000000..6e118fc3a --- /dev/null +++ b/src/com/android/camera/ui/LayoutNotifyView.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +/* + * Customized view to support onLayoutChange() at or before API 10. + */ +public class LayoutNotifyView extends View implements LayoutChangeNotifier { + private LayoutChangeHelper mLayoutChangeHelper = new LayoutChangeHelper(this); + + public LayoutNotifyView(Context context) { + super(context); + } + + public LayoutNotifyView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void setOnLayoutChangeListener( + LayoutChangeNotifier.Listener listener) { + mLayoutChangeHelper.setOnLayoutChangeListener(listener); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mLayoutChangeHelper.onLayout(changed, l, t, r, b); + } +} diff --git a/src/com/android/camera/ui/ListPrefSettingPopup.java b/src/com/android/camera/ui/ListPrefSettingPopup.java new file mode 100644 index 000000000..cfef73f49 --- /dev/null +++ b/src/com/android/camera/ui/ListPrefSettingPopup.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.ListView; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.SimpleAdapter; + +import com.android.camera.IconListPreference; +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +// A popup window that shows one camera setting. The title is the name of the +// setting (ex: white-balance). The entries are the supported values (ex: +// daylight, incandescent, etc). If initialized with an IconListPreference, +// the entries will contain both text and icons. Otherwise, entries will be +// shown in text. +public class ListPrefSettingPopup extends AbstractSettingPopup implements + AdapterView.OnItemClickListener { + private static final String TAG = "ListPrefSettingPopup"; + private ListPreference mPreference; + private Listener mListener; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public ListPrefSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + private class ListPrefSettingAdapter extends SimpleAdapter { + ListPrefSettingAdapter(Context context, List<? extends Map<String, ?>> data, + int resource, String[] from, int[] to) { + super(context, data, resource, from, to); + } + + @Override + public void setViewImage(ImageView v, String value) { + if ("".equals(value)) { + // Some settings have no icons. Ex: exposure compensation. + v.setVisibility(View.GONE); + } else { + super.setViewImage(v, value); + } + } + } + + public void initialize(ListPreference preference) { + mPreference = preference; + Context context = getContext(); + CharSequence[] entries = mPreference.getEntries(); + int[] iconIds = null; + if (preference instanceof IconListPreference) { + iconIds = ((IconListPreference) mPreference).getImageIds(); + if (iconIds == null) { + iconIds = ((IconListPreference) mPreference).getLargeIconIds(); + } + } + // Set title. + mTitle.setText(mPreference.getTitle()); + + // Prepare the ListView. + ArrayList<HashMap<String, Object>> listItem = + new ArrayList<HashMap<String, Object>>(); + for(int i = 0; i < entries.length; ++i) { + HashMap<String, Object> map = new HashMap<String, Object>(); + map.put("text", entries[i].toString()); + if (iconIds != null) map.put("image", iconIds[i]); + listItem.add(map); + } + SimpleAdapter listItemAdapter = new ListPrefSettingAdapter(context, listItem, + R.layout.setting_item, + new String[] {"text", "image"}, + new int[] {R.id.text, R.id.image}); + ((ListView) mSettingList).setAdapter(listItemAdapter); + ((ListView) mSettingList).setOnItemClickListener(this); + reloadPreference(); + } + + // The value of the preference may have changed. Update the UI. + @Override + public void reloadPreference() { + int index = mPreference.findIndexOfValue(mPreference.getValue()); + if (index != -1) { + ((ListView) mSettingList).setItemChecked(index, true); + } else { + Log.e(TAG, "Invalid preference value."); + mPreference.print(); + } + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, + int index, long id) { + mPreference.setValueIndex(index); + if (mListener != null) mListener.onListPrefChanged(mPreference); + } +} diff --git a/src/com/android/camera/ui/MoreSettingPopup.java b/src/com/android/camera/ui/MoreSettingPopup.java new file mode 100644 index 000000000..5900058df --- /dev/null +++ b/src/com/android/camera/ui/MoreSettingPopup.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import com.android.camera.ListPreference; +import com.android.camera.PreferenceGroup; +import com.android.gallery3d.R; + +import java.util.ArrayList; + +/* A popup window that contains several camera settings. */ +public class MoreSettingPopup extends AbstractSettingPopup + implements InLineSettingItem.Listener, + AdapterView.OnItemClickListener { + @SuppressWarnings("unused") + private static final String TAG = "MoreSettingPopup"; + + private Listener mListener; + private ArrayList<ListPreference> mListItem = new ArrayList<ListPreference>(); + + // Keep track of which setting items are disabled + // e.g. White balance will be disabled when scene mode is set to non-auto + private boolean[] mEnabled; + + static public interface Listener { + public void onSettingChanged(ListPreference pref); + public void onPreferenceClicked(ListPreference pref); + } + + private class MoreSettingAdapter extends ArrayAdapter<ListPreference> { + LayoutInflater mInflater; + String mOnString; + String mOffString; + MoreSettingAdapter() { + super(MoreSettingPopup.this.getContext(), 0, mListItem); + Context context = getContext(); + mInflater = LayoutInflater.from(context); + mOnString = context.getString(R.string.setting_on); + mOffString = context.getString(R.string.setting_off); + } + + private int getSettingLayoutId(ListPreference pref) { + + if (isOnOffPreference(pref)) { + return R.layout.in_line_setting_check_box; + } + return R.layout.in_line_setting_menu; + } + + private boolean isOnOffPreference(ListPreference pref) { + CharSequence[] entries = pref.getEntries(); + if (entries.length != 2) return false; + String str1 = entries[0].toString(); + String str2 = entries[1].toString(); + return ((str1.equals(mOnString) && str2.equals(mOffString)) || + (str1.equals(mOffString) && str2.equals(mOnString))); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView != null) return convertView; + + ListPreference pref = mListItem.get(position); + + int viewLayoutId = getSettingLayoutId(pref); + InLineSettingItem view = (InLineSettingItem) + mInflater.inflate(viewLayoutId, parent, false); + + view.initialize(pref); // no init for restore one + view.setSettingChangedListener(MoreSettingPopup.this); + if (position >= 0 && position < mEnabled.length) { + view.setEnabled(mEnabled[position]); + } else { + Log.w(TAG, "Invalid input: enabled list length, " + mEnabled.length + + " position " + position); + } + return view; + } + + @Override + public boolean isEnabled(int position) { + if (position >= 0 && position < mEnabled.length) { + return mEnabled[position]; + } + return true; + } + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public MoreSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize(PreferenceGroup group, String[] keys) { + // Prepare the setting items. + for (int i = 0; i < keys.length; ++i) { + ListPreference pref = group.findPreference(keys[i]); + if (pref != null) mListItem.add(pref); + } + + ArrayAdapter<ListPreference> mListItemAdapter = new MoreSettingAdapter(); + ((ListView) mSettingList).setAdapter(mListItemAdapter); + ((ListView) mSettingList).setOnItemClickListener(this); + ((ListView) mSettingList).setSelector(android.R.color.transparent); + // Initialize mEnabled + mEnabled = new boolean[mListItem.size()]; + for (int i = 0; i < mEnabled.length; i++) { + mEnabled[i] = true; + } + } + + // When preferences are disabled, we will display them grayed out. Users + // will not be able to change the disabled preferences, but they can still see + // the current value of the preferences + public void setPreferenceEnabled(String key, boolean enable) { + int count = mEnabled == null ? 0 : mEnabled.length; + for (int j = 0; j < count; j++) { + ListPreference pref = mListItem.get(j); + if (pref != null && key.equals(pref.getKey())) { + mEnabled[j] = enable; + break; + } + } + } + + public void onSettingChanged(ListPreference pref) { + if (mListener != null) { + mListener.onSettingChanged(pref); + } + } + + // Scene mode can override other camera settings (ex: flash mode). + public void overrideSettings(final String ... keyvalues) { + int count = mEnabled == null ? 0 : mEnabled.length; + for (int i = 0; i < keyvalues.length; i += 2) { + String key = keyvalues[i]; + String value = keyvalues[i + 1]; + for (int j = 0; j < count; j++) { + ListPreference pref = mListItem.get(j); + if (pref != null && key.equals(pref.getKey())) { + // Change preference + if (value != null) pref.setValue(value); + // If the preference is overridden, disable the preference + boolean enable = value == null; + mEnabled[j] = enable; + if (mSettingList.getChildCount() > j) { + mSettingList.getChildAt(j).setEnabled(enable); + } + } + } + } + reloadPreference(); + } + + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, + long id) { + if (mListener != null) { + ListPreference pref = mListItem.get(position); + mListener.onPreferenceClicked(pref); + } + } + + @Override + public void reloadPreference() { + int count = mSettingList.getChildCount(); + for (int i = 0; i < count; i++) { + ListPreference pref = mListItem.get(i); + if (pref != null) { + InLineSettingItem settingItem = + (InLineSettingItem) mSettingList.getChildAt(i); + settingItem.reloadPreference(); + } + } + } +} diff --git a/src/com/android/camera/ui/OnIndicatorEventListener.java b/src/com/android/camera/ui/OnIndicatorEventListener.java new file mode 100644 index 000000000..566f5c7a8 --- /dev/null +++ b/src/com/android/camera/ui/OnIndicatorEventListener.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 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; + +public interface OnIndicatorEventListener { + public static int EVENT_ENTER_SECOND_LEVEL_INDICATOR_BAR = 0; + public static int EVENT_LEAVE_SECOND_LEVEL_INDICATOR_BAR = 1; + public static int EVENT_ENTER_ZOOM_CONTROL = 2; + public static int EVENT_LEAVE_ZOOM_CONTROL = 3; + void onIndicatorEvent(int event); +} diff --git a/src/com/android/camera/ui/OverlayRenderer.java b/src/com/android/camera/ui/OverlayRenderer.java new file mode 100644 index 000000000..417e219aa --- /dev/null +++ b/src/com/android/camera/ui/OverlayRenderer.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.view.MotionEvent; + +public abstract class OverlayRenderer implements RenderOverlay.Renderer { + + private static final String TAG = "CAM OverlayRenderer"; + protected RenderOverlay mOverlay; + + protected int mLeft, mTop, mRight, mBottom; + + protected boolean mVisible; + + public void setVisible(boolean vis) { + mVisible = vis; + update(); + } + + public boolean isVisible() { + return mVisible; + } + + // default does not handle touch + @Override + public boolean handlesTouch() { + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent evt) { + return false; + } + + public abstract void onDraw(Canvas canvas); + + public void draw(Canvas canvas) { + if (mVisible) { + onDraw(canvas); + } + } + + @Override + public void setOverlay(RenderOverlay overlay) { + mOverlay = overlay; + } + + @Override + public void layout(int left, int top, int right, int bottom) { + mLeft = left; + mRight = right; + mTop = top; + mBottom = bottom; + } + + protected Context getContext() { + if (mOverlay != null) { + return mOverlay.getContext(); + } else { + return null; + } + } + + public int getWidth() { + return mRight - mLeft; + } + + public int getHeight() { + return mBottom - mTop; + } + + protected void update() { + if (mOverlay != null) { + mOverlay.update(); + } + } + +} diff --git a/src/com/android/camera/ui/PieItem.java b/src/com/android/camera/ui/PieItem.java new file mode 100644 index 000000000..47fe06758 --- /dev/null +++ b/src/com/android/camera/ui/PieItem.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.drawable.Drawable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Pie menu item + */ +public class PieItem { + + public static interface OnClickListener { + void onClick(PieItem item); + } + + private Drawable mDrawable; + private int level; + + private boolean mSelected; + private boolean mEnabled; + private List<PieItem> mItems; + private Path mPath; + private OnClickListener mOnClickListener; + private float mAlpha; + private CharSequence mLabel; + + // Gray out the view when disabled + private static final float ENABLED_ALPHA = 1; + private static final float DISABLED_ALPHA = (float) 0.3; + private boolean mChangeAlphaWhenDisabled = true; + + public PieItem(Drawable drawable, int level) { + mDrawable = drawable; + this.level = level; + if (drawable != null) { + setAlpha(1f); + } + mEnabled = true; + } + + public void setLabel(CharSequence txt) { + mLabel = txt; + } + + public CharSequence getLabel() { + return mLabel; + } + + public boolean hasItems() { + return mItems != null; + } + + public List<PieItem> getItems() { + return mItems; + } + + public void addItem(PieItem item) { + if (mItems == null) { + mItems = new ArrayList<PieItem>(); + } + mItems.add(item); + } + + public void clearItems() { + mItems = null; + } + + public void setLevel(int level) { + this.level = level; + } + + public void setPath(Path p) { + mPath = p; + } + + public Path getPath() { + return mPath; + } + + public void setChangeAlphaWhenDisabled (boolean enable) { + mChangeAlphaWhenDisabled = enable; + } + + public void setAlpha(float alpha) { + mAlpha = alpha; + mDrawable.setAlpha((int) (255 * alpha)); + } + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + if (mChangeAlphaWhenDisabled) { + if (mEnabled) { + setAlpha(ENABLED_ALPHA); + } else { + setAlpha(DISABLED_ALPHA); + } + } + } + + public boolean isEnabled() { + return mEnabled; + } + + public void setSelected(boolean s) { + mSelected = s; + } + + public boolean isSelected() { + return mSelected; + } + + public int getLevel() { + return level; + } + + + public void setOnClickListener(OnClickListener listener) { + mOnClickListener = listener; + } + + public void performClick() { + if (mOnClickListener != null) { + mOnClickListener.onClick(this); + } + } + + public int getIntrinsicWidth() { + return mDrawable.getIntrinsicWidth(); + } + + public int getIntrinsicHeight() { + return mDrawable.getIntrinsicHeight(); + } + + public void setBounds(int left, int top, int right, int bottom) { + mDrawable.setBounds(left, top, right, bottom); + } + + public void draw(Canvas canvas) { + mDrawable.draw(canvas); + } + + public void setImageResource(Context context, int resId) { + Drawable d = context.getResources().getDrawable(resId).mutate(); + d.setBounds(mDrawable.getBounds()); + mDrawable = d; + setAlpha(mAlpha); + } + +} diff --git a/src/com/android/camera/ui/PieMenuButton.java b/src/com/android/camera/ui/PieMenuButton.java new file mode 100644 index 000000000..0e23226b2 --- /dev/null +++ b/src/com/android/camera/ui/PieMenuButton.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +public class PieMenuButton extends View { + private boolean mPressed; + private boolean mReadyToClick = false; + public PieMenuButton(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + mPressed = isPressed(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean handled = super.onTouchEvent(event); + if (MotionEvent.ACTION_UP == event.getAction() && mPressed) { + // Perform a customized click as soon as the ACTION_UP event + // is received. The reason for doing this is that Framework + // delays the performClick() call after ACTION_UP. But we do not + // want the delay because it affects an important state change + // for PieRenderer. + mReadyToClick = true; + performClick(); + } + return handled; + } + + @Override + public boolean performClick() { + if (mReadyToClick) { + // We only respond to our customized click which happens right + // after ACTION_UP event is received, with no delay. + mReadyToClick = false; + return super.performClick(); + } + return false; + } +};
\ No newline at end of file diff --git a/src/com/android/camera/ui/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java new file mode 100644 index 000000000..c78107ce9 --- /dev/null +++ b/src/com/android/camera/ui/PieRenderer.java @@ -0,0 +1,1091 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.RectF; +import android.os.Handler; +import android.os.Message; +import android.util.FloatMath; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.animation.Animation; +import android.view.animation.Transformation; + +import com.android.camera.drawable.TextDrawable; +import com.android.gallery3d.R; + +import java.util.ArrayList; +import java.util.List; + +public class PieRenderer extends OverlayRenderer + implements FocusIndicator { + + private static final String TAG = "CAM Pie"; + + // Sometimes continuous autofocus starts and stops several times quickly. + // These states are used to make sure the animation is run for at least some + // time. + private volatile int mState; + private ScaleAnimation mAnimation = new ScaleAnimation(); + private static final int STATE_IDLE = 0; + private static final int STATE_FOCUSING = 1; + private static final int STATE_FINISHING = 2; + private static final int STATE_PIE = 8; + + private static final float MATH_PI_2 = (float)(Math.PI / 2); + + private Runnable mDisappear = new Disappear(); + private Animation.AnimationListener mEndAction = new EndAction(); + private static final int SCALING_UP_TIME = 600; + private static final int SCALING_DOWN_TIME = 100; + private static final int DISAPPEAR_TIMEOUT = 200; + private static final int DIAL_HORIZONTAL = 157; + // fade out timings + private static final int PIE_FADE_OUT_DURATION = 600; + + private static final long PIE_FADE_IN_DURATION = 200; + private static final long PIE_XFADE_DURATION = 200; + private static final long PIE_SELECT_FADE_DURATION = 300; + private static final long PIE_OPEN_SUB_DELAY = 400; + private static final long PIE_SLICE_DURATION = 80; + + private static final int MSG_OPEN = 0; + private static final int MSG_CLOSE = 1; + private static final int MSG_OPENSUBMENU = 2; + + protected static float CENTER = (float) Math.PI / 2; + protected static float RAD24 = (float)(24 * Math.PI / 180); + protected static final float SWEEP_SLICE = 0.14f; + protected static final float SWEEP_ARC = 0.23f; + + // geometry + private int mRadius; + private int mRadiusInc; + + // the detection if touch is inside a slice is offset + // inbounds by this amount to allow the selection to show before the + // finger covers it + private int mTouchOffset; + + private List<PieItem> mOpen; + + private Paint mSelectedPaint; + private Paint mSubPaint; + private Paint mMenuArcPaint; + + // touch handling + private PieItem mCurrentItem; + + private Paint mFocusPaint; + private int mSuccessColor; + private int mFailColor; + private int mCircleSize; + private int mFocusX; + private int mFocusY; + private int mCenterX; + private int mCenterY; + private int mArcCenterY; + private int mSliceCenterY; + private int mPieCenterX; + private int mPieCenterY; + private int mSliceRadius; + private int mArcRadius; + private int mArcOffset; + + private int mDialAngle; + private RectF mCircle; + private RectF mDial; + private Point mPoint1; + private Point mPoint2; + private int mStartAnimationAngle; + private boolean mFocused; + private int mInnerOffset; + private int mOuterStroke; + private int mInnerStroke; + private boolean mTapMode; + private boolean mBlockFocus; + private int mTouchSlopSquared; + private Point mDown; + private boolean mOpening; + private ValueAnimator mXFade; + private ValueAnimator mFadeIn; + private ValueAnimator mFadeOut; + private ValueAnimator mSlice; + private volatile boolean mFocusCancelled; + private PointF mPolar = new PointF(); + private TextDrawable mLabel; + private int mDeadZone; + private int mAngleZone; + private float mCenterAngle; + + + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_OPEN: + if (mListener != null) { + mListener.onPieOpened(mPieCenterX, mPieCenterY); + } + break; + case MSG_CLOSE: + if (mListener != null) { + mListener.onPieClosed(); + } + break; + case MSG_OPENSUBMENU: + onEnterOpen(); + break; + } + + } + }; + + private PieListener mListener; + + static public interface PieListener { + public void onPieOpened(int centerX, int centerY); + public void onPieClosed(); + } + + public void setPieListener(PieListener pl) { + mListener = pl; + } + + public PieRenderer(Context context) { + init(context); + } + + private void init(Context ctx) { + setVisible(false); + mOpen = new ArrayList<PieItem>(); + mOpen.add(new PieItem(null, 0)); + Resources res = ctx.getResources(); + mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); + mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); + mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset); + mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); + mSelectedPaint = new Paint(); + mSelectedPaint.setColor(Color.argb(255, 51, 181, 229)); + mSelectedPaint.setAntiAlias(true); + mSubPaint = new Paint(); + mSubPaint.setAntiAlias(true); + mSubPaint.setColor(Color.argb(200, 250, 230, 128)); + mFocusPaint = new Paint(); + mFocusPaint.setAntiAlias(true); + mFocusPaint.setColor(Color.WHITE); + mFocusPaint.setStyle(Paint.Style.STROKE); + mSuccessColor = Color.GREEN; + mFailColor = Color.RED; + mCircle = new RectF(); + mDial = new RectF(); + mPoint1 = new Point(); + mPoint2 = new Point(); + mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset); + mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); + mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); + mState = STATE_IDLE; + mBlockFocus = false; + mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop(); + mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared; + mDown = new Point(); + mMenuArcPaint = new Paint(); + mMenuArcPaint.setAntiAlias(true); + mMenuArcPaint.setColor(Color.argb(140, 255, 255, 255)); + mMenuArcPaint.setStrokeWidth(10); + mMenuArcPaint.setStyle(Paint.Style.STROKE); + mSliceRadius = res.getDimensionPixelSize(R.dimen.pie_item_radius); + mArcRadius = res.getDimensionPixelSize(R.dimen.pie_arc_radius); + mArcOffset = res.getDimensionPixelSize(R.dimen.pie_arc_offset); + mLabel = new TextDrawable(res); + mLabel.setDropShadow(true); + mDeadZone = res.getDimensionPixelSize(R.dimen.pie_deadzone_width); + mAngleZone = res.getDimensionPixelSize(R.dimen.pie_anglezone_width); + } + + private PieItem getRoot() { + return mOpen.get(0); + } + + public boolean showsItems() { + return mTapMode; + } + + public void addItem(PieItem item) { + // add the item to the pie itself + getRoot().addItem(item); + } + + public void clearItems() { + getRoot().clearItems(); + } + + public void showInCenter() { + if ((mState == STATE_PIE) && isVisible()) { + mTapMode = false; + show(false); + } else { + if (mState != STATE_IDLE) { + cancelFocus(); + } + mState = STATE_PIE; + resetPieCenter(); + setCenter(mPieCenterX, mPieCenterY); + mTapMode = true; + show(true); + } + } + + public void hide() { + show(false); + } + + /** + * guaranteed has center set + * @param show + */ + private void show(boolean show) { + if (show) { + if (mXFade != null) { + mXFade.cancel(); + } + mState = STATE_PIE; + // ensure clean state + mCurrentItem = null; + PieItem root = getRoot(); + for (PieItem openItem : mOpen) { + if (openItem.hasItems()) { + for (PieItem item : openItem.getItems()) { + item.setSelected(false); + } + } + } + mLabel.setText(""); + mOpen.clear(); + mOpen.add(root); + layoutPie(); + fadeIn(); + } else { + mState = STATE_IDLE; + mTapMode = false; + if (mXFade != null) { + mXFade.cancel(); + } + if (mLabel != null) { + mLabel.setText(""); + } + } + setVisible(show); + mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); + } + + public boolean isOpen() { + return mState == STATE_PIE && isVisible(); + } + + private void fadeIn() { + mFadeIn = new ValueAnimator(); + mFadeIn.setFloatValues(0f, 1f); + mFadeIn.setDuration(PIE_FADE_IN_DURATION); + // linear interpolation + mFadeIn.setInterpolator(null); + mFadeIn.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mFadeIn = null; + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator arg0) { + } + }); + mFadeIn.start(); + } + + public void setCenter(int x, int y) { + mPieCenterX = x; + mPieCenterY = y; + mSliceCenterY = y + mSliceRadius - mArcOffset; + mArcCenterY = y - mArcOffset + mArcRadius; + } + + @Override + public void layout(int l, int t, int r, int b) { + super.layout(l, t, r, b); + mCenterX = (r - l) / 2; + mCenterY = (b - t) / 2; + + mFocusX = mCenterX; + mFocusY = mCenterY; + resetPieCenter(); + setCircle(mFocusX, mFocusY); + if (isVisible() && mState == STATE_PIE) { + setCenter(mPieCenterX, mPieCenterY); + layoutPie(); + } + } + + private void resetPieCenter() { + mPieCenterX = mCenterX; + mPieCenterY = (int) (getHeight() - 2.5f * mDeadZone); + } + + private void layoutPie() { + mCenterAngle = getCenterAngle(); + layoutItems(0, getRoot().getItems()); + layoutLabel(getLevel()); + } + + private void layoutLabel(int level) { + int x = mPieCenterX - (int) (FloatMath.sin(mCenterAngle - CENTER) + * (mArcRadius + (level + 2) * mRadiusInc)); + int y = mArcCenterY - mArcRadius - (level + 2) * mRadiusInc; + int w = mLabel.getIntrinsicWidth(); + int h = mLabel.getIntrinsicHeight(); + mLabel.setBounds(x - w/2, y - h/2, x + w/2, y + h/2); + } + + private void layoutItems(int level, List<PieItem> items) { + int extend = 1; + Path path = makeSlice(getDegrees(0) + extend, getDegrees(SWEEP_ARC) - extend, + mArcRadius, mArcRadius + mRadiusInc + mRadiusInc / 4, + mPieCenterX, mArcCenterY - level * mRadiusInc); + final int count = items.size(); + int pos = 0; + for (PieItem item : items) { + // shared between items + item.setPath(path); + float angle = getArcCenter(item, pos, count); + int w = item.getIntrinsicWidth(); + int h = item.getIntrinsicHeight(); + // move views to outer border + int r = mArcRadius + mRadiusInc * 2 / 3; + int x = (int) (r * Math.cos(angle)); + int y = mArcCenterY - (level * mRadiusInc) - (int) (r * Math.sin(angle)) - h / 2; + x = mPieCenterX + x - w / 2; + item.setBounds(x, y, x + w, y + h); + item.setLevel(level); + if (item.hasItems()) { + layoutItems(level + 1, item.getItems()); + } + pos++; + } + } + + private Path makeSlice(float start, float end, int inner, int outer, int cx, int cy) { + RectF bb = + new RectF(cx - outer, cy - outer, cx + outer, + cy + outer); + RectF bbi = + new RectF(cx - inner, cy - inner, cx + inner, + cy + inner); + Path path = new Path(); + path.arcTo(bb, start, end - start, true); + path.arcTo(bbi, end, start - end); + path.close(); + return path; + } + + private float getArcCenter(PieItem item, int pos, int count) { + return getCenter(pos, count, SWEEP_ARC); + } + + private float getSliceCenter(PieItem item, int pos, int count) { + float center = (getCenterAngle() - CENTER) * 0.5f + CENTER; + return center + (count - 1) * SWEEP_SLICE / 2f + - pos * SWEEP_SLICE; + } + + private float getCenter(int pos, int count, float sweep) { + return mCenterAngle + (count - 1) * sweep / 2f - pos * sweep; + } + + private float getCenterAngle() { + float center = CENTER; + if (mPieCenterX < mDeadZone + mAngleZone) { + center = CENTER - (mAngleZone - mPieCenterX + mDeadZone) * RAD24 + / (float) mAngleZone; + } else if (mPieCenterX > getWidth() - mDeadZone - mAngleZone) { + center = CENTER + (mPieCenterX - (getWidth() - mDeadZone - mAngleZone)) * RAD24 + / (float) mAngleZone; + } + return center; + } + + /** + * converts a + * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock) + * @return skia angle + */ + private float getDegrees(double angle) { + return (float) (360 - 180 * angle / Math.PI); + } + + private void startFadeOut(final PieItem item) { + if (mFadeIn != null) { + mFadeIn.cancel(); + } + if (mXFade != null) { + mXFade.cancel(); + } + mFadeOut = new ValueAnimator(); + mFadeOut.setFloatValues(1f, 0f); + mFadeOut.setDuration(PIE_FADE_OUT_DURATION); + mFadeOut.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animator) { + } + + @Override + public void onAnimationEnd(Animator animator) { + item.performClick(); + mFadeOut = null; + deselect(); + show(false); + mOverlay.setAlpha(1); + } + + @Override + public void onAnimationRepeat(Animator animator) { + } + + @Override + public void onAnimationCancel(Animator animator) { + } + + }); + mFadeOut.start(); + } + + // root does not count + private boolean hasOpenItem() { + return mOpen.size() > 1; + } + + // pop an item of the open item stack + private PieItem closeOpenItem() { + PieItem item = getOpenItem(); + mOpen.remove(mOpen.size() -1); + return item; + } + + private PieItem getOpenItem() { + return mOpen.get(mOpen.size() - 1); + } + + // return the children either the root or parent of the current open item + private PieItem getParent() { + return mOpen.get(Math.max(0, mOpen.size() - 2)); + } + + private int getLevel() { + return mOpen.size() - 1; + } + + @Override + public void onDraw(Canvas canvas) { + float alpha = 1; + if (mXFade != null) { + alpha = (Float) mXFade.getAnimatedValue(); + } else if (mFadeIn != null) { + alpha = (Float) mFadeIn.getAnimatedValue(); + } else if (mFadeOut != null) { + alpha = (Float) mFadeOut.getAnimatedValue(); + } + int state = canvas.save(); + if (mFadeIn != null) { + float sf = 0.9f + alpha * 0.1f; + canvas.scale(sf, sf, mPieCenterX, mPieCenterY); + } + if (mState != STATE_PIE) { + drawFocus(canvas); + } + if (mState == STATE_FINISHING) { + canvas.restoreToCount(state); + return; + } + if (mState != STATE_PIE) return; + if (!hasOpenItem() || (mXFade != null)) { + // draw base menu + drawArc(canvas, getLevel(), getParent()); + List<PieItem> items = getParent().getItems(); + final int count = items.size(); + int pos = 0; + for (PieItem item : getParent().getItems()) { + drawItem(Math.max(0, mOpen.size() - 2), pos, count, canvas, item, alpha); + pos++; + } + mLabel.draw(canvas); + } + if (hasOpenItem()) { + int level = getLevel(); + drawArc(canvas, level, getOpenItem()); + List<PieItem> items = getOpenItem().getItems(); + final int count = items.size(); + int pos = 0; + for (PieItem inner : items) { + if (mFadeOut != null) { + drawItem(level, pos, count, canvas, inner, alpha); + } else { + drawItem(level, pos, count, canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1); + } + pos++; + } + mLabel.draw(canvas); + } + canvas.restoreToCount(state); + } + + private void drawArc(Canvas canvas, int level, PieItem item) { + // arc + if (mState == STATE_PIE) { + final int count = item.getItems().size(); + float start = mCenterAngle + (count * SWEEP_ARC / 2f); + float end = mCenterAngle - (count * SWEEP_ARC / 2f); + int cy = mArcCenterY - level * mRadiusInc; + canvas.drawArc(new RectF(mPieCenterX - mArcRadius, cy - mArcRadius, + mPieCenterX + mArcRadius, cy + mArcRadius), + getDegrees(end), getDegrees(start) - getDegrees(end), false, mMenuArcPaint); + } + } + + private void drawItem(int level, int pos, int count, Canvas canvas, PieItem item, float alpha) { + if (mState == STATE_PIE) { + if (item.getPath() != null) { + int y = mArcCenterY - level * mRadiusInc; + if (item.isSelected()) { + Paint p = mSelectedPaint; + int state = canvas.save(); + float angle = 0; + if (mSlice != null) { + angle = (Float) mSlice.getAnimatedValue(); + } else { + angle = getArcCenter(item, pos, count) - SWEEP_ARC / 2f; + } + angle = getDegrees(angle); + canvas.rotate(angle, mPieCenterX, y); + if (mFadeOut != null) { + p.setAlpha((int)(255 * alpha)); + } + canvas.drawPath(item.getPath(), p); + if (mFadeOut != null) { + p.setAlpha(255); + } + canvas.restoreToCount(state); + } + if (mFadeOut == null) { + alpha = alpha * (item.isEnabled() ? 1 : 0.3f); + // draw the item view + item.setAlpha(alpha); + } + item.draw(canvas); + } + } + } + + @Override + public boolean onTouchEvent(MotionEvent evt) { + float x = evt.getX(); + float y = evt.getY(); + int action = evt.getActionMasked(); + getPolar(x, y, !mTapMode, mPolar); + if (MotionEvent.ACTION_DOWN == action) { + if ((x < mDeadZone) || (x > getWidth() - mDeadZone)) { + return false; + } + mDown.x = (int) evt.getX(); + mDown.y = (int) evt.getY(); + mOpening = false; + if (mTapMode) { + PieItem item = findItem(mPolar); + if ((item != null) && (mCurrentItem != item)) { + mState = STATE_PIE; + onEnter(item); + } + } else { + setCenter((int) x, (int) y); + show(true); + } + return true; + } else if (MotionEvent.ACTION_UP == action) { + if (isVisible()) { + PieItem item = mCurrentItem; + if (mTapMode) { + item = findItem(mPolar); + if (mOpening) { + mOpening = false; + return true; + } + } + if (item == null) { + mTapMode = false; + show(false); + } else if (!mOpening && !item.hasItems()) { + startFadeOut(item); + mTapMode = false; + } else { + mTapMode = true; + } + return true; + } + } else if (MotionEvent.ACTION_CANCEL == action) { + if (isVisible() || mTapMode) { + show(false); + } + deselect(); + mHandler.removeMessages(MSG_OPENSUBMENU); + return false; + } else if (MotionEvent.ACTION_MOVE == action) { + if (pulledToCenter(mPolar)) { + mHandler.removeMessages(MSG_OPENSUBMENU); + if (hasOpenItem()) { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + closeOpenItem(); + mCurrentItem = null; + } else { + deselect(); + } + mLabel.setText(""); + return false; + } + PieItem item = findItem(mPolar); + boolean moved = hasMoved(evt); + if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) { + mHandler.removeMessages(MSG_OPENSUBMENU); + // only select if we didn't just open or have moved past slop + if (moved) { + // switch back to swipe mode + mTapMode = false; + } + onEnterSelect(item); + mHandler.sendEmptyMessageDelayed(MSG_OPENSUBMENU, PIE_OPEN_SUB_DELAY); + } + } + return false; + } + + private boolean pulledToCenter(PointF polarCoords) { + return polarCoords.y < mArcRadius - mRadiusInc; + } + + private boolean inside(PointF polar, PieItem item, int pos, int count) { + float start = getSliceCenter(item, pos, count) - SWEEP_SLICE / 2f; + boolean res = (mArcRadius < polar.y) + && (start < polar.x) + && (start + SWEEP_SLICE > polar.x) + && (!mTapMode || (mArcRadius + mRadiusInc > polar.y)); + return res; + } + + private void getPolar(float x, float y, boolean useOffset, PointF res) { + // get angle and radius from x/y + res.x = (float) Math.PI / 2; + x = x - mPieCenterX; + float y1 = mSliceCenterY - getLevel() * mRadiusInc - y; + float y2 = mArcCenterY - getLevel() * mRadiusInc - y; + res.y = (float) Math.sqrt(x * x + y2 * y2); + if (x != 0) { + res.x = (float) Math.atan2(y1, x); + if (res.x < 0) { + res.x = (float) (2 * Math.PI + res.x); + } + } + res.y = res.y + (useOffset ? mTouchOffset : 0); + } + + private boolean hasMoved(MotionEvent e) { + return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x) + + (e.getY() - mDown.y) * (e.getY() - mDown.y); + } + + private void onEnterSelect(PieItem item) { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (item != null && item.isEnabled()) { + moveSelection(mCurrentItem, item); + item.setSelected(true); + mCurrentItem = item; + mLabel.setText(mCurrentItem.getLabel()); + layoutLabel(getLevel()); + } else { + mCurrentItem = null; + } + } + + private void onEnterOpen() { + if ((mCurrentItem != null) && (mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) { + openCurrentItem(); + } + } + + /** + * enter a slice for a view + * updates model only + * @param item + */ + private void onEnter(PieItem item) { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (item != null && item.isEnabled()) { + item.setSelected(true); + mCurrentItem = item; + mLabel.setText(mCurrentItem.getLabel()); + if ((mCurrentItem != getOpenItem()) && mCurrentItem.hasItems()) { + openCurrentItem(); + layoutLabel(getLevel()); + } + } else { + mCurrentItem = null; + } + } + + private void deselect() { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (hasOpenItem()) { + PieItem item = closeOpenItem(); + onEnter(item); + } else { + mCurrentItem = null; + } + } + + private int getItemPos(PieItem target) { + List<PieItem> items = getOpenItem().getItems(); + return items.indexOf(target); + } + + private int getCurrentCount() { + return getOpenItem().getItems().size(); + } + + private void moveSelection(PieItem from, PieItem to) { + final int count = getCurrentCount(); + final int fromPos = getItemPos(from); + final int toPos = getItemPos(to); + if (fromPos != -1 && toPos != -1) { + float startAngle = getArcCenter(from, getItemPos(from), count) + - SWEEP_ARC / 2f; + float endAngle = getArcCenter(to, getItemPos(to), count) + - SWEEP_ARC / 2f; + mSlice = new ValueAnimator(); + mSlice.setFloatValues(startAngle, endAngle); + // linear interpolater + mSlice.setInterpolator(null); + mSlice.setDuration(PIE_SLICE_DURATION); + mSlice.addListener(new AnimatorListener() { + @Override + public void onAnimationEnd(Animator arg0) { + mSlice = null; + } + + @Override + public void onAnimationRepeat(Animator arg0) { + } + + @Override + public void onAnimationStart(Animator arg0) { + } + + @Override + public void onAnimationCancel(Animator arg0) { + } + }); + mSlice.start(); + } + } + + private void openCurrentItem() { + if ((mCurrentItem != null) && mCurrentItem.hasItems()) { + mOpen.add(mCurrentItem); + layoutLabel(getLevel()); + mOpening = true; + if (mFadeIn != null) { + mFadeIn.cancel(); + } + mXFade = new ValueAnimator(); + mXFade.setFloatValues(1f, 0f); + mXFade.setDuration(PIE_XFADE_DURATION); + // Linear interpolation + mXFade.setInterpolator(null); + final PieItem ci = mCurrentItem; + mXFade.addListener(new AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + mXFade = null; + ci.setSelected(false); + mOpening = false; + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + + @Override + public void onAnimationCancel(Animator arg0) { + } + }); + mXFade.start(); + } + } + + /** + * @param polar x: angle, y: dist + * @return the item at angle/dist or null + */ + private PieItem findItem(PointF polar) { + // find the matching item: + List<PieItem> items = getOpenItem().getItems(); + final int count = items.size(); + int pos = 0; + for (PieItem item : items) { + if (inside(polar, item, pos, count)) { + return item; + } + pos++; + } + return null; + } + + + @Override + public boolean handlesTouch() { + return true; + } + + // focus specific code + + public void setBlockFocus(boolean blocked) { + mBlockFocus = blocked; + if (blocked) { + clear(); + } + } + + public void setFocus(int x, int y) { + mFocusX = x; + mFocusY = y; + setCircle(mFocusX, mFocusY); + } + + public void alignFocus(int x, int y) { + mOverlay.removeCallbacks(mDisappear); + mAnimation.cancel(); + mAnimation.reset(); + mFocusX = x; + mFocusY = y; + mDialAngle = DIAL_HORIZONTAL; + setCircle(x, y); + mFocused = false; + } + + public int getSize() { + return 2 * mCircleSize; + } + + private int getRandomRange() { + return (int)(-60 + 120 * Math.random()); + } + + private void setCircle(int cx, int cy) { + mCircle.set(cx - mCircleSize, cy - mCircleSize, + cx + mCircleSize, cy + mCircleSize); + mDial.set(cx - mCircleSize + mInnerOffset, cy - mCircleSize + mInnerOffset, + cx + mCircleSize - mInnerOffset, cy + mCircleSize - mInnerOffset); + } + + public void drawFocus(Canvas canvas) { + if (mBlockFocus) return; + mFocusPaint.setStrokeWidth(mOuterStroke); + canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint); + if (mState == STATE_PIE) return; + int color = mFocusPaint.getColor(); + if (mState == STATE_FINISHING) { + mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor); + } + mFocusPaint.setStrokeWidth(mInnerStroke); + drawLine(canvas, mDialAngle, mFocusPaint); + drawLine(canvas, mDialAngle + 45, mFocusPaint); + drawLine(canvas, mDialAngle + 180, mFocusPaint); + drawLine(canvas, mDialAngle + 225, mFocusPaint); + canvas.save(); + // rotate the arc instead of its offset to better use framework's shape caching + canvas.rotate(mDialAngle, mFocusX, mFocusY); + canvas.drawArc(mDial, 0, 45, false, mFocusPaint); + canvas.drawArc(mDial, 180, 45, false, mFocusPaint); + canvas.restore(); + mFocusPaint.setColor(color); + } + + private void drawLine(Canvas canvas, int angle, Paint p) { + convertCart(angle, mCircleSize - mInnerOffset, mPoint1); + convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2); + canvas.drawLine(mPoint1.x + mFocusX, mPoint1.y + mFocusY, + mPoint2.x + mFocusX, mPoint2.y + mFocusY, p); + } + + private static void convertCart(int angle, int radius, Point out) { + double a = 2 * Math.PI * (angle % 360) / 360; + out.x = (int) (radius * Math.cos(a) + 0.5); + out.y = (int) (radius * Math.sin(a) + 0.5); + } + + @Override + public void showStart() { + if (mState == STATE_PIE) return; + cancelFocus(); + mStartAnimationAngle = 67; + int range = getRandomRange(); + startAnimation(SCALING_UP_TIME, + false, mStartAnimationAngle, mStartAnimationAngle + range); + mState = STATE_FOCUSING; + } + + @Override + public void showSuccess(boolean timeout) { + if (mState == STATE_FOCUSING) { + startAnimation(SCALING_DOWN_TIME, + timeout, mStartAnimationAngle); + mState = STATE_FINISHING; + mFocused = true; + } + } + + @Override + public void showFail(boolean timeout) { + if (mState == STATE_FOCUSING) { + startAnimation(SCALING_DOWN_TIME, + timeout, mStartAnimationAngle); + mState = STATE_FINISHING; + mFocused = false; + } + } + + private void cancelFocus() { + mFocusCancelled = true; + mOverlay.removeCallbacks(mDisappear); + if (mAnimation != null && !mAnimation.hasEnded()) { + mAnimation.cancel(); + } + mFocusCancelled = false; + mFocused = false; + mState = STATE_IDLE; + } + + @Override + public void clear() { + if (mState == STATE_PIE) return; + cancelFocus(); + mOverlay.post(mDisappear); + } + + private void startAnimation(long duration, boolean timeout, + float toScale) { + startAnimation(duration, timeout, mDialAngle, + toScale); + } + + private void startAnimation(long duration, boolean timeout, + float fromScale, float toScale) { + setVisible(true); + mAnimation.reset(); + mAnimation.setDuration(duration); + mAnimation.setScale(fromScale, toScale); + mAnimation.setAnimationListener(timeout ? mEndAction : null); + mOverlay.startAnimation(mAnimation); + update(); + } + + private class EndAction implements Animation.AnimationListener { + @Override + public void onAnimationEnd(Animation animation) { + // Keep the focus indicator for some time. + if (!mFocusCancelled) { + mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT); + } + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationStart(Animation animation) { + } + } + + private class Disappear implements Runnable { + @Override + public void run() { + if (mState == STATE_PIE) return; + setVisible(false); + mFocusX = mCenterX; + mFocusY = mCenterY; + mState = STATE_IDLE; + setCircle(mFocusX, mFocusY); + mFocused = false; + } + } + + private class ScaleAnimation extends Animation { + private float mFrom = 1f; + private float mTo = 1f; + + public ScaleAnimation() { + setFillAfter(true); + } + + public void setScale(float from, float to) { + mFrom = from; + mTo = to; + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + mDialAngle = (int)(mFrom + (mTo - mFrom) * interpolatedTime); + } + } + +} diff --git a/src/com/android/camera/ui/PopupManager.java b/src/com/android/camera/ui/PopupManager.java new file mode 100644 index 000000000..0dcf34fd7 --- /dev/null +++ b/src/com/android/camera/ui/PopupManager.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.view.View; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * A manager which notifies the event of a new popup in order to dismiss the + * old popup if exists. + */ +public class PopupManager { + private static HashMap<Context, PopupManager> sMap = + new HashMap<Context, PopupManager>(); + + public interface OnOtherPopupShowedListener { + public void onOtherPopupShowed(); + } + + private PopupManager() {} + + private ArrayList<OnOtherPopupShowedListener> mListeners = new ArrayList<OnOtherPopupShowedListener>(); + + public void notifyShowPopup(View view) { + for (OnOtherPopupShowedListener listener : mListeners) { + if ((View) listener != view) { + listener.onOtherPopupShowed(); + } + } + } + + public void setOnOtherPopupShowedListener(OnOtherPopupShowedListener listener) { + mListeners.add(listener); + } + + public static PopupManager getInstance(Context context) { + PopupManager instance = sMap.get(context); + if (instance == null) { + instance = new PopupManager(); + sMap.put(context, instance); + } + return instance; + } + + public static void removeInstance(Context context) { + PopupManager instance = sMap.get(context); + sMap.remove(context); + } +} diff --git a/src/com/android/camera/ui/PreviewSurfaceView.java b/src/com/android/camera/ui/PreviewSurfaceView.java new file mode 100644 index 000000000..9a428e23c --- /dev/null +++ b/src/com/android/camera/ui/PreviewSurfaceView.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; + +import com.android.gallery3d.common.ApiHelper; + +public class PreviewSurfaceView extends SurfaceView { + public PreviewSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + setZOrderMediaOverlay(true); + getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } + + public void shrink() { + setLayoutSize(1); + } + + public void expand() { + setLayoutSize(ViewGroup.LayoutParams.MATCH_PARENT); + } + + private void setLayoutSize(int size) { + ViewGroup.LayoutParams p = getLayoutParams(); + if (p.width != size || p.height != size) { + p.width = size; + p.height = size; + setLayoutParams(p); + } + } +} diff --git a/src/com/android/camera/ui/RenderOverlay.java b/src/com/android/camera/ui/RenderOverlay.java new file mode 100644 index 000000000..d82ce18b6 --- /dev/null +++ b/src/com/android/camera/ui/RenderOverlay.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import com.android.camera.PreviewGestures; + +import java.util.ArrayList; +import java.util.List; + +public class RenderOverlay extends FrameLayout { + + private static final String TAG = "CAM_Overlay"; + + interface Renderer { + + public boolean handlesTouch(); + public boolean onTouchEvent(MotionEvent evt); + public void setOverlay(RenderOverlay overlay); + public void layout(int left, int top, int right, int bottom); + public void draw(Canvas canvas); + + } + + private RenderView mRenderView; + private List<Renderer> mClients; + private PreviewGestures mGestures; + // reverse list of touch clients + private List<Renderer> mTouchClients; + private int[] mPosition = new int[2]; + + public RenderOverlay(Context context, AttributeSet attrs) { + super(context, attrs); + mRenderView = new RenderView(context); + addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + mClients = new ArrayList<Renderer>(10); + mTouchClients = new ArrayList<Renderer>(10); + setWillNotDraw(false); + } + + public void setGestures(PreviewGestures gestures) { + mGestures = gestures; + } + + public void addRenderer(Renderer renderer) { + mClients.add(renderer); + renderer.setOverlay(this); + if (renderer.handlesTouch()) { + mTouchClients.add(0, renderer); + } + renderer.layout(getLeft(), getTop(), getRight(), getBottom()); + } + + public void addRenderer(int pos, Renderer renderer) { + mClients.add(pos, renderer); + renderer.setOverlay(this); + renderer.layout(getLeft(), getTop(), getRight(), getBottom()); + } + + public void remove(Renderer renderer) { + mClients.remove(renderer); + renderer.setOverlay(null); + } + + public int getClientSize() { + return mClients.size(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent m) { + if (mGestures != null) { + if (!mGestures.isEnabled()) return false; + mGestures.dispatchTouch(m); + } + return true; + } + + public boolean directDispatchTouch(MotionEvent m, Renderer target) { + mRenderView.setTouchTarget(target); + boolean res = mRenderView.dispatchTouchEvent(m); + mRenderView.setTouchTarget(null); + return res; + } + + private void adjustPosition() { + getLocationInWindow(mPosition); + } + + public int getWindowPositionX() { + return mPosition[0]; + } + + public int getWindowPositionY() { + return mPosition[1]; + } + + public void update() { + mRenderView.invalidate(); + } + + private class RenderView extends View { + + private Renderer mTouchTarget; + + public RenderView(Context context) { + super(context); + setWillNotDraw(false); + } + + public void setTouchTarget(Renderer target) { + mTouchTarget = target; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent evt) { + + if (mTouchTarget != null) { + return mTouchTarget.onTouchEvent(evt); + } + if (mTouchClients != null) { + boolean res = false; + for (Renderer client : mTouchClients) { + res |= client.onTouchEvent(evt); + } + return res; + } + return false; + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + adjustPosition(); + super.onLayout(changed, left, top, right, bottom); + if (mClients == null) return; + for (Renderer renderer : mClients) { + renderer.layout(left, top, right, bottom); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mClients == null) return; + boolean redraw = false; + for (Renderer renderer : mClients) { + renderer.draw(canvas); + redraw = redraw || ((OverlayRenderer) renderer).isVisible(); + } + if (redraw) { + invalidate(); + } + } + } + +} diff --git a/src/com/android/camera/ui/Rotatable.java b/src/com/android/camera/ui/Rotatable.java new file mode 100644 index 000000000..6d428b8c6 --- /dev/null +++ b/src/com/android/camera/ui/Rotatable.java @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2011 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; + +public interface Rotatable { + // Set parameter 'animation' to true to have animation when rotation. + public void setOrientation(int orientation, boolean animation); +} diff --git a/src/com/android/camera/ui/RotatableLayout.java b/src/com/android/camera/ui/RotatableLayout.java new file mode 100644 index 000000000..965d62a90 --- /dev/null +++ b/src/com/android/camera/ui/RotatableLayout.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.android.camera.Util; + +/* RotatableLayout rotates itself as well as all its children when orientation + * changes. Specifically, when going from portrait to landscape, camera + * controls move from the bottom of the screen to right side of the screen + * (i.e. counter clockwise). Similarly, when the screen changes to portrait, we + * need to move the controls from right side to the bottom of the screen, which + * is a clockwise rotation. + */ + +public class RotatableLayout extends FrameLayout { + + private static final String TAG = "RotatableLayout"; + // Initial orientation of the layout (ORIENTATION_PORTRAIT, or ORIENTATION_LANDSCAPE) + private int mInitialOrientation; + private int mPrevRotation; + private RotationListener mListener = null; + public interface RotationListener { + public void onRotation(int rotation); + } + public RotatableLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mInitialOrientation = getResources().getConfiguration().orientation; + } + + public RotatableLayout(Context context, AttributeSet attrs) { + super(context, attrs); + mInitialOrientation = getResources().getConfiguration().orientation; + } + + public RotatableLayout(Context context) { + super(context); + mInitialOrientation = getResources().getConfiguration().orientation; + } + + @Override + public void onAttachedToWindow() { + mPrevRotation = Util.getDisplayRotation((Activity) getContext()); + // check if there is any rotation before the view is attached to window + int currentOrientation = getResources().getConfiguration().orientation; + int orientation = getUnifiedRotation(); + if (mInitialOrientation == currentOrientation && orientation < 180) { + return; + } + + if (mInitialOrientation == Configuration.ORIENTATION_LANDSCAPE + && currentOrientation == Configuration.ORIENTATION_PORTRAIT) { + rotateLayout(true); + } else if (mInitialOrientation == Configuration.ORIENTATION_PORTRAIT + && currentOrientation == Configuration.ORIENTATION_LANDSCAPE) { + rotateLayout(false); + } + // In reverse landscape and reverse portrait, camera controls will be laid out + // on the wrong side of the screen. We need to make adjustment to move the controls + // to the USB side + if (orientation >= 180) { + flipChildren(); + } + } + + protected int getUnifiedRotation() { + // all the layout code assumes camera device orientation to be portrait + // adjust rotation for landscape + int orientation = getResources().getConfiguration().orientation; + int rotation = Util.getDisplayRotation((Activity) getContext()); + int camOrientation = (rotation % 180 == 0) ? Configuration.ORIENTATION_PORTRAIT + : Configuration.ORIENTATION_LANDSCAPE; + if (camOrientation != orientation) { + return (rotation + 90) % 360; + } + return rotation; + } + + public void checkLayoutFlip() { + int currentRotation = Util.getDisplayRotation((Activity) getContext()); + if ((currentRotation - mPrevRotation + 360) % 360 == 180) { + mPrevRotation = currentRotation; + flipChildren(); + getParent().requestLayout(); + } + } + + @Override + public void onWindowVisibilityChanged(int visibility) { + if (visibility == View.VISIBLE) { + // Make sure when coming back from onPause, the layout is rotated correctly + checkLayoutFlip(); + } + } + + @Override + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + int rotation = Util.getDisplayRotation((Activity) getContext()); + int diff = (rotation - mPrevRotation + 360) % 360; + if ( diff == 0) { + // No rotation + return; + } else if (diff == 180) { + // 180-degree rotation + mPrevRotation = rotation; + flipChildren(); + return; + } + // 90 or 270-degree rotation + boolean clockwise = isClockWiseRotation(mPrevRotation, rotation); + mPrevRotation = rotation; + rotateLayout(clockwise); + } + + protected void rotateLayout(boolean clockwise) { + // Change the size of the layout + ViewGroup.LayoutParams lp = getLayoutParams(); + int width = lp.width; + int height = lp.height; + lp.height = width; + lp.width = height; + setLayoutParams(lp); + + // rotate all the children + rotateChildren(clockwise); + } + + protected void rotateChildren(boolean clockwise) { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + rotate(child, clockwise); + } + if (mListener != null) mListener.onRotation(clockwise ? 90 : 270); + } + + protected void flipChildren() { + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + flip(child); + } + if (mListener != null) mListener.onRotation(180); + } + + public void setRotationListener(RotationListener listener) { + mListener = listener; + } + + public static boolean isClockWiseRotation(int prevRotation, int currentRotation) { + if (prevRotation == (currentRotation + 90) % 360) { + return true; + } + return false; + } + + public static void rotate(View view, boolean isClockwise) { + if (isClockwise) { + rotateClockwise(view); + } else { + rotateCounterClockwise(view); + } + } + + private static boolean contains(int value, int mask) { + return (value & mask) == mask; + } + + public static void rotateClockwise(View view) { + if (view == null) return; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int gravity = lp.gravity; + int ngravity = 0; + // rotate gravity + if (contains(gravity, Gravity.LEFT)) { + ngravity |= Gravity.TOP; + } + if (contains(gravity, Gravity.RIGHT)) { + ngravity |= Gravity.BOTTOM; + } + if (contains(gravity, Gravity.TOP)) { + ngravity |= Gravity.RIGHT; + } + if (contains(gravity, Gravity.BOTTOM)) { + ngravity |= Gravity.LEFT; + } + if (contains(gravity, Gravity.CENTER)) { + ngravity |= Gravity.CENTER; + } + if (contains(gravity, Gravity.CENTER_HORIZONTAL)) { + ngravity |= Gravity.CENTER_VERTICAL; + } + if (contains(gravity, Gravity.CENTER_VERTICAL)) { + ngravity |= Gravity.CENTER_HORIZONTAL; + } + lp.gravity = ngravity; + int ml = lp.leftMargin; + int mr = lp.rightMargin; + int mt = lp.topMargin; + int mb = lp.bottomMargin; + lp.leftMargin = mb; + lp.rightMargin = mt; + lp.topMargin = ml; + lp.bottomMargin = mr; + int width = lp.width; + int height = lp.height; + lp.width = height; + lp.height = width; + view.setLayoutParams(lp); + } + + public static void rotateCounterClockwise(View view) { + if (view == null) return; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int gravity = lp.gravity; + int ngravity = 0; + // change gravity + if (contains(gravity, Gravity.RIGHT)) { + ngravity |= Gravity.TOP; + } + if (contains(gravity, Gravity.LEFT)) { + ngravity |= Gravity.BOTTOM; + } + if (contains(gravity, Gravity.TOP)) { + ngravity |= Gravity.LEFT; + } + if (contains(gravity, Gravity.BOTTOM)) { + ngravity |= Gravity.RIGHT; + } + if (contains(gravity, Gravity.CENTER)) { + ngravity |= Gravity.CENTER; + } + if (contains(gravity, Gravity.CENTER_HORIZONTAL)) { + ngravity |= Gravity.CENTER_VERTICAL; + } + if (contains(gravity, Gravity.CENTER_VERTICAL)) { + ngravity |= Gravity.CENTER_HORIZONTAL; + } + lp.gravity = ngravity; + int ml = lp.leftMargin; + int mr = lp.rightMargin; + int mt = lp.topMargin; + int mb = lp.bottomMargin; + lp.leftMargin = mt; + lp.rightMargin = mb; + lp.topMargin = mr; + lp.bottomMargin = ml; + int width = lp.width; + int height = lp.height; + lp.width = height; + lp.height = width; + view.setLayoutParams(lp); + } + + // Rotate a given view 180 degrees + public static void flip(View view) { + rotateClockwise(view); + rotateClockwise(view); + } +}
\ No newline at end of file diff --git a/src/com/android/camera/ui/RotateImageView.java b/src/com/android/camera/ui/RotateImageView.java new file mode 100644 index 000000000..05e1a7c5b --- /dev/null +++ b/src/com/android/camera/ui/RotateImageView.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.media.ThumbnailUtils; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; + +/** + * A @{code ImageView} which can rotate it's content. + */ +public class RotateImageView extends TwoStateImageView implements Rotatable { + + @SuppressWarnings("unused") + private static final String TAG = "RotateImageView"; + + private static final int ANIMATION_SPEED = 270; // 270 deg/sec + + private int mCurrentDegree = 0; // [0, 359] + private int mStartDegree = 0; + private int mTargetDegree = 0; + + private boolean mClockwise = false, mEnableAnimation = true; + + private long mAnimationStartTime = 0; + private long mAnimationEndTime = 0; + + public RotateImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RotateImageView(Context context) { + super(context); + } + + protected int getDegree() { + return mTargetDegree; + } + + // Rotate the view counter-clockwise + @Override + public void setOrientation(int degree, boolean animation) { + mEnableAnimation = animation; + // make sure in the range of [0, 359] + degree = degree >= 0 ? degree % 360 : degree % 360 + 360; + if (degree == mTargetDegree) return; + + mTargetDegree = degree; + if (mEnableAnimation) { + mStartDegree = mCurrentDegree; + mAnimationStartTime = AnimationUtils.currentAnimationTimeMillis(); + + int diff = mTargetDegree - mCurrentDegree; + diff = diff >= 0 ? diff : 360 + diff; // make it in range [0, 359] + + // Make it in range [-179, 180]. That's the shorted distance between the + // two angles + diff = diff > 180 ? diff - 360 : diff; + + mClockwise = diff >= 0; + mAnimationEndTime = mAnimationStartTime + + Math.abs(diff) * 1000 / ANIMATION_SPEED; + } else { + mCurrentDegree = mTargetDegree; + } + + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + Drawable drawable = getDrawable(); + if (drawable == null) return; + + Rect bounds = drawable.getBounds(); + int w = bounds.right - bounds.left; + int h = bounds.bottom - bounds.top; + + if (w == 0 || h == 0) return; // nothing to draw + + if (mCurrentDegree != mTargetDegree) { + long time = AnimationUtils.currentAnimationTimeMillis(); + if (time < mAnimationEndTime) { + int deltaTime = (int)(time - mAnimationStartTime); + int degree = mStartDegree + ANIMATION_SPEED + * (mClockwise ? deltaTime : -deltaTime) / 1000; + degree = degree >= 0 ? degree % 360 : degree % 360 + 360; + mCurrentDegree = degree; + invalidate(); + } else { + mCurrentDegree = mTargetDegree; + } + } + + int left = getPaddingLeft(); + int top = getPaddingTop(); + int right = getPaddingRight(); + int bottom = getPaddingBottom(); + int width = getWidth() - left - right; + int height = getHeight() - top - bottom; + + int saveCount = canvas.getSaveCount(); + + // Scale down the image first if required. + if ((getScaleType() == ImageView.ScaleType.FIT_CENTER) && + ((width < w) || (height < h))) { + float ratio = Math.min((float) width / w, (float) height / h); + canvas.scale(ratio, ratio, width / 2.0f, height / 2.0f); + } + canvas.translate(left + width / 2, top + height / 2); + canvas.rotate(-mCurrentDegree); + canvas.translate(-w / 2, -h / 2); + drawable.draw(canvas); + canvas.restoreToCount(saveCount); + } + + private Bitmap mThumb; + private Drawable[] mThumbs; + private TransitionDrawable mThumbTransition; + + public void setBitmap(Bitmap bitmap) { + // Make sure uri and original are consistently both null or both + // non-null. + if (bitmap == null) { + mThumb = null; + mThumbs = null; + setImageDrawable(null); + setVisibility(GONE); + return; + } + + LayoutParams param = getLayoutParams(); + final int miniThumbWidth = param.width + - getPaddingLeft() - getPaddingRight(); + final int miniThumbHeight = param.height + - getPaddingTop() - getPaddingBottom(); + mThumb = ThumbnailUtils.extractThumbnail( + bitmap, miniThumbWidth, miniThumbHeight); + Drawable drawable; + if (mThumbs == null || !mEnableAnimation) { + mThumbs = new Drawable[2]; + mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb); + setImageDrawable(mThumbs[1]); + } else { + mThumbs[0] = mThumbs[1]; + mThumbs[1] = new BitmapDrawable(getContext().getResources(), mThumb); + mThumbTransition = new TransitionDrawable(mThumbs); + setImageDrawable(mThumbTransition); + mThumbTransition.startTransition(500); + } + setVisibility(VISIBLE); + } +} diff --git a/src/com/android/camera/ui/RotateLayout.java b/src/com/android/camera/ui/RotateLayout.java new file mode 100644 index 000000000..86f5c814d --- /dev/null +++ b/src/com/android/camera/ui/RotateLayout.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import com.android.gallery3d.common.ApiHelper; +import com.android.gallery3d.util.MotionEventHelper; + +// A RotateLayout is designed to display a single item and provides the +// capabilities to rotate the item. +public class RotateLayout extends ViewGroup implements Rotatable { + @SuppressWarnings("unused") + private static final String TAG = "RotateLayout"; + private int mOrientation; + private Matrix mMatrix = new Matrix(); + protected View mChild; + + public RotateLayout(Context context, AttributeSet attrs) { + super(context, attrs); + // The transparent background here is a workaround of the render issue + // happened when the view is rotated as the device's orientation + // changed. The view looks fine in landscape. After rotation, the view + // is invisible. + setBackgroundResource(android.R.color.transparent); + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + protected void onFinishInflate() { + mChild = getChildAt(0); + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + mChild.setPivotX(0); + mChild.setPivotY(0); + } + } + + @Override + protected void onLayout( + boolean change, int left, int top, int right, int bottom) { + int width = right - left; + int height = bottom - top; + switch (mOrientation) { + case 0: + case 180: + mChild.layout(0, 0, width, height); + break; + case 90: + case 270: + mChild.layout(0, 0, height, width); + break; + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + final int w = getMeasuredWidth(); + final int h = getMeasuredHeight(); + switch (mOrientation) { + case 0: + mMatrix.setTranslate(0, 0); + break; + case 90: + mMatrix.setTranslate(0, -h); + break; + case 180: + mMatrix.setTranslate(-w, -h); + break; + case 270: + mMatrix.setTranslate(-w, 0); + break; + } + mMatrix.postRotate(mOrientation); + event = MotionEventHelper.transformEvent(event, mMatrix); + } + return super.dispatchTouchEvent(event); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + super.dispatchDraw(canvas); + } else { + canvas.save(); + int w = getMeasuredWidth(); + int h = getMeasuredHeight(); + switch (mOrientation) { + case 0: + canvas.translate(0, 0); + break; + case 90: + canvas.translate(0, h); + break; + case 180: + canvas.translate(w, h); + break; + case 270: + canvas.translate(w, 0); + break; + } + canvas.rotate(-mOrientation, 0, 0); + super.dispatchDraw(canvas); + canvas.restore(); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int w = 0, h = 0; + switch(mOrientation) { + case 0: + case 180: + measureChild(mChild, widthSpec, heightSpec); + w = mChild.getMeasuredWidth(); + h = mChild.getMeasuredHeight(); + break; + case 90: + case 270: + measureChild(mChild, heightSpec, widthSpec); + w = mChild.getMeasuredHeight(); + h = mChild.getMeasuredWidth(); + break; + } + setMeasuredDimension(w, h); + + if (ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES) { + switch (mOrientation) { + case 0: + mChild.setTranslationX(0); + mChild.setTranslationY(0); + break; + case 90: + mChild.setTranslationX(0); + mChild.setTranslationY(h); + break; + case 180: + mChild.setTranslationX(w); + mChild.setTranslationY(h); + break; + case 270: + mChild.setTranslationX(w); + mChild.setTranslationY(0); + break; + } + mChild.setRotation(-mOrientation); + } + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + // Rotate the view counter-clockwise + @Override + public void setOrientation(int orientation, boolean animation) { + orientation = orientation % 360; + if (mOrientation == orientation) return; + mOrientation = orientation; + requestLayout(); + } + + public int getOrientation() { + return mOrientation; + } + + @Override + public ViewParent invalidateChildInParent(int[] location, Rect r) { + if (!ApiHelper.HAS_VIEW_TRANSFORM_PROPERTIES && mOrientation != 0) { + // The workaround invalidates the entire rotate layout. After + // rotation, the correct area to invalidate may be larger than the + // size of the child. Ex: ListView. There is no way to invalidate + // only the necessary area. + r.set(0, 0, getWidth(), getHeight()); + } + return super.invalidateChildInParent(location, r); + } +} diff --git a/src/com/android/camera/ui/RotateTextToast.java b/src/com/android/camera/ui/RotateTextToast.java new file mode 100644 index 000000000..c78a258b0 --- /dev/null +++ b/src/com/android/camera/ui/RotateTextToast.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.app.Activity; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.camera.Util; +import com.android.gallery3d.R; + +public class RotateTextToast { + private static final int TOAST_DURATION = 5000; // milliseconds + ViewGroup mLayoutRoot; + RotateLayout mToast; + Handler mHandler; + + public RotateTextToast(Activity activity, int textResourceId, int orientation) { + mLayoutRoot = (ViewGroup) activity.getWindow().getDecorView(); + LayoutInflater inflater = activity.getLayoutInflater(); + View v = inflater.inflate(R.layout.rotate_text_toast, mLayoutRoot); + mToast = (RotateLayout) v.findViewById(R.id.rotate_toast); + TextView tv = (TextView) mToast.findViewById(R.id.message); + tv.setText(textResourceId); + mToast.setOrientation(orientation, false); + mHandler = new Handler(); + } + + private final Runnable mRunnable = new Runnable() { + @Override + public void run() { + Util.fadeOut(mToast); + mLayoutRoot.removeView(mToast); + mToast = null; + } + }; + + public void show() { + mToast.setVisibility(View.VISIBLE); + mHandler.postDelayed(mRunnable, TOAST_DURATION); + } +} diff --git a/src/com/android/camera/ui/Switch.java b/src/com/android/camera/ui/Switch.java new file mode 100644 index 000000000..ac21758a7 --- /dev/null +++ b/src/com/android/camera/ui/Switch.java @@ -0,0 +1,505 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.CompoundButton; + +import com.android.gallery3d.R; +import com.android.gallery3d.common.ApiHelper; + +import java.util.Arrays; + +/** + * A Switch is a two-state toggle switch widget that can select between two + * options. The user may drag the "thumb" back and forth to choose the selected option, + * or simply tap to toggle as if it were a checkbox. + */ +public class Switch extends CompoundButton { + private static final int TOUCH_MODE_IDLE = 0; + private static final int TOUCH_MODE_DOWN = 1; + private static final int TOUCH_MODE_DRAGGING = 2; + + private Drawable mThumbDrawable; + private Drawable mTrackDrawable; + private int mThumbTextPadding; + private int mSwitchMinWidth; + private int mSwitchTextMaxWidth; + private int mSwitchPadding; + private CharSequence mTextOn; + private CharSequence mTextOff; + + private int mTouchMode; + private int mTouchSlop; + private float mTouchX; + private float mTouchY; + private VelocityTracker mVelocityTracker = VelocityTracker.obtain(); + private int mMinFlingVelocity; + + private float mThumbPosition; + private int mSwitchWidth; + private int mSwitchHeight; + private int mThumbWidth; // Does not include padding + + private int mSwitchLeft; + private int mSwitchTop; + private int mSwitchRight; + private int mSwitchBottom; + + private TextPaint mTextPaint; + private ColorStateList mTextColors; + private Layout mOnLayout; + private Layout mOffLayout; + + @SuppressWarnings("hiding") + private final Rect mTempRect = new Rect(); + + private static final int[] CHECKED_STATE_SET = { + android.R.attr.state_checked + }; + + /** + * Construct a new Switch with default styling, overriding specific style + * attributes as requested. + * + * @param context The Context that will determine this widget's theming. + * @param attrs Specification of attributes that should deviate from default styling. + */ + public Switch(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.switchStyle); + } + + /** + * Construct a new Switch with a default style determined by the given theme attribute, + * overriding specific style attributes as requested. + * + * @param context The Context that will determine this widget's theming. + * @param attrs Specification of attributes that should deviate from the default styling. + * @param defStyle An attribute ID within the active theme containing a reference to the + * default style for this widget. e.g. android.R.attr.switchStyle. + */ + public Switch(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + Resources res = getResources(); + DisplayMetrics dm = res.getDisplayMetrics(); + mTextPaint.density = dm.density; + mThumbDrawable = res.getDrawable(R.drawable.switch_inner_holo_dark); + mTrackDrawable = res.getDrawable(R.drawable.switch_track_holo_dark); + mTextOn = res.getString(R.string.capital_on); + mTextOff = res.getString(R.string.capital_off); + mThumbTextPadding = res.getDimensionPixelSize(R.dimen.thumb_text_padding); + 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); + + ViewConfiguration config = ViewConfiguration.get(context); + mTouchSlop = config.getScaledTouchSlop(); + mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); + + // Refresh display with current params + refreshDrawableState(); + setChecked(isChecked()); + } + + /** + * Sets the switch text color, size, style, hint color, and highlight color + * from the specified TextAppearance resource. + */ + public void setSwitchTextAppearance(Context context, int resid) { + Resources res = getResources(); + mTextColors = getTextColors(); + int ts = res.getDimensionPixelSize(R.dimen.thumb_text_size); + if (ts != mTextPaint.getTextSize()) { + mTextPaint.setTextSize(ts); + requestLayout(); + } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + if (mOnLayout == null) { + mOnLayout = makeLayout(mTextOn, mSwitchTextMaxWidth); + } + if (mOffLayout == null) { + mOffLayout = makeLayout(mTextOff, mSwitchTextMaxWidth); + } + + mTrackDrawable.getPadding(mTempRect); + final int maxTextWidth = Math.min(mSwitchTextMaxWidth, + Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())); + final int switchWidth = Math.max(mSwitchMinWidth, + maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right); + final int switchHeight = mTrackDrawable.getIntrinsicHeight(); + + mThumbWidth = maxTextWidth + mThumbTextPadding * 2; + + mSwitchWidth = switchWidth; + mSwitchHeight = switchHeight; + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int measuredHeight = getMeasuredHeight(); + final int measuredWidth = getMeasuredWidth(); + if (measuredHeight < switchHeight) { + setMeasuredDimension(measuredWidth, switchHeight); + } + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + CharSequence text = isChecked() ? mOnLayout.getText() : mOffLayout.getText(); + if (!TextUtils.isEmpty(text)) { + event.getText().add(text); + } + } + + private Layout makeLayout(CharSequence text, int maxWidth) { + int actual_width = (int) Math.ceil(Layout.getDesiredWidth(text, mTextPaint)); + StaticLayout l = new StaticLayout(text, 0, text.length(), mTextPaint, + actual_width, + Layout.Alignment.ALIGN_NORMAL, 1.f, 0, true, + TextUtils.TruncateAt.END, + (int) Math.min(actual_width, maxWidth)); + return l; + } + + /** + * @return true if (x, y) is within the target area of the switch thumb + */ + private boolean hitThumb(float x, float y) { + mThumbDrawable.getPadding(mTempRect); + final int thumbTop = mSwitchTop - mTouchSlop; + final int thumbLeft = mSwitchLeft + (int) (mThumbPosition + 0.5f) - mTouchSlop; + final int thumbRight = thumbLeft + mThumbWidth + + mTempRect.left + mTempRect.right + mTouchSlop; + final int thumbBottom = mSwitchBottom + mTouchSlop; + return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mVelocityTracker.addMovement(ev); + final int action = ev.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + if (isEnabled() && hitThumb(x, y)) { + mTouchMode = TOUCH_MODE_DOWN; + mTouchX = x; + mTouchY = y; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + switch (mTouchMode) { + case TOUCH_MODE_IDLE: + // Didn't target the thumb, treat normally. + break; + + case TOUCH_MODE_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + if (Math.abs(x - mTouchX) > mTouchSlop || + Math.abs(y - mTouchY) > mTouchSlop) { + mTouchMode = TOUCH_MODE_DRAGGING; + getParent().requestDisallowInterceptTouchEvent(true); + mTouchX = x; + mTouchY = y; + return true; + } + break; + } + + case TOUCH_MODE_DRAGGING: { + final float x = ev.getX(); + final float dx = x - mTouchX; + float newPos = Math.max(0, + Math.min(mThumbPosition + dx, getThumbScrollRange())); + if (newPos != mThumbPosition) { + mThumbPosition = newPos; + mTouchX = x; + invalidate(); + } + return true; + } + } + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + if (mTouchMode == TOUCH_MODE_DRAGGING) { + stopDrag(ev); + return true; + } + mTouchMode = TOUCH_MODE_IDLE; + mVelocityTracker.clear(); + break; + } + } + + return super.onTouchEvent(ev); + } + + private void cancelSuperTouch(MotionEvent ev) { + MotionEvent cancel = MotionEvent.obtain(ev); + cancel.setAction(MotionEvent.ACTION_CANCEL); + super.onTouchEvent(cancel); + cancel.recycle(); + } + + /** + * Called from onTouchEvent to end a drag operation. + * + * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL + */ + private void stopDrag(MotionEvent ev) { + mTouchMode = TOUCH_MODE_IDLE; + // Up and not canceled, also checks the switch has not been disabled during the drag + boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled(); + + cancelSuperTouch(ev); + + if (commitChange) { + boolean newState; + mVelocityTracker.computeCurrentVelocity(1000); + float xvel = mVelocityTracker.getXVelocity(); + if (Math.abs(xvel) > mMinFlingVelocity) { + newState = xvel > 0; + } else { + newState = getTargetCheckedState(); + } + animateThumbToCheckedState(newState); + } else { + animateThumbToCheckedState(isChecked()); + } + } + + private void animateThumbToCheckedState(boolean newCheckedState) { + setChecked(newCheckedState); + } + + private boolean getTargetCheckedState() { + return mThumbPosition >= getThumbScrollRange() / 2; + } + + private void setThumbPosition(boolean checked) { + mThumbPosition = checked ? getThumbScrollRange() : 0; + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + setThumbPosition(checked); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + setThumbPosition(isChecked()); + + int switchRight; + int switchLeft; + + switchRight = getWidth() - getPaddingRight(); + switchLeft = switchRight - mSwitchWidth; + + int switchTop = 0; + int switchBottom = 0; + switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) { + default: + case Gravity.TOP: + switchTop = getPaddingTop(); + switchBottom = switchTop + mSwitchHeight; + break; + + case Gravity.CENTER_VERTICAL: + switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 - + mSwitchHeight / 2; + switchBottom = switchTop + mSwitchHeight; + break; + + case Gravity.BOTTOM: + switchBottom = getHeight() - getPaddingBottom(); + switchTop = switchBottom - mSwitchHeight; + break; + } + + mSwitchLeft = switchLeft; + mSwitchTop = switchTop; + mSwitchBottom = switchBottom; + mSwitchRight = switchRight; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the switch + int switchLeft = mSwitchLeft; + int switchTop = mSwitchTop; + int switchRight = mSwitchRight; + int switchBottom = mSwitchBottom; + + mTrackDrawable.setBounds(switchLeft, switchTop, switchRight, switchBottom); + mTrackDrawable.draw(canvas); + + canvas.save(); + + mTrackDrawable.getPadding(mTempRect); + int switchInnerLeft = switchLeft + mTempRect.left; + int switchInnerTop = switchTop + mTempRect.top; + int switchInnerRight = switchRight - mTempRect.right; + int switchInnerBottom = switchBottom - mTempRect.bottom; + canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom); + + mThumbDrawable.getPadding(mTempRect); + final int thumbPos = (int) (mThumbPosition + 0.5f); + int thumbLeft = switchInnerLeft - mTempRect.left + thumbPos; + int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + mTempRect.right; + + mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); + mThumbDrawable.draw(canvas); + + // mTextColors should not be null, but just in case + if (mTextColors != null) { + mTextPaint.setColor(mTextColors.getColorForState(getDrawableState(), + mTextColors.getDefaultColor())); + } + mTextPaint.drawableState = getDrawableState(); + + Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; + + canvas.translate((thumbLeft + thumbRight) / 2 - switchText.getEllipsizedWidth() / 2, + (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2); + switchText.draw(canvas); + + canvas.restore(); + } + + @Override + public int getCompoundPaddingRight() { + int padding = super.getCompoundPaddingRight() + mSwitchWidth; + if (!TextUtils.isEmpty(getText())) { + padding += mSwitchPadding; + } + return padding; + } + + private int getThumbScrollRange() { + if (mTrackDrawable == null) { + return 0; + } + mTrackDrawable.getPadding(mTempRect); + return mSwitchWidth - mThumbWidth - mTempRect.left - mTempRect.right; + } + + @Override + protected int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + + if (isChecked()) { + mergeDrawableStates(drawableState, CHECKED_STATE_SET); + } + return drawableState; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + int[] myDrawableState = getDrawableState(); + + // Set the state of the Drawable + // Drawable may be null when checked state is set from XML, from super constructor + if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState); + if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState); + + invalidate(); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; + } + + @TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB) + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + mThumbDrawable.jumpToCurrentState(); + mTrackDrawable.jumpToCurrentState(); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + event.setClassName(Switch.class.getName()); + } + + @TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + info.setClassName(Switch.class.getName()); + CharSequence switchText = isChecked() ? mTextOn : mTextOff; + if (!TextUtils.isEmpty(switchText)) { + CharSequence oldText = info.getText(); + if (TextUtils.isEmpty(oldText)) { + info.setText(switchText); + } else { + StringBuilder newText = new StringBuilder(); + newText.append(oldText).append(' ').append(switchText); + info.setText(newText); + } + } + } +} diff --git a/src/com/android/camera/ui/TimeIntervalPopup.java b/src/com/android/camera/ui/TimeIntervalPopup.java new file mode 100644 index 000000000..18ad9f5da --- /dev/null +++ b/src/com/android/camera/ui/TimeIntervalPopup.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.camera.ui; + +import android.content.Context; +import android.content.res.Resources; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.NumberPicker; +import android.widget.Switch; +import android.widget.TextView; + +import com.android.camera.IconListPreference; +import com.android.camera.ListPreference; +import com.android.gallery3d.R; + +/** + * This is a popup window that allows users to turn on/off time lapse feature, + * and to select a time interval for taking a time lapse video. + */ +public class TimeIntervalPopup extends AbstractSettingPopup { + private static final String TAG = "TimeIntervalPopup"; + private NumberPicker mNumberSpinner; + private NumberPicker mUnitSpinner; + private Switch mTimeLapseSwitch; + private final String[] mUnits; + private final String[] mDurations; + private IconListPreference mPreference; + private Listener mListener; + private Button mConfirmButton; + private TextView mHelpText; + private View mTimePicker; + + static public interface Listener { + public void onListPrefChanged(ListPreference pref); + } + + public void setSettingChangedListener(Listener listener) { + mListener = listener; + } + + public TimeIntervalPopup(Context context, AttributeSet attrs) { + super(context, attrs); + + Resources res = context.getResources(); + mUnits = res.getStringArray(R.array.pref_video_time_lapse_frame_interval_units); + mDurations = res + .getStringArray(R.array.pref_video_time_lapse_frame_interval_duration_values); + } + + public void initialize(IconListPreference preference) { + mPreference = preference; + + // Set title. + mTitle.setText(mPreference.getTitle()); + + // Duration + int durationCount = mDurations.length; + mNumberSpinner = (NumberPicker) findViewById(R.id.duration); + mNumberSpinner.setMinValue(0); + mNumberSpinner.setMaxValue(durationCount - 1); + mNumberSpinner.setDisplayedValues(mDurations); + mNumberSpinner.setWrapSelectorWheel(false); + + // Units for duration (i.e. seconds, minutes, etc) + mUnitSpinner = (NumberPicker) findViewById(R.id.duration_unit); + mUnitSpinner.setMinValue(0); + mUnitSpinner.setMaxValue(mUnits.length - 1); + mUnitSpinner.setDisplayedValues(mUnits); + mUnitSpinner.setWrapSelectorWheel(false); + + mTimePicker = findViewById(R.id.time_interval_picker); + mTimeLapseSwitch = (Switch) findViewById(R.id.time_lapse_switch); + mHelpText = (TextView) findViewById(R.id.set_time_interval_help_text); + mConfirmButton = (Button) findViewById(R.id.time_lapse_interval_set_button); + + // Disable focus on the spinners to prevent keyboard from coming up + mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + mUnitSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + mTimeLapseSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + setTimeSelectionEnabled(isChecked); + } + }); + mConfirmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + updateInputState(); + } + }); + } + + private void restoreSetting() { + int index = mPreference.findIndexOfValue(mPreference.getValue()); + if (index == -1) { + Log.e(TAG, "Invalid preference value."); + mPreference.print(); + throw new IllegalArgumentException(); + } else if (index == 0) { + // default choice: time lapse off + mTimeLapseSwitch.setChecked(false); + setTimeSelectionEnabled(false); + } else { + mTimeLapseSwitch.setChecked(true); + setTimeSelectionEnabled(true); + int durationCount = mNumberSpinner.getMaxValue() + 1; + int unit = (index - 1) / durationCount; + int number = (index - 1) % durationCount; + mUnitSpinner.setValue(unit); + mNumberSpinner.setValue(number); + } + } + + @Override + public void setVisibility(int visibility) { + if (visibility == View.VISIBLE) { + if (getVisibility() != View.VISIBLE) { + // Set the number pickers and on/off switch to be consistent + // with the preference + restoreSetting(); + } + } + super.setVisibility(visibility); + } + + protected void setTimeSelectionEnabled(boolean enabled) { + mHelpText.setVisibility(enabled ? GONE : VISIBLE); + mTimePicker.setVisibility(enabled ? VISIBLE : GONE); + } + + @Override + public void reloadPreference() { + } + + private void updateInputState() { + if (mTimeLapseSwitch.isChecked()) { + int newId = mUnitSpinner.getValue() * (mNumberSpinner.getMaxValue() + 1) + + mNumberSpinner.getValue() + 1; + mPreference.setValueIndex(newId); + } else { + mPreference.setValueIndex(0); + } + + if (mListener != null) { + mListener.onListPrefChanged(mPreference); + } + } +} diff --git a/src/com/android/camera/ui/TwoStateImageView.java b/src/com/android/camera/ui/TwoStateImageView.java new file mode 100644 index 000000000..cd5b27fc1 --- /dev/null +++ b/src/com/android/camera/ui/TwoStateImageView.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; + +/** + * A @{code ImageView} which change the opacity of the icon if disabled. + */ +public class TwoStateImageView extends ImageView { + private static final int ENABLED_ALPHA = 255; + private static final int DISABLED_ALPHA = (int) (255 * 0.4); + private boolean mFilterEnabled = true; + + public TwoStateImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public TwoStateImageView(Context context) { + this(context, null); + } + + @SuppressWarnings("deprecation") + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (mFilterEnabled) { + if (enabled) { + setAlpha(ENABLED_ALPHA); + } else { + setAlpha(DISABLED_ALPHA); + } + } + } + + public void enableFilter(boolean enabled) { + mFilterEnabled = enabled; + } +} diff --git a/src/com/android/camera/ui/ZoomRenderer.java b/src/com/android/camera/ui/ZoomRenderer.java new file mode 100644 index 000000000..86b82b459 --- /dev/null +++ b/src/com/android/camera/ui/ZoomRenderer.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.camera.ui; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.ScaleGestureDetector; + +import com.android.gallery3d.R; + +public class ZoomRenderer extends OverlayRenderer + implements ScaleGestureDetector.OnScaleGestureListener { + + private static final String TAG = "CAM_Zoom"; + + private int mMaxZoom; + private int mMinZoom; + private OnZoomChangedListener mListener; + + private ScaleGestureDetector mDetector; + private Paint mPaint; + private Paint mTextPaint; + private int mCircleSize; + private int mCenterX; + private int mCenterY; + private float mMaxCircle; + private float mMinCircle; + private int mInnerStroke; + private int mOuterStroke; + private int mZoomSig; + private int mZoomFraction; + private Rect mTextBounds; + + public interface OnZoomChangedListener { + void onZoomStart(); + void onZoomEnd(); + void onZoomValueChanged(int index); // only for immediate zoom + } + + public ZoomRenderer(Context ctx) { + Resources res = ctx.getResources(); + mPaint = new Paint(); + mPaint.setAntiAlias(true); + mPaint.setColor(Color.WHITE); + mPaint.setStyle(Paint.Style.STROKE); + mTextPaint = new Paint(mPaint); + mTextPaint.setStyle(Paint.Style.FILL); + mTextPaint.setTextSize(res.getDimensionPixelSize(R.dimen.zoom_font_size)); + mTextPaint.setTextAlign(Paint.Align.LEFT); + mTextPaint.setAlpha(192); + mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke); + mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke); + mDetector = new ScaleGestureDetector(ctx, this); + mMinCircle = res.getDimensionPixelSize(R.dimen.zoom_ring_min); + mTextBounds = new Rect(); + setVisible(false); + } + + // set from module + public void setZoomMax(int zoomMaxIndex) { + mMaxZoom = zoomMaxIndex; + mMinZoom = 0; + } + + public void setZoom(int index) { + mCircleSize = (int) (mMinCircle + index * (mMaxCircle - mMinCircle) / (mMaxZoom - mMinZoom)); + } + + public void setZoomValue(int value) { + value = value / 10; + mZoomSig = value / 10; + mZoomFraction = value % 10; + } + + public void setOnZoomChangeListener(OnZoomChangedListener listener) { + mListener = listener; + } + + @Override + public void layout(int l, int t, int r, int b) { + super.layout(l, t, r, b); + mCenterX = (r - l) / 2; + mCenterY = (b - t) / 2; + mMaxCircle = Math.min(getWidth(), getHeight()); + mMaxCircle = (mMaxCircle - mMinCircle) / 2; + } + + public boolean isScaling() { + return mDetector.isInProgress(); + } + + @Override + public void onDraw(Canvas canvas) { + mPaint.setStrokeWidth(mInnerStroke); + canvas.drawCircle(mCenterX, mCenterY, mMinCircle, mPaint); + canvas.drawCircle(mCenterX, mCenterY, mMaxCircle, mPaint); + canvas.drawLine(mCenterX - mMinCircle, mCenterY, + mCenterX - mMaxCircle - 4, mCenterY, mPaint); + mPaint.setStrokeWidth(mOuterStroke); + canvas.drawCircle((float) mCenterX, (float) mCenterY, + (float) mCircleSize, mPaint); + String txt = mZoomSig+"."+mZoomFraction+"x"; + mTextPaint.getTextBounds(txt, 0, txt.length(), mTextBounds); + canvas.drawText(txt, mCenterX - mTextBounds.centerX(), mCenterY - mTextBounds.centerY(), + mTextPaint); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + final float sf = detector.getScaleFactor(); + float circle = (int) (mCircleSize * sf * sf); + circle = Math.max(mMinCircle, circle); + circle = Math.min(mMaxCircle, circle); + if (mListener != null && (int) circle != mCircleSize) { + mCircleSize = (int) circle; + int zoom = mMinZoom + (int) ((mCircleSize - mMinCircle) * (mMaxZoom - mMinZoom) / (mMaxCircle - mMinCircle)); + mListener.onZoomValueChanged(zoom); + } + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + setVisible(true); + if (mListener != null) { + mListener.onZoomStart(); + } + update(); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + setVisible(false); + if (mListener != null) { + mListener.onZoomEnd(); + } + } + +} |