diff options
author | Michael Kolb <kolby@google.com> | 2013-01-29 10:33:22 -0800 |
---|---|---|
committer | Michael Kolb <kolby@google.com> | 2013-01-29 10:51:20 -0800 |
commit | 8872c23e739de38d74f04a8c852ebb5199c905f6 (patch) | |
tree | 63e6ca8d492217f647ae87527e0039e5a0da2c97 /src/com/android/camera/ui | |
parent | c58d88b469fd345df9bdbff0c147d91caa9959b5 (diff) | |
download | android_packages_apps_Snap-8872c23e739de38d74f04a8c852ebb5199c905f6.tar.gz android_packages_apps_Snap-8872c23e739de38d74f04a8c852ebb5199c905f6.tar.bz2 android_packages_apps_Snap-8872c23e739de38d74f04a8c852ebb5199c905f6.zip |
Move Camera Java/Native source into Gallery2
Change-Id: I968efe4d656e88a7760d3c0044f65b4adac2ddd1
Diffstat (limited to 'src/com/android/camera/ui')
32 files changed, 4647 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..49df77b30 --- /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.camera.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/CameraSwitcher.java b/src/com/android/camera/ui/CameraSwitcher.java new file mode 100644 index 000000000..7b9fb6499 --- /dev/null +++ b/src/com/android/camera/ui/CameraSwitcher.java @@ -0,0 +1,293 @@ +/* + * 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.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.LinearLayout; + +import com.android.camera.R; +import com.android.gallery3d.common.ApiHelper; + +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 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); + } + + 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)) { + 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; + 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) { + 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_pan: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_panorama)); + break; + case R.drawable.ic_switch_photosphere: + item.setContentDescription(getContext().getResources().getString( + R.string.accessibility_switch_to_new_panorama)); + break; + default: + break; + } + content.addView(item, new LinearLayout.LayoutParams(mItemSize, mItemSize)); + } + } + + public boolean showsPopup() { + return mShowingPopup; + } + + public boolean isInsidePopup(MotionEvent evt) { + if (!showsPopup()) return false; + return evt.getX() >= mPopup.getLeft() + && evt.getX() < mPopup.getRight() + && evt.getY() >= mPopup.getTop() + && evt.getY() < mPopup.getBottom(); + } + + private void hidePopup() { + mShowingPopup = false; + setVisibility(View.VISIBLE); + if (mPopup != null && !animateHidePopup()) { + mPopup.setVisibility(View.INVISIBLE); + } + mParent.setOnTouchListener(null); + } + + private void showSwitcher() { + mShowingPopup = true; + if (mPopup == null) { + initPopup(); + } + 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 updateInitialTranslations() { + if (getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT) { + mTranslationX = -getWidth() / 2; + mTranslationY = getHeight(); + } else { + mTranslationX = getWidth(); + mTranslationY = getHeight() / 2; + } + } + private void popupAnimationSetup() { + if (!ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + return; + } + updateInitialTranslations(); + 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.setVisibility(View.INVISIBLE); + } + } + }; + } + 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); + } + } + }; + } + 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..ade25c33a --- /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.camera.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/EffectSettingPopup.java b/src/com/android/camera/ui/EffectSettingPopup.java new file mode 100644 index 000000000..628d8155a --- /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.camera.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..9e6f98245 --- /dev/null +++ b/src/com/android/camera/ui/FaceView.java @@ -0,0 +1,217 @@ +/* + * 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.CameraActivity; +import com.android.camera.CameraScreenNail; +import com.android.camera.R; +import com.android.camera.Util; +import com.android.gallery3d.common.ApiHelper; + +@TargetApi(ApiHelper.VERSION_CODES.ICE_CREAM_SANDWICH) +public class FaceView extends View implements FocusIndicator, Rotatable { + 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 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)); + } + + 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)) { + final CameraScreenNail sn = ((CameraActivity) getContext()).getCameraScreenNail(); + int rw = sn.getUncroppedRenderWidth(); + int rh = sn.getUncroppedRenderHeight(); + // 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/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..5d9cc388d --- /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.camera.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..4f88f2738 --- /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.camera.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..2fe89349a --- /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.camera.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..c0411c90d --- /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.camera.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..ab1babaab --- /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.camera.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..677e5acc8 --- /dev/null +++ b/src/com/android/camera/ui/PieItem.java @@ -0,0 +1,203 @@ +/* + * 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.BitmapDrawable; +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 float mCenter; + private float start; + private float sweep; + private float animate; + private int inner; + private int outer; + private boolean mSelected; + private boolean mEnabled; + private List<PieItem> mItems; + private Path mPath; + private OnClickListener mOnClickListener; + private float mAlpha; + + // 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; + setAlpha(1f); + mEnabled = true; + setAnimationAngle(getAnimationAngle()); + start = -1; + mCenter = -1; + } + + 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 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 setAnimationAngle(float a) { + animate = a; + } + + public float getAnimationAngle() { + return animate; + } + + 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 setGeometry(float st, float sw, int inside, int outside) { + start = st; + sweep = sw; + inner = inside; + outer = outside; + } + + public void setFixedSlice(float center, float sweep) { + mCenter = center; + this.sweep = sweep; + } + + public float getCenter() { + return mCenter; + } + + public float getStart() { + return start; + } + + public float getStartAngle() { + return start + animate; + } + + public float getSweep() { + return sweep; + } + + public int getInnerRadius() { + return inner; + } + + public int getOuterRadius() { + return outer; + } + + 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/PieRenderer.java b/src/com/android/camera/ui/PieRenderer.java new file mode 100644 index 000000000..b592508e1 --- /dev/null +++ b/src/com/android/camera/ui/PieRenderer.java @@ -0,0 +1,825 @@ +/* + * 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.AnimatorListenerAdapter; +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.view.MotionEvent; +import android.view.ViewConfiguration; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.LinearInterpolator; +import android.view.animation.Transformation; + +import com.android.camera.R; +import com.android.gallery3d.common.ApiHelper; + +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 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; + + 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 int MSG_OPEN = 0; + private static final int MSG_CLOSE = 1; + private static final float PIE_SWEEP = (float)(Math.PI * 2 / 3); + // geometry + private Point mCenter; + 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> mItems; + + private PieItem mOpenItem; + + private Paint mSelectedPaint; + private Paint mSubPaint; + + // 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 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 LinearAnimation mXFade; + private LinearAnimation mFadeIn; + private volatile boolean mFocusCancelled; + + private Handler mHandler = new Handler() { + public void handleMessage(Message msg) { + switch(msg.what) { + case MSG_OPEN: + if (mListener != null) { + mListener.onPieOpened(mCenter.x, mCenter.y); + } + break; + case MSG_CLOSE: + if (mListener != null) { + mListener.onPieClosed(); + } + 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); + mItems = new ArrayList<PieItem>(); + Resources res = ctx.getResources(); + mRadius = (int) res.getDimensionPixelSize(R.dimen.pie_radius_start); + mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset); + mRadiusInc = (int) res.getDimensionPixelSize(R.dimen.pie_radius_increment); + mTouchOffset = (int) res.getDimensionPixelSize(R.dimen.pie_touch_offset); + mCenter = new Point(0,0); + 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(); + } + + public boolean showsItems() { + return mTapMode; + } + + public void addItem(PieItem item) { + // add the item to the pie itself + mItems.add(item); + } + + public void removeItem(PieItem item) { + mItems.remove(item); + } + + public void clearItems() { + mItems.clear(); + } + + public void showInCenter() { + if ((mState == STATE_PIE) && isVisible()) { + mTapMode = false; + show(false); + } else { + if (mState != STATE_IDLE) { + cancelFocus(); + } + mState = STATE_PIE; + setCenter(mCenterX, mCenterY); + mTapMode = true; + show(true); + } + } + + public void hide() { + show(false); + } + + /** + * guaranteed has center set + * @param show + */ + private void show(boolean show) { + if (show) { + mState = STATE_PIE; + // ensure clean state + mCurrentItem = null; + mOpenItem = null; + for (PieItem item : mItems) { + item.setSelected(false); + } + layoutPie(); + fadeIn(); + } else { + mState = STATE_IDLE; + mTapMode = false; + if (mXFade != null) { + mXFade.cancel(); + } + } + setVisible(show); + mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE); + } + + private void fadeIn() { + mFadeIn = new LinearAnimation(0, 1); + mFadeIn.setDuration(PIE_FADE_IN_DURATION); + mFadeIn.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mFadeIn = null; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + mFadeIn.startNow(); + mOverlay.startAnimation(mFadeIn); + } + + public void setCenter(int x, int y) { + mCenter.x = x; + mCenter.y = y; + // when using the pie menu, align the focus ring + alignFocus(x, y); + } + + private void layoutPie() { + int rgap = 2; + int inner = mRadius + rgap; + int outer = mRadius + mRadiusInc - rgap; + int gap = 1; + layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap); + } + + private void layoutItems(List<PieItem> items, float centerAngle, int inner, + int outer, int gap) { + float emptyangle = PIE_SWEEP / 16; + float sweep = (float) (PIE_SWEEP - 2 * emptyangle) / items.size(); + float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2; + // check if we have custom geometry + // first item we find triggers custom sweep for all + // this allows us to re-use the path + for (PieItem item : items) { + if (item.getCenter() >= 0) { + sweep = item.getSweep(); + break; + } + } + Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, + outer, inner, mCenter); + for (PieItem item : items) { + // shared between items + item.setPath(path); + if (item.getCenter() >= 0) { + angle = item.getCenter(); + } + int w = item.getIntrinsicWidth(); + int h = item.getIntrinsicHeight(); + // move views to outer border + int r = inner + (outer - inner) * 2 / 3; + int x = (int) (r * Math.cos(angle)); + int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2; + x = mCenter.x + x - w / 2; + item.setBounds(x, y, x + w, y + h); + float itemstart = angle - sweep / 2; + item.setGeometry(itemstart, sweep, inner, outer); + if (item.hasItems()) { + layoutItems(item.getItems(), angle, inner, + outer + mRadiusInc / 2, gap); + } + angle += sweep; + } + } + + private Path makeSlice(float start, float end, int outer, int inner, Point center) { + RectF bb = + new RectF(center.x - outer, center.y - outer, center.x + outer, + center.y + outer); + RectF bbi = + new RectF(center.x - inner, center.y - inner, center.x + inner, + center.y + inner); + Path path = new Path(); + path.arcTo(bb, start, end - start, true); + path.arcTo(bbi, end, start - end); + path.close(); + return path; + } + + /** + * 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() { + if (ApiHelper.HAS_VIEW_PROPERTY_ANIMATOR) { + mOverlay.animate().alpha(0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + deselect(); + show(false); + mOverlay.setAlpha(1); + super.onAnimationEnd(animation); + } + }).setDuration(PIE_SELECT_FADE_DURATION); + } else { + deselect(); + show(false); + } + } + + @Override + public void onDraw(Canvas canvas) { + float alpha = 1; + if (mXFade != null) { + alpha = mXFade.getValue(); + } else if (mFadeIn != null) { + alpha = mFadeIn.getValue(); + } + int state = canvas.save(); + if (mFadeIn != null) { + float sf = 0.9f + alpha * 0.1f; + canvas.scale(sf, sf, mCenter.x, mCenter.y); + } + drawFocus(canvas); + if (mState == STATE_FINISHING) { + canvas.restoreToCount(state); + return; + } + if ((mOpenItem == null) || (mXFade != null)) { + // draw base menu + for (PieItem item : mItems) { + drawItem(canvas, item, alpha); + } + } + if (mOpenItem != null) { + for (PieItem inner : mOpenItem.getItems()) { + drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1); + } + } + canvas.restoreToCount(state); + } + + private void drawItem(Canvas canvas, PieItem item, float alpha) { + if (mState == STATE_PIE) { + if (item.getPath() != null) { + if (item.isSelected()) { + Paint p = mSelectedPaint; + int state = canvas.save(); + float r = getDegrees(item.getStartAngle()); + canvas.rotate(r, mCenter.x, mCenter.y); + canvas.drawPath(item.getPath(), p); + canvas.restoreToCount(state); + } + 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(); + PointF polar = getPolar(x, y, !(mTapMode)); + if (MotionEvent.ACTION_DOWN == action) { + mDown.x = (int) evt.getX(); + mDown.y = (int) evt.getY(); + mOpening = false; + if (mTapMode) { + PieItem item = findItem(polar); + 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(polar); + if (item != null && mOpening) { + mOpening = false; + return true; + } + } + if (item == null) { + mTapMode = false; + show(false); + } else if (!mOpening + && !item.hasItems()) { + item.performClick(); + startFadeOut(); + mTapMode = false; + } + return true; + } + } else if (MotionEvent.ACTION_CANCEL == action) { + if (isVisible() || mTapMode) { + show(false); + } + deselect(); + return false; + } else if (MotionEvent.ACTION_MOVE == action) { + if (polar.y < mRadius) { + if (mOpenItem != null) { + mOpenItem = null; + } else { + deselect(); + } + return false; + } + PieItem item = findItem(polar); + boolean moved = hasMoved(evt); + if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) { + // only select if we didn't just open or have moved past slop + mOpening = false; + if (moved) { + // switch back to swipe mode + mTapMode = false; + } + onEnter(item); + } + } + return false; + } + + private boolean hasMoved(MotionEvent e) { + return mTouchSlopSquared < (e.getX() - mDown.x) * (e.getX() - mDown.x) + + (e.getY() - mDown.y) * (e.getY() - mDown.y); + } + + /** + * 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; + if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) { + openCurrentItem(); + } + } else { + mCurrentItem = null; + } + } + + private void deselect() { + if (mCurrentItem != null) { + mCurrentItem.setSelected(false); + } + if (mOpenItem != null) { + mOpenItem = null; + } + mCurrentItem = null; + } + + private void openCurrentItem() { + if ((mCurrentItem != null) && mCurrentItem.hasItems()) { + mCurrentItem.setSelected(false); + mOpenItem = mCurrentItem; + mOpening = true; + mXFade = new LinearAnimation(1, 0); + mXFade.setDuration(PIE_XFADE_DURATION); + mXFade.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + mXFade = null; + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + mXFade.startNow(); + mOverlay.startAnimation(mXFade); + } + } + + private PointF getPolar(float x, float y, boolean useOffset) { + PointF res = new PointF(); + // get angle and radius from x/y + res.x = (float) Math.PI / 2; + x = x - mCenter.x; + y = mCenter.y - y; + res.y = (float) Math.sqrt(x * x + y * y); + if (x != 0) { + res.x = (float) Math.atan2(y, x); + if (res.x < 0) { + res.x = (float) (2 * Math.PI + res.x); + } + } + res.y = res.y + (useOffset ? mTouchOffset : 0); + return res; + } + + /** + * @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 = (mOpenItem != null) ? mOpenItem.getItems() : mItems; + for (PieItem item : items) { + if (inside(polar, item)) { + return item; + } + } + return null; + } + + private boolean inside(PointF polar, PieItem item) { + return (item.getInnerRadius() < polar.y) + && (item.getStartAngle() < polar.x) + && (item.getStartAngle() + item.getSweep() > polar.x) + && (!mTapMode || (item.getOuterRadius() > polar.y)); + } + + @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()); + } + + @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; + setCircle(mFocusX, mFocusY); + if (isVisible() && mState == STATE_PIE) { + setCenter(mCenterX, mCenterY); + layoutPie(); + } + } + + 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.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); + } + } + + + private class LinearAnimation extends Animation { + private float mFrom; + private float mTo; + private float mValue; + + public LinearAnimation(float from, float to) { + setFillAfter(true); + setInterpolator(new LinearInterpolator()); + mFrom = from; + mTo = to; + } + + public float getValue() { + return mValue; + } + + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + mValue = (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..ba2591511 --- /dev/null +++ b/src/com/android/camera/ui/RenderOverlay.java @@ -0,0 +1,165 @@ +/* + * 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 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; + + // 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 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) { + return false; + } + + public boolean directDispatchTouch(MotionEvent m, Renderer target) { + mRenderView.setTouchTarget(target); + boolean res = super.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 onTouchEvent(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/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..f73c03362 --- /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.R; +import com.android.camera.Util; + +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..5b1ab4c97 --- /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.camera.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..b79663be2 --- /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.camera.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/TimerSettingPopup.java b/src/com/android/camera/ui/TimerSettingPopup.java new file mode 100644 index 000000000..06d7e4e50 --- /dev/null +++ b/src/com/android/camera/ui/TimerSettingPopup.java @@ -0,0 +1,153 @@ +/* + * 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.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.ListPreference; +import com.android.camera.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 TimerSettingPopup extends AbstractSettingPopup { + private static final String TAG = "TimerSettingPopup"; + private NumberPicker mNumberSpinner; + private Switch mTimerSwitch; + private String[] mDurations; + private ListPreference 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 TimerSettingPopup(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void initialize(ListPreference preference) { + mPreference = preference; + + // Set title. + mTitle.setText(mPreference.getTitle()); + + // Duration + CharSequence[] entries = mPreference.getEntryValues(); + mDurations = new String[entries.length - 1]; + Locale locale = getResources().getConfiguration().locale; + for (int i = 1; i < entries.length; i++) + mDurations[i-1] = 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); + + mTimerSwitch = (Switch) findViewById(R.id.timer_setting_switch); + mHelpText = (TextView) findViewById(R.id.set_timer_help_text); + mConfirmButton = (Button) findViewById(R.id.timer_set_button); + mTimePicker = findViewById(R.id.time_duration_picker); + + // Disable focus on the spinners to prevent keyboard from coming up + mNumberSpinner.setDescendantFocusability(NumberPicker.FOCUS_BLOCK_DESCENDANTS); + + mTimerSwitch.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 + mTimerSwitch.setChecked(false); + setTimeSelectionEnabled(false); + } else { + mTimerSwitch.setChecked(true); + setTimeSelectionEnabled(true); + mNumberSpinner.setValue(index - 1); + } + } + + @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 (mTimerSwitch.isChecked()) { + int newId = 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..10c5e80d4 --- /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.camera.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(); + } + } + +} |