/* * 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.content.Context; 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; 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; } @Override public RawTexture copyToTexture(GLCanvas c, RawTexture texture, int w, int h) { // We shouldn't be here since requireSurfaceTexture() returns true. return null; } }; 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(); // The client should implement this if requiresSurfaceTexture() is false; RawTexture copyToTexture(GLCanvas c, RawTexture texture, int width, int height); } public CameraScreenNail(Listener listener, Context ctx) { mListener = listener; mCaptureAnimManager = new CaptureAnimManager(ctx); } 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) { 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(); 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, x, y, width, height); } } 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 = mDraw.copyToTexture( canvas, mAnimTexture, getTextureWidth(), getTextureHeight()); } else { int width = mAnimTexture.getWidth(); int height = mAnimTexture.getHeight(); canvas.beginRenderTarget(mAnimTexture); // 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(); } } }