blob: d531f4adf66bedc7bce45bc140842b027ae24df4 [file] [log] [blame]
/*
* 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;
import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.opengl.Matrix;
import android.util.Log;
import com.android.gallery3d.common.ApiHelper;
import com.android.gallery3d.glrenderer.GLCanvas;
import com.android.gallery3d.glrenderer.RawTexture;
import com.android.gallery3d.ui.SurfaceTextureScreenNail;
/*
* This is a ScreenNail which can display camera's preview.
*/
@TargetApi(ApiHelper.VERSION_CODES.HONEYCOMB)
public class CameraScreenNail extends SurfaceTextureScreenNail {
private static final String TAG = "CAM_ScreenNail";
private static final int ANIM_NONE = 0;
// Capture animation is about to start.
private static final int ANIM_CAPTURE_START = 1;
// Capture animation is running.
private static final int ANIM_CAPTURE_RUNNING = 2;
// Switch camera animation needs to copy texture.
private static final int ANIM_SWITCH_COPY_TEXTURE = 3;
// Switch camera animation shows the initial feedback by darkening the
// preview.
private static final int ANIM_SWITCH_DARK_PREVIEW = 4;
// Switch camera animation is waiting for the first frame.
private static final int ANIM_SWITCH_WAITING_FIRST_FRAME = 5;
// Switch camera animation is about to start.
private static final int ANIM_SWITCH_START = 6;
// Switch camera animation is running.
private static final int ANIM_SWITCH_RUNNING = 7;
private boolean mVisible;
// True if first onFrameAvailable has been called. If screen nail is drawn
// too early, it will be all white.
private boolean mFirstFrameArrived;
private Listener mListener;
private final float[] mTextureTransformMatrix = new float[16];
// Animation.
private CaptureAnimManager mCaptureAnimManager = new CaptureAnimManager();
private SwitchAnimManager mSwitchAnimManager = new SwitchAnimManager();
private int mAnimState = ANIM_NONE;
private RawTexture mAnimTexture;
// Some methods are called by GL thread and some are called by main thread.
// This protects mAnimState, mVisible, and surface texture. This also makes
// sure some code are atomic. For example, requestRender and setting
// mAnimState.
private Object mLock = new Object();
private OnFrameDrawnListener mOneTimeFrameDrawnListener;
private int mRenderWidth;
private int mRenderHeight;
// This represents the scaled, uncropped size of the texture
// Needed for FaceView
private int mUncroppedRenderWidth;
private int mUncroppedRenderHeight;
private float mScaleX = 1f, mScaleY = 1f;
private boolean mFullScreen;
private boolean mEnableAspectRatioClamping = false;
private boolean mAcquireTexture = false;
private final DrawClient mDefaultDraw = new DrawClient() {
@Override
public void onDraw(GLCanvas canvas, int x, int y, int width, int height) {
CameraScreenNail.super.draw(canvas, x, y, width, height);
}
@Override
public boolean requiresSurfaceTexture() {
return true;
}
};
private DrawClient mDraw = mDefaultDraw;
private float mAlpha = 1f;
private Runnable mOnFrameDrawnListener;
public interface Listener {
void requestRender();
// Preview has been copied to a texture.
void onPreviewTextureCopied();
void onCaptureTextureCopied();
}
public interface OnFrameDrawnListener {
void onFrameDrawn(CameraScreenNail c);
}
public interface DrawClient {
void onDraw(GLCanvas canvas, int x, int y, int width, int height);
boolean requiresSurfaceTexture();
}
public CameraScreenNail(Listener listener) {
mListener = listener;
}
public void setFullScreen(boolean full) {
synchronized (mLock) {
mFullScreen = full;
}
}
/**
* returns the uncropped, but scaled, width of the rendered texture
*/
public int getUncroppedRenderWidth() {
return mUncroppedRenderWidth;
}
/**
* returns the uncropped, but scaled, width of the rendered texture
*/
public int getUncroppedRenderHeight() {
return mUncroppedRenderHeight;
}
@Override
public int getWidth() {
return mEnableAspectRatioClamping ? mRenderWidth : getTextureWidth();
}
@Override
public int getHeight() {
return mEnableAspectRatioClamping ? mRenderHeight : getTextureHeight();
}
private int getTextureWidth() {
return super.getWidth();
}
private int getTextureHeight() {
return super.getHeight();
}
@Override
public void setSize(int w, int h) {
super.setSize(w, h);
mEnableAspectRatioClamping = false;
if (mRenderWidth == 0) {
mRenderWidth = w;
mRenderHeight = h;
}
updateRenderSize();
}
/**
* Tells the ScreenNail to override the default aspect ratio scaling
* and instead perform custom scaling to basically do a centerCrop instead
* of the default centerInside
*
* Note that calls to setSize will disable this
*/
public void enableAspectRatioClamping() {
mEnableAspectRatioClamping = true;
updateRenderSize();
}
private void setPreviewLayoutSize(int w, int h) {
Log.i(TAG, "preview layout size: "+w+"/"+h);
mRenderWidth = w;
mRenderHeight = h;
updateRenderSize();
}
private void updateRenderSize() {
if (!mEnableAspectRatioClamping) {
mScaleX = mScaleY = 1f;
mUncroppedRenderWidth = getTextureWidth();
mUncroppedRenderHeight = getTextureHeight();
Log.i(TAG, "aspect ratio clamping disabled");
return;
}
float aspectRatio;
if (getTextureWidth() > getTextureHeight()) {
aspectRatio = (float) getTextureWidth() / (float) getTextureHeight();
} else {
aspectRatio = (float) getTextureHeight() / (float) getTextureWidth();
}
float scaledTextureWidth, scaledTextureHeight;
if (mRenderWidth > mRenderHeight) {
scaledTextureWidth = Math.max(mRenderWidth,
(int) (mRenderHeight * aspectRatio));
scaledTextureHeight = Math.max(mRenderHeight,
(int)(mRenderWidth / aspectRatio));
} else {
scaledTextureWidth = Math.max(mRenderWidth,
(int) (mRenderHeight / aspectRatio));
scaledTextureHeight = Math.max(mRenderHeight,
(int) (mRenderWidth * aspectRatio));
}
mScaleX = mRenderWidth / scaledTextureWidth;
mScaleY = mRenderHeight / scaledTextureHeight;
mUncroppedRenderWidth = Math.round(scaledTextureWidth);
mUncroppedRenderHeight = Math.round(scaledTextureHeight);
Log.i(TAG, "aspect ratio clamping enabled, surfaceTexture scale: " + mScaleX + ", " + mScaleY);
}
public void acquireSurfaceTexture() {
synchronized (mLock) {
mFirstFrameArrived = false;
mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
mAcquireTexture = true;
}
mListener.requestRender();
}
@Override
public void releaseSurfaceTexture() {
synchronized (mLock) {
if (mAcquireTexture) {
mAcquireTexture = false;
mLock.notifyAll();
} else {
if (super.getSurfaceTexture() != null) {
super.releaseSurfaceTexture();
}
mAnimState = ANIM_NONE; // stop the animation
}
}
}
public void copyTexture() {
synchronized (mLock) {
mListener.requestRender();
mAnimState = ANIM_SWITCH_COPY_TEXTURE;
}
}
public void animateSwitchCamera() {
Log.v(TAG, "animateSwitchCamera");
synchronized (mLock) {
if (mAnimState == ANIM_SWITCH_DARK_PREVIEW) {
// Do not request render here because camera has been just
// started. We do not want to draw black frames.
mAnimState = ANIM_SWITCH_WAITING_FIRST_FRAME;
}
}
}
public void animateCapture(int displayRotation) {
synchronized (mLock) {
mCaptureAnimManager.setOrientation(displayRotation);
mCaptureAnimManager.animateFlashAndSlide();
mListener.requestRender();
mAnimState = ANIM_CAPTURE_START;
}
}
public RawTexture getAnimationTexture() {
return mAnimTexture;
}
public void animateFlash(int displayRotation) {
synchronized (mLock) {
mCaptureAnimManager.setOrientation(displayRotation);
mCaptureAnimManager.animateFlash();
mListener.requestRender();
mAnimState = ANIM_CAPTURE_START;
}
}
public void animateSlide() {
synchronized (mLock) {
// Ignore the case where animateFlash is skipped but animateSlide is called
// e.g. Double tap shutter and immediately swipe to gallery, and quickly swipe back
// to camera. This case only happens in monkey tests, not applicable to normal
// human beings.
if (mAnimState != ANIM_CAPTURE_RUNNING) {
Log.v(TAG, "Cannot animateSlide outside of animateCapture!"
+ " Animation state = " + mAnimState);
return;
}
mCaptureAnimManager.animateSlide();
mListener.requestRender();
}
}
private void callbackIfNeeded() {
if (mOneTimeFrameDrawnListener != null) {
mOneTimeFrameDrawnListener.onFrameDrawn(this);
mOneTimeFrameDrawnListener = null;
}
}
@Override
protected void updateTransformMatrix(float[] matrix) {
super.updateTransformMatrix(matrix);
Matrix.translateM(matrix, 0, .5f, .5f, 0);
Matrix.scaleM(matrix, 0, mScaleX, mScaleY, 1f);
Matrix.translateM(matrix, 0, -.5f, -.5f, 0);
}
public void directDraw(GLCanvas canvas, int x, int y, int width, int height) {
DrawClient draw;
synchronized (mLock) {
draw = mDraw;
}
draw.onDraw(canvas, x, y, width, height);
}
public void setDraw(DrawClient draw) {
synchronized (mLock) {
if (draw == null) {
mDraw = mDefaultDraw;
} else {
mDraw = draw;
}
}
mListener.requestRender();
}
@Override
public void draw(GLCanvas canvas, int x, int y, int width, int height) {
synchronized (mLock) {
allocateTextureIfRequested(canvas);
if (!mVisible) mVisible = true;
SurfaceTexture surfaceTexture = getSurfaceTexture();
if (mDraw.requiresSurfaceTexture() && (surfaceTexture == null || !mFirstFrameArrived)) {
return;
}
if (mOnFrameDrawnListener != null) {
mOnFrameDrawnListener.run();
mOnFrameDrawnListener = null;
}
float oldAlpha = canvas.getAlpha();
canvas.setAlpha(mAlpha);
switch (mAnimState) {
case ANIM_NONE:
directDraw(canvas, x, y, width, height);
break;
case ANIM_SWITCH_COPY_TEXTURE:
copyPreviewTexture(canvas);
mSwitchAnimManager.setReviewDrawingSize(width, height);
mListener.onPreviewTextureCopied();
mAnimState = ANIM_SWITCH_DARK_PREVIEW;
// The texture is ready. Fall through to draw darkened
// preview.
case ANIM_SWITCH_DARK_PREVIEW:
case ANIM_SWITCH_WAITING_FIRST_FRAME:
// Consume the frame. If the buffers are full,
// onFrameAvailable will not be called. Animation state
// relies on onFrameAvailable.
surfaceTexture.updateTexImage();
mSwitchAnimManager.drawDarkPreview(canvas, x, y, width,
height, mAnimTexture);
break;
case ANIM_SWITCH_START:
mSwitchAnimManager.startAnimation();
mAnimState = ANIM_SWITCH_RUNNING;
break;
case ANIM_CAPTURE_START:
copyPreviewTexture(canvas);
mListener.onCaptureTextureCopied();
mCaptureAnimManager.startAnimation(x, y, width, height);
mAnimState = ANIM_CAPTURE_RUNNING;
break;
}
if (mAnimState == ANIM_CAPTURE_RUNNING || mAnimState == ANIM_SWITCH_RUNNING) {
boolean drawn;
if (mAnimState == ANIM_CAPTURE_RUNNING) {
if (!mFullScreen) {
// Skip the animation if no longer in full screen mode
drawn = false;
} else {
drawn = mCaptureAnimManager.drawAnimation(canvas, this, mAnimTexture);
}
} else {
drawn = mSwitchAnimManager.drawAnimation(canvas, x, y,
width, height, this, mAnimTexture);
}
if (drawn) {
mListener.requestRender();
} else {
// Continue to the normal draw procedure if the animation is
// not drawn.
mAnimState = ANIM_NONE;
directDraw(canvas, x, y, width, height);
}
}
canvas.setAlpha(oldAlpha);
callbackIfNeeded();
} // mLock
}
private void copyPreviewTexture(GLCanvas canvas) {
if (!mDraw.requiresSurfaceTexture() && mAnimTexture == null) {
mAnimTexture = new RawTexture(getTextureWidth(), getTextureHeight(), true);
mAnimTexture.setIsFlippedVertically(true);
}
int width = mAnimTexture.getWidth();
int height = mAnimTexture.getHeight();
canvas.beginRenderTarget(mAnimTexture);
if (!mDraw.requiresSurfaceTexture()) {
mDraw.onDraw(canvas, 0, 0, width, height);
} else {
// Flip preview texture vertically. OpenGL uses bottom left point
// as the origin (0, 0).
canvas.translate(0, height);
canvas.scale(1, -1, 1);
getSurfaceTexture().getTransformMatrix(mTextureTransformMatrix);
updateTransformMatrix(mTextureTransformMatrix);
canvas.drawTexture(mExtTexture, mTextureTransformMatrix, 0, 0, width, height);
}
canvas.endRenderTarget();
}
@Override
public void noDraw() {
synchronized (mLock) {
mVisible = false;
}
}
@Override
public void recycle() {
synchronized (mLock) {
mVisible = false;
}
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
synchronized (mLock) {
if (getSurfaceTexture() != surfaceTexture) {
return;
}
mFirstFrameArrived = true;
if (mVisible) {
if (mAnimState == ANIM_SWITCH_WAITING_FIRST_FRAME) {
mAnimState = ANIM_SWITCH_START;
}
// We need to ask for re-render if the SurfaceTexture receives a new
// frame.
mListener.requestRender();
}
}
}
// We need to keep track of the size of preview frame on the screen because
// it's needed when we do switch-camera animation. See comments in
// SwitchAnimManager.java. This is based on the natural orientation, not the
// view system orientation.
public void setPreviewFrameLayoutSize(int width, int height) {
synchronized (mLock) {
mSwitchAnimManager.setPreviewFrameLayoutSize(width, height);
setPreviewLayoutSize(width, height);
}
}
public void setOneTimeOnFrameDrawnListener(OnFrameDrawnListener l) {
synchronized (mLock) {
mFirstFrameArrived = false;
mOneTimeFrameDrawnListener = l;
}
}
@Override
public SurfaceTexture getSurfaceTexture() {
synchronized (mLock) {
SurfaceTexture surfaceTexture = super.getSurfaceTexture();
if (surfaceTexture == null && mAcquireTexture) {
try {
mLock.wait();
surfaceTexture = super.getSurfaceTexture();
} catch (InterruptedException e) {
Log.w(TAG, "unexpected interruption");
}
}
return surfaceTexture;
}
}
private void allocateTextureIfRequested(GLCanvas canvas) {
synchronized (mLock) {
if (mAcquireTexture) {
super.acquireSurfaceTexture(canvas);
mAcquireTexture = false;
mLock.notifyAll();
}
}
}
public void setOnFrameDrawnOneShot(Runnable run) {
synchronized (mLock) {
mOnFrameDrawnListener = run;
}
}
public float getAlpha() {
synchronized (mLock) {
return mAlpha;
}
}
public void setAlpha(float alpha) {
synchronized (mLock) {
mAlpha = alpha;
mListener.requestRender();
}
}
}