Initial checkin of AOSP Messaging app.

b/23110861

Change-Id: I11db999bd10656801e618f78ab2b2ef74136fff1
This commit is contained in:
Mike Dodd
2015-08-12 12:47:26 -07:00
parent ef8c7abbcf
commit d3b009ae55
1645 changed files with 186271 additions and 0 deletions
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.mediapicker.camerafocus;
public interface FocusIndicator {
public void showStart();
public void showSuccess(boolean timeout);
public void showFail(boolean timeout);
public void clear();
}
@@ -0,0 +1,589 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.mediapicker.camerafocus;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.Camera.Area;
import android.hardware.Camera.Parameters;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import java.util.ArrayList;
import java.util.List;
/* A class that handles everything about focus in still picture mode.
* This also handles the metering area because it is the same as focus area.
*
* The test cases:
* (1) The camera has continuous autofocus. Move the camera. Take a picture when
* CAF is not in progress.
* (2) The camera has continuous autofocus. Move the camera. Take a picture when
* CAF is in progress.
* (3) The camera has face detection. Point the camera at some faces. Hold the
* shutter. Release to take a picture.
* (4) The camera has face detection. Point the camera at some faces. Single tap
* the shutter to take a picture.
* (5) The camera has autofocus. Single tap the shutter to take a picture.
* (6) The camera has autofocus. Hold the shutter. Release to take a picture.
* (7) The camera has no autofocus. Single tap the shutter and take a picture.
* (8) The camera has autofocus and supports focus area. Touch the screen to
* trigger autofocus. Take a picture.
* (9) The camera has autofocus and supports focus area. Touch the screen to
* trigger autofocus. Wait until it times out.
* (10) The camera has no autofocus and supports metering area. Touch the screen
* to change metering area.
*/
public class FocusOverlayManager {
private static final String TAG = LogUtil.BUGLE_TAG;
private static final String TRUE = "true";
private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported";
private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED =
"auto-whitebalance-lock-supported";
private static final int RESET_TOUCH_FOCUS = 0;
private static final int RESET_TOUCH_FOCUS_DELAY = 3000;
private int mState = STATE_IDLE;
private static final int STATE_IDLE = 0; // Focus is not active.
private static final int STATE_FOCUSING = 1; // Focus is in progress.
// Focus is in progress and the camera should take a picture after focus finishes.
private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2;
private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds.
private static final int STATE_FAIL = 4; // Focus finishes and fails.
private boolean mInitialized;
private boolean mFocusAreaSupported;
private boolean mMeteringAreaSupported;
private boolean mLockAeAwbNeeded;
private boolean mAeAwbLock;
private Matrix mMatrix;
private PieRenderer mPieRenderer;
private int mPreviewWidth; // The width of the preview frame layout.
private int mPreviewHeight; // The height of the preview frame layout.
private boolean mMirror; // true if the camera is front-facing.
private int mDisplayOrientation;
private List<Object> mFocusArea; // focus area in driver format
private List<Object> mMeteringArea; // metering area in driver format
private String mFocusMode;
private String mOverrideFocusMode;
private Parameters mParameters;
private Handler mHandler;
Listener mListener;
public interface Listener {
public void autoFocus();
public void cancelAutoFocus();
public boolean capture();
public void setFocusParameters();
}
private class MainHandler extends Handler {
public MainHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case RESET_TOUCH_FOCUS: {
cancelAutoFocus();
break;
}
}
}
}
public FocusOverlayManager(Listener listener, Looper looper) {
mHandler = new MainHandler(looper);
mMatrix = new Matrix();
mListener = listener;
}
public void setFocusRenderer(PieRenderer renderer) {
mPieRenderer = renderer;
mInitialized = (mMatrix != null);
}
public void setParameters(Parameters parameters) {
// parameters can only be null when onConfigurationChanged is called
// before camera is open. We will just return in this case, because
// parameters will be set again later with the right parameters after
// camera is open.
if (parameters == null) {
return;
}
mParameters = parameters;
mFocusAreaSupported = isFocusAreaSupported(parameters);
mMeteringAreaSupported = isMeteringAreaSupported(parameters);
mLockAeAwbNeeded = (isAutoExposureLockSupported(mParameters) ||
isAutoWhiteBalanceLockSupported(mParameters));
}
public void setPreviewSize(int previewWidth, int previewHeight) {
if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) {
mPreviewWidth = previewWidth;
mPreviewHeight = previewHeight;
setMatrix();
}
}
public void setMirror(boolean mirror) {
mMirror = mirror;
setMatrix();
}
public void setDisplayOrientation(int displayOrientation) {
mDisplayOrientation = displayOrientation;
setMatrix();
}
private void setMatrix() {
if (mPreviewWidth != 0 && mPreviewHeight != 0) {
Matrix matrix = new Matrix();
prepareMatrix(matrix, mMirror, mDisplayOrientation,
mPreviewWidth, mPreviewHeight);
// In face detection, the matrix converts the driver coordinates to UI
// coordinates. In tap focus, the inverted matrix converts the UI
// coordinates to driver coordinates.
matrix.invert(mMatrix);
mInitialized = (mPieRenderer != null);
}
}
private void lockAeAwbIfNeeded() {
if (mLockAeAwbNeeded && !mAeAwbLock) {
mAeAwbLock = true;
mListener.setFocusParameters();
}
}
private void unlockAeAwbIfNeeded() {
if (mLockAeAwbNeeded && mAeAwbLock && (mState != STATE_FOCUSING_SNAP_ON_FINISH)) {
mAeAwbLock = false;
mListener.setFocusParameters();
}
}
public void onShutterDown() {
if (!mInitialized) {
return;
}
boolean autoFocusCalled = false;
if (needAutoFocusCall()) {
// Do not focus if touch focus has been triggered.
if (mState != STATE_SUCCESS && mState != STATE_FAIL) {
autoFocus();
autoFocusCalled = true;
}
}
if (!autoFocusCalled) {
lockAeAwbIfNeeded();
}
}
public void onShutterUp() {
if (!mInitialized) {
return;
}
if (needAutoFocusCall()) {
// User releases half-pressed focus key.
if (mState == STATE_FOCUSING || mState == STATE_SUCCESS
|| mState == STATE_FAIL) {
cancelAutoFocus();
}
}
// Unlock AE and AWB after cancelAutoFocus. Camera API does not
// guarantee setParameters can be called during autofocus.
unlockAeAwbIfNeeded();
}
public void doSnap() {
if (!mInitialized) {
return;
}
// If the user has half-pressed the shutter and focus is completed, we
// can take the photo right away. If the focus mode is infinity, we can
// also take the photo.
if (!needAutoFocusCall() || (mState == STATE_SUCCESS || mState == STATE_FAIL)) {
capture();
} else if (mState == STATE_FOCUSING) {
// Half pressing the shutter (i.e. the focus button event) will
// already have requested AF for us, so just request capture on
// focus here.
mState = STATE_FOCUSING_SNAP_ON_FINISH;
} else if (mState == STATE_IDLE) {
// We didn't do focus. This can happen if the user press focus key
// while the snapshot is still in progress. The user probably wants
// the next snapshot as soon as possible, so we just do a snapshot
// without focusing again.
capture();
}
}
public void onAutoFocus(boolean focused, boolean shutterButtonPressed) {
if (mState == STATE_FOCUSING_SNAP_ON_FINISH) {
// Take the picture no matter focus succeeds or fails. No need
// to play the AF sound if we're about to play the shutter
// sound.
if (focused) {
mState = STATE_SUCCESS;
} else {
mState = STATE_FAIL;
}
updateFocusUI();
capture();
} else if (mState == STATE_FOCUSING) {
// This happens when (1) user is half-pressing the focus key or
// (2) touch focus is triggered. Play the focus tone. Do not
// take the picture now.
if (focused) {
mState = STATE_SUCCESS;
} else {
mState = STATE_FAIL;
}
updateFocusUI();
// If this is triggered by touch focus, cancel focus after a
// while.
if (mFocusArea != null) {
mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
}
if (shutterButtonPressed) {
// Lock AE & AWB so users can half-press shutter and recompose.
lockAeAwbIfNeeded();
}
} else if (mState == STATE_IDLE) {
// User has released the focus key before focus completes.
// Do nothing.
}
}
public void onAutoFocusMoving(boolean moving) {
if (!mInitialized) {
return;
}
// Ignore if we have requested autofocus. This method only handles
// continuous autofocus.
if (mState != STATE_IDLE) {
return;
}
if (moving) {
mPieRenderer.showStart();
} else {
mPieRenderer.showSuccess(true);
}
}
private void initializeFocusAreas(int focusWidth, int focusHeight,
int x, int y, int previewWidth, int previewHeight) {
if (mFocusArea == null) {
mFocusArea = new ArrayList<Object>();
mFocusArea.add(new Area(new Rect(), 1));
}
// Convert the coordinates to driver format.
calculateTapArea(focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight,
((Area) mFocusArea.get(0)).rect);
}
private void initializeMeteringAreas(int focusWidth, int focusHeight,
int x, int y, int previewWidth, int previewHeight) {
if (mMeteringArea == null) {
mMeteringArea = new ArrayList<Object>();
mMeteringArea.add(new Area(new Rect(), 1));
}
// Convert the coordinates to driver format.
// AE area is bigger because exposure is sensitive and
// easy to over- or underexposure if area is too small.
calculateTapArea(focusWidth, focusHeight, 1.5f, x, y, previewWidth, previewHeight,
((Area) mMeteringArea.get(0)).rect);
}
public void onSingleTapUp(int x, int y) {
if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
return;
}
// Let users be able to cancel previous touch focus.
if ((mFocusArea != null) && (mState == STATE_FOCUSING ||
mState == STATE_SUCCESS || mState == STATE_FAIL)) {
cancelAutoFocus();
}
// Initialize variables.
int focusWidth = mPieRenderer.getSize();
int focusHeight = mPieRenderer.getSize();
if (focusWidth == 0 || mPieRenderer.getWidth() == 0 || mPieRenderer.getHeight() == 0) {
return;
}
int previewWidth = mPreviewWidth;
int previewHeight = mPreviewHeight;
// Initialize mFocusArea.
if (mFocusAreaSupported) {
initializeFocusAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
}
// Initialize mMeteringArea.
if (mMeteringAreaSupported) {
initializeMeteringAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
}
// Use margin to set the focus indicator to the touched area.
mPieRenderer.setFocus(x, y);
// Set the focus area and metering area.
mListener.setFocusParameters();
if (mFocusAreaSupported) {
autoFocus();
} else { // Just show the indicator in all other cases.
updateFocusUI();
// Reset the metering area in 3 seconds.
mHandler.removeMessages(RESET_TOUCH_FOCUS);
mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
}
}
public void onPreviewStarted() {
mState = STATE_IDLE;
}
public void onPreviewStopped() {
// If auto focus was in progress, it would have been stopped.
mState = STATE_IDLE;
resetTouchFocus();
updateFocusUI();
}
public void onCameraReleased() {
onPreviewStopped();
}
private void autoFocus() {
LogUtil.v(TAG, "Start autofocus.");
mListener.autoFocus();
mState = STATE_FOCUSING;
updateFocusUI();
mHandler.removeMessages(RESET_TOUCH_FOCUS);
}
private void cancelAutoFocus() {
LogUtil.v(TAG, "Cancel autofocus.");
// Reset the tap area before calling mListener.cancelAutofocus.
// Otherwise, focus mode stays at auto and the tap area passed to the
// driver is not reset.
resetTouchFocus();
mListener.cancelAutoFocus();
mState = STATE_IDLE;
updateFocusUI();
mHandler.removeMessages(RESET_TOUCH_FOCUS);
}
private void capture() {
if (mListener.capture()) {
mState = STATE_IDLE;
mHandler.removeMessages(RESET_TOUCH_FOCUS);
}
}
public String getFocusMode() {
if (mOverrideFocusMode != null) {
return mOverrideFocusMode;
}
List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
if (mFocusAreaSupported && mFocusArea != null) {
// Always use autofocus in tap-to-focus.
mFocusMode = Parameters.FOCUS_MODE_AUTO;
} else {
mFocusMode = Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
}
if (!isSupported(mFocusMode, supportedFocusModes)) {
// For some reasons, the driver does not support the current
// focus mode. Fall back to auto.
if (isSupported(Parameters.FOCUS_MODE_AUTO,
mParameters.getSupportedFocusModes())) {
mFocusMode = Parameters.FOCUS_MODE_AUTO;
} else {
mFocusMode = mParameters.getFocusMode();
}
}
return mFocusMode;
}
public List getFocusAreas() {
return mFocusArea;
}
public List getMeteringAreas() {
return mMeteringArea;
}
public void updateFocusUI() {
if (!mInitialized) {
return;
}
FocusIndicator focusIndicator = mPieRenderer;
if (mState == STATE_IDLE) {
if (mFocusArea == null) {
focusIndicator.clear();
} else {
// Users touch on the preview and the indicator represents the
// metering area. Either focus area is not supported or
// autoFocus call is not required.
focusIndicator.showStart();
}
} else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
focusIndicator.showStart();
} else {
if (Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
// TODO: check HAL behavior and decide if this can be removed.
focusIndicator.showSuccess(false);
} else if (mState == STATE_SUCCESS) {
focusIndicator.showSuccess(false);
} else if (mState == STATE_FAIL) {
focusIndicator.showFail(false);
}
}
}
public void resetTouchFocus() {
if (!mInitialized) {
return;
}
// Put focus indicator to the center. clear reset position
mPieRenderer.clear();
mFocusArea = null;
mMeteringArea = null;
}
private void calculateTapArea(int focusWidth, int focusHeight, float areaMultiple,
int x, int y, int previewWidth, int previewHeight, Rect rect) {
int areaWidth = (int) (focusWidth * areaMultiple);
int areaHeight = (int) (focusHeight * areaMultiple);
int left = clamp(x - areaWidth / 2, 0, previewWidth - areaWidth);
int top = clamp(y - areaHeight / 2, 0, previewHeight - areaHeight);
RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight);
mMatrix.mapRect(rectF);
rectFToRect(rectF, rect);
}
/* package */ int getFocusState() {
return mState;
}
public boolean isFocusCompleted() {
return mState == STATE_SUCCESS || mState == STATE_FAIL;
}
public boolean isFocusingSnapOnFinish() {
return mState == STATE_FOCUSING_SNAP_ON_FINISH;
}
public void removeMessages() {
mHandler.removeMessages(RESET_TOUCH_FOCUS);
}
public void overrideFocusMode(String focusMode) {
mOverrideFocusMode = focusMode;
}
public void setAeAwbLock(boolean lock) {
mAeAwbLock = lock;
}
public boolean getAeAwbLock() {
return mAeAwbLock;
}
private boolean needAutoFocusCall() {
String focusMode = getFocusMode();
return !(focusMode.equals(Parameters.FOCUS_MODE_INFINITY)
|| focusMode.equals(Parameters.FOCUS_MODE_FIXED)
|| focusMode.equals(Parameters.FOCUS_MODE_EDOF));
}
public static boolean isAutoExposureLockSupported(Parameters params) {
return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED));
}
public static boolean isAutoWhiteBalanceLockSupported(Parameters params) {
return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED));
}
public static boolean isSupported(String value, List<String> supported) {
return supported != null && supported.indexOf(value) >= 0;
}
public static boolean isMeteringAreaSupported(Parameters params) {
return params.getMaxNumMeteringAreas() > 0;
}
public static boolean isFocusAreaSupported(Parameters params) {
return (params.getMaxNumFocusAreas() > 0
&& isSupported(Parameters.FOCUS_MODE_AUTO,
params.getSupportedFocusModes()));
}
public static void prepareMatrix(Matrix matrix, boolean mirror, int displayOrientation,
int viewWidth, int viewHeight) {
// Need mirror for front camera.
matrix.setScale(mirror ? -1 : 1, 1);
// This is the value for android.hardware.Camera.setDisplayOrientation.
matrix.postRotate(displayOrientation);
// Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
// UI coordinates range from (0, 0) to (width, height).
matrix.postScale(viewWidth / 2000f, viewHeight / 2000f);
matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);
}
public static int clamp(int x, int min, int max) {
Assert.isTrue(max >= min);
if (x > max) {
return max;
}
if (x < min) {
return min;
}
return x;
}
public static void rectFToRect(RectF rectF, Rect rect) {
rect.left = Math.round(rectF.left);
rect.top = Math.round(rectF.top);
rect.right = Math.round(rectF.right);
rect.bottom = Math.round(rectF.bottom);
}
}
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.mediapicker.camerafocus;
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();
}
}
}
@@ -0,0 +1,202 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.mediapicker.camerafocus;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import java.util.ArrayList;
import java.util.List;
/**
* Pie menu item
*/
public class PieItem {
public static interface OnClickListener {
void onClick(PieItem item);
}
private Drawable mDrawable;
private int level;
private 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);
}
}
@@ -0,0 +1,825 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.mediapicker.camerafocus;
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.messaging.R;
import java.util.ArrayList;
import java.util.List;
public class PieRenderer extends OverlayRenderer
implements FocusIndicator {
// 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;
public static 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() {
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);
}
@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);
}
}
}
@@ -0,0 +1,3 @@
The files in this package were copied from the android-4.4.4_r1 branch of ASOP from the folders
com/android/camera/ and com/android/camera/ui from files with the same name. Some modifications
have been made to remove unneeded features and adjust to our needs.
@@ -0,0 +1,178 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.ui.mediapicker.camerafocus;
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 {
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);
addRenderer(new PieRenderer(context));
}
public PieRenderer getPieRenderer() {
for (Renderer renderer : mClients) {
if (renderer instanceof PieRenderer) {
return (PieRenderer) renderer;
}
}
return null;
}
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();
}
}
}
}