package com.glview.view; import java.util.ArrayList; import java.util.List; import android.content.Context; import android.graphics.Rect; import android.media.AudioManager; import android.opengl.GLSurfaceView; import android.util.AttributeSet; import android.util.Log; import android.util.LogPrinter; import android.util.Printer; import android.view.InputEvent; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.SurfaceHolder; import android.view.SurfaceView; import com.glview.animation.Animator; import com.glview.graphics.Bitmap; import com.glview.hwui.task.Task; import com.glview.hwui.task.TaskHandler; import com.glview.thread.Handler; import com.glview.thread.Looper; import com.glview.thread.Message; import com.glview.view.View.AttachInfo; import com.glview.view.ViewGroup.LayoutParams; import com.glview.view.animation.AnimationUtils; /** * @hide * @author lijing.lj */ final public class GLRootView extends SurfaceView implements SurfaceHolder.Callback, SurfaceHolder.Callback2, android.view.ViewTreeObserver.OnTouchModeChangeListener { protected final static String TAG = "GLRootView"; final static boolean DEBUG = true; final static boolean DEBUG_GL_MESSAGE = false; final static boolean DEBUG_LOOPER = false; private static final int FLAG_NEED_LAYOUT = 0x00000002; public static final int GL_ROOT_VIEW_ID = 10190511; /** * The renderer only renders * when the surface is created, or when {@link #requestRender} is called. * * @see #getRenderMode() * @see #setRenderMode(int) * @see #requestRender() */ public final static int RENDERMODE_WHEN_DIRTY = 0; /** * The renderer is called * continuously to re-render the scene. * * @see #getRenderMode() * @see #setRenderMode(int) */ public final static int RENDERMODE_CONTINUOUSLY = 1; Printer pw = new LogPrinter(Log.DEBUG, TAG); GLRenderer mRenderer; protected GLHandler mGLHandler = new GLHandler(); Thread mThread; boolean mRenderRequested = false; boolean mRenderPrepared = false; boolean mAttatched = false; boolean mFirst; // This is the top-level view of the window, containing the window decor. private ViewGroup mDecor; private int mRenderMode = RENDERMODE_WHEN_DIRTY; private int mFlags = FLAG_NEED_LAYOUT; Handler mHandler = new Handler(Looper.getMainLooper()); android.os.Handler mAndroidHandler = new android.os.Handler(android.os.Looper.getMainLooper()); AttachInfo mAttachInfo; WindowId mWindowId = new WindowId(); List<Callback> mCallbacks = new ArrayList<Callback>(); /** * see {@link PlaySound#playSoundEffect(int)} */ AudioManager mAudioManager; public GLRootView(Context context) { super(context); init(); } public GLRootView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { mFirst = true; mAttachInfo = new AttachInfo(this, mWindowId, mHandler, mAndroidHandler, new RootCallbacks()); mThread = mHandler.getLooper().getThread(); getHolder().addCallback(this); setId(GL_ROOT_VIEW_ID); setFocusable(true); setFocusableInTouchMode(true); } public void setDebugEnable(boolean enable) { if (enable) { mRenderMode = RENDERMODE_CONTINUOUSLY; } else { mRenderMode = RENDERMODE_WHEN_DIRTY; } } GLRenderer getRenderer() { if (mRenderer == null) { mRenderer = GLRenderer.createRender(); } return mRenderer; } @Override public void surfaceRedrawNeeded(SurfaceHolder holder) { } /** * This method is part of the SurfaceHolder.Callback interface, we get * a surface to draw our UI. */ @Override public void surfaceCreated(SurfaceHolder holder) { if (DEBUG) Log.d(TAG, "surfaceCreated called."); mGLHandler.sendSyncMessage(GLHandler.MSG_SURFACE_CREATED, 0, 0, holder); } /** * This method is part of the SurfaceHolder.Callback interface, now we * are identified of the size of the surface, so we can set out viewport. */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { if (DEBUG) Log.d(TAG, String.format("surfaceChanged called; format=%s, width=%s, height=%s.", format, width, height)); mGLHandler.sendSyncMessage(GLHandler.MSG_SURFACE_CHANGED, width, height, holder); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (DEBUG) Log.d(TAG, String.format("onLayout called; left=%s, top=%s, right=%s, bottom=%s.", left, top, right, bottom)); if (changed) mGLHandler.sendSyncMessage(GLHandler.MSG_REQUEST_LAYOUT, 0, 0, null); } /** * This method is part of the SurfaceHolder.Callback interface, we lost * the surface, it has been destroyed, we can not do any drawing. * Maybe it's just because our window is invisible right now, we should * hold the resources which can be recycled in method {@link #onDetachedFromWindow()} */ @Override public void surfaceDestroyed(SurfaceHolder holder) { if (DEBUG) Log.d(TAG, "surfaceDestroyed called."); mGLHandler.sendSyncMessage(GLHandler.MSG_SURFACE_DESTROYED, 0, 0, null); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mAttatched = true; if (DEBUG) Log.d(TAG, "onAttachedToWindow called."); getViewTreeObserver().addOnTouchModeChangeListener(this); mGLHandler.sendSyncMessage(GLHandler.MSG_ATTACHED_TO_WINDOW, 0, 0, null); } /** * The SurfaceView is detached, we consider that it's out of its lifecircle, * so it's time to recycle unused resources.. */ @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mAttatched = false; if (DEBUG) Log.d(TAG, "onDetachedFromWindow called."); getViewTreeObserver().removeOnTouchModeChangeListener(this); mGLHandler.sendSyncMessage(GLHandler.MSG_DETACHED_FROM_WINDOW, 0, 0, null); } public void setContentView(View view) { if (DEBUG) Log.d(TAG, "setContentView called. view=" + view); mGLHandler.sendSyncMessage(GLHandler.MSG_SET_CONTENT_VIEW, 0, 0, view); } public void setContentView(int resId) { if (DEBUG) Log.d(TAG, "setContentView called. resId=" + resId); mGLHandler.sendSyncMessage(GLHandler.MSG_SET_CONTENT_VIEW, resId, 0, null); } void attachContentView(int resId) { if (resId > 0) { if (mDecor == null) { installDecor(); } View v = LayoutInflater.from(getContext()).inflate(resId, mDecor, false); if (v != null) { attachContentView(v); } } } void attachContentView(View view) { if (mDecor == null) { installDecor(); } else { mDecor.removeAllViews(); } mDecor.addView(view); mAttachInfo.mRootView = view; dispatchAttach(); } void dispatchAttach() { for (Callback callback : mCallbacks) { callback.onAttached(mDecor); } } private void installDecor() { if (mDecor == null) { mDecor = new DecorView(getContext()); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); mAttachInfo.mRootView = mDecor; if (mAttatched) { attachToRoot(); } } } public void addCallback(Callback callback) { if (!mCallbacks.contains(callback)) { mCallbacks.add(callback); } } public void removeCallback(Callback callback) { mCallbacks.remove(callback); } void detachFromRoot() { if (mDecor != null && mDecor.isAttachedToWindow()) { mDecor.dispatchDetachedFromWindow(); } } void attachToRoot() { if (mDecor != null && !mDecor.isAttachedToWindow()) { mDecor.dispatchAttachedToWindow(mAttachInfo, 0); requestLayoutGLContentView(); } } public void requestRender() { checkThread(); scheduleRender(); } void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } } /** * @hide * Start some RenderThread-driven animations, these animations runs in RenderThread. * Only called from ViewPropertyAnimatorRT. * @see ViewPropertyAnimatorRT#startAnimation(ViewPropertyAnimator) * * @param animationStarter */ void startRTAnimation(List<Animator> animators) { getRenderer().startRTAnimation(animators); } /** * @hide * Stop some RenderThread-driven animations. * @param animators */ void stopRTAnimation(List<Animator> animators) { getRenderer().stopRTAnimation(animators); } Bitmap buildDrawingCache(View v) { return getRenderer().buildDrawingCache(v); } void scheduleRender() { mRenderRequested = true; if (getRenderer().isEnable() && !mRenderPrepared) { mRenderRequested = false; mRenderPrepared = true; mGLHandler.post(mRenderRunnable); } } void unscheduleRender() { mRenderRequested = false; mRenderPrepared = false; mGLHandler.remove(mRenderRunnable); } Task mRenderRunnable = new Task() { @Override public void doTask() { mRenderPrepared = false; if (getRenderer().isEnable()) { onDrawFrame(); } } }; protected void onDrawFrame() { checkThread(); collectViewAttributes(); mAttachInfo.mDrawingTime = AnimationUtils.currentAnimationTimeMillis(); // if need layout if ( (mFlags & FLAG_NEED_LAYOUT) == FLAG_NEED_LAYOUT ) { layoutContentPane(); mAttachInfo.mTreeObserver.dispatchOnGlobalLayout(); } if (mFirst) { boolean isInTouchMode = isInTouchMode(); mAttachInfo.mInTouchMode = !isInTouchMode; ensureTouchMode(isInTouchMode); } boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw(); if (!cancelDraw) { if (mAttachInfo.mViewScrollChanged) { mAttachInfo.mViewScrollChanged = false; mAttachInfo.mTreeObserver.dispatchOnScrollChanged(); } mAttachInfo.mTreeObserver.dispatchOnDraw(); if (mDecor != null) { getRenderer().draw(mDecor); } } else { // Try again scheduleRender(); } if (mRenderMode == RENDERMODE_CONTINUOUSLY) { requestRender(); } mFirst = false; } private boolean collectViewAttributes() { if (mAttachInfo.mRecomputeGlobalAttributes) { mAttachInfo.mRecomputeGlobalAttributes = false; mAttachInfo.mRecomputeGlobalAttributes = false; boolean oldScreenOn = mAttachInfo.mKeepScreenOn; mAttachInfo.mKeepScreenOn = false; mAttachInfo.mSystemUiVisibility = 0; mAttachInfo.mHasSystemUiListeners = false; mDecor.dispatchCollectViewAttributes(mAttachInfo, 0); mAttachInfo.mSystemUiVisibility &= ~mAttachInfo.mDisabledSystemUiVisibility; if (mAttachInfo.mKeepScreenOn != oldScreenOn || mAttachInfo.mSystemUiVisibility != getSystemUiVisibility()) { removeCallbacks(mViewAttributesRunnable); post(mViewAttributesRunnable); mDecor.dispatchSystemUiVisibilityChanged(mAttachInfo.mSystemUiVisibility); } } return false; } Runnable mViewAttributesRunnable = new Runnable() { @Override public void run() { setKeepScreenOn(mAttachInfo.mKeepScreenOn); setSystemUiVisibility(mAttachInfo.mSystemUiVisibility); } }; /** * Recycle all the resources. * Called by method {@link #onDetachedFromWindow()}. */ void destroy() { getRenderer().destroy(true); } public void requestLayoutGLContentView() { checkThread(); if (mDecor == null) return; // "View" system will invoke onLayout() for initialization(bug ?), we // have to ignore it since the GLThread is not ready yet. mFlags |= FLAG_NEED_LAYOUT; requestRender(); } @Override public boolean dispatchTouchEvent(MotionEvent event) { if (!isEnabled()) return false; if (mDecor != null) { MotionEvent me = MotionEvent.obtain(event); me.offsetLocation(getLeft(), getTop()); mInputEventTask.mInputEvent = me; mGLHandler.postAndWait(mInputEventTask); me.recycle(); return mInputEventTask.mResult; } return super.dispatchTouchEvent(event); } InputEventTask mInputEventTask = new InputEventTask(); @Override public boolean dispatchKeyEvent(KeyEvent event) { if (!isEnabled()) return false; if (mDecor != null) { mInputEventTask.mInputEvent = event; mGLHandler.postAndWait(mInputEventTask); return mInputEventTask.mResult; } return super.dispatchKeyEvent(event); } @Override protected void onFocusChanged(final boolean gainFocus, final int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); } private void layoutContentPane() { mFlags &= ~FLAG_NEED_LAYOUT; int w = getWidth(); int h = getHeight(); if (mDecor != null && mDecor.getVisibility() != View.GONE && w != 0 && h != 0) { int rootWidthSpec; int rootHeightSpec; LayoutParams lp = mDecor.getLayoutParams(); if (lp != null) { if (lp.width == LayoutParams.FILL_PARENT || lp.width == LayoutParams.MATCH_PARENT) { rootWidthSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY); } else if (lp.width == LayoutParams.WRAP_CONTENT) { rootWidthSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.AT_MOST); } else { rootWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); } if (lp.height == LayoutParams.FILL_PARENT || lp.height == LayoutParams.MATCH_PARENT) { rootHeightSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY); } else if (lp.height == LayoutParams.WRAP_CONTENT) { rootHeightSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.AT_MOST); } else { rootHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); } } else { rootWidthSpec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY); rootHeightSpec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY); } mDecor.measure(rootWidthSpec, rootHeightSpec); mDecor.layout(0, 0, mDecor.getMeasuredWidth(), getMeasuredHeight()); } } public View getContentPane() { return mDecor; } public void recomputeViewAttributes(View child) { checkThread(); if (mDecor == child) { mAttachInfo.mRecomputeGlobalAttributes = true; scheduleRender(); } } class InputEventTask extends Task { InputEvent mInputEvent; boolean mResult; @Override public void doTask() { if (mDecor != null && mInputEvent != null) { if (mInputEvent instanceof KeyEvent) { mResult = mDecor.dispatchKeyEvent((KeyEvent) mInputEvent); } else if (mInputEvent instanceof MotionEvent) { mResult = mDecor.dispatchTouchEvent((MotionEvent) mInputEvent); } else { mResult = false; } } else { mResult = false; } mInputEvent = null; } } public static final class CalledFromWrongThreadException extends RuntimeException { private static final long serialVersionUID = 5556515200079720110L; public CalledFromWrongThreadException(String msg) { super(msg); } } private AudioManager getAudioManager() { if (mAudioManager == null) { mAudioManager = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE); } return mAudioManager; } private class RootCallbacks implements AttachInfo.Callbacks { @Override public void playSoundEffect(int effectId) { try { final AudioManager audioManager = getAudioManager(); switch (effectId) { case SoundEffectConstants.CLICK: audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK); return; case SoundEffectConstants.NAVIGATION_DOWN: audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_DOWN); return; case SoundEffectConstants.NAVIGATION_LEFT: audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_LEFT); return; case SoundEffectConstants.NAVIGATION_RIGHT: audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_RIGHT); return; case SoundEffectConstants.NAVIGATION_UP: audioManager.playSoundEffect(AudioManager.FX_FOCUS_NAVIGATION_UP); return; default: throw new IllegalArgumentException("unknown effect id " + effectId + " not defined in " + SoundEffectConstants.class.getCanonicalName()); } } catch (IllegalStateException e) { // Exception thrown by getAudioManager() when mView is null Log.e(TAG, "FATAL EXCEPTION when attempting to play sound effect: " + e); e.printStackTrace(); } } @Override public boolean performHapticFeedback(int effectId, boolean always) { return GLRootView.this.performHapticFeedback(effectId); } } // It's now depend on android handler. // We consider to striper the handler from android later. protected class GLHandler extends TaskHandler { final static int MSG_SURFACE_CREATED = 1; final static int MSG_SURFACE_CHANGED = 2; final static int MSG_SURFACE_DESTROYED = 3; final static int MSG_DETACHED_FROM_WINDOW = 4; final static int MSG_ATTACHED_TO_WINDOW = 5; final static int MSG_REQUEST_LAYOUT = 6; final static int MSG_SET_CONTENT_VIEW = 7; SyncMessageTask mSyncMessageTask = new SyncMessageTask(); public GLHandler() { super(Looper.getMainLooper()); } class SyncMessageTask extends Task { Message mMessage; SyncMessageTask() { } @Override public void doTask() { if (mMessage != null) { dispatchMessage(mMessage); mMessage.recycle(); } mMessage = null; } } @Override public String getMessageName(Message msg) { switch (msg.what) { case MSG_SURFACE_CREATED: return "MSG_SURFACE_CREATED"; case MSG_SURFACE_CHANGED: return "MSG_SURFACE_CHANGED"; case MSG_SURFACE_DESTROYED: return "MSG_SURFACE_DESTROYED"; case MSG_ATTACHED_TO_WINDOW: return "MSG_ATTACHED_TO_WINDOW"; case MSG_DETACHED_FROM_WINDOW: return "MSG_DETACHED_FROM_WINDOW"; case MSG_REQUEST_LAYOUT: return "MSG_REQUEST_LAYOUT"; case MSG_SET_CONTENT_VIEW: return "MSG_SET_CONTENT_VIEW"; default: break; } return super.getMessageName(msg); } @Override public void handleMessage(Message msg) { if (DEBUG_GL_MESSAGE) Log.d(TAG, "GLHandler handleMessage, message=" + getMessageName(msg)); if (DEBUG_LOOPER) getLooper().dump(pw, "looper"); switch (msg.what) { case MSG_SURFACE_CREATED: getRenderer().initialize(msg.obj); scheduleRender(); break; case MSG_SURFACE_CHANGED: getRenderer().setSize(msg.obj, msg.arg1, msg.arg2); scheduleRender(); break; case MSG_SURFACE_DESTROYED: getRenderer().destroy(false); unscheduleRender(); break; case MSG_ATTACHED_TO_WINDOW: attachToRoot(); break; case MSG_DETACHED_FROM_WINDOW: detachFromRoot(); destroy(); unscheduleRender(); break; case MSG_REQUEST_LAYOUT: requestLayoutGLContentView(); break; case MSG_SET_CONTENT_VIEW: if (msg.obj != null) { attachContentView((View) msg.obj); } else { attachContentView(msg.arg1); } break; default: break; } } /** * This may block the current thread if current thread is not this handler thread. * Methods like {@link GLSurfaceView#surfaceCreated(SurfaceHolder)} * {@link GLSurfaceView#surfaceChanged(SurfaceHolder, int, int, int)} * {@link GLSurfaceView#surfaceDestroyed(SurfaceHolder)} * {@link GLSurfaceView#onDetachedFromWindow()} should use this to send sync messages. * @param what * @param arg1 * @param arg2 * @param obj */ void sendSyncMessage(int what, int arg1, int arg2, Object obj) { // Post a sync message. // Shouldn't call this frequently, or should we use a task poll? // This method is only called from the Android main thread until now, so we can cache the task object in a local member. final SyncMessageTask task = mSyncMessageTask;//new SyncMessageTask(); task.mMessage = obtainMessage(what, arg1, arg2, obj); postAndWait(task); } Message sendLocalMessage(int what, int arg1, int arg2, Object obj) { Message message = obtainMessage(what, arg1, arg2, obj); runOnGLThread(message); return message; } void runOnGLThread(Message msg) { if (isCurrentThread()) { dispatchMessage(msg); } else { sendMessage(msg); } } } public static interface Callback { public void onAttached(View content); } @Override public void onTouchModeChanged(boolean isInTouchMode) { ensureTouchMode(isInTouchMode); } /** * Something in the current window tells us we need to change the touch mode. For * example, we are not in touch mode, and the user touches the screen. * * If the touch mode has changed, tell the window manager, and handle it locally. * * @param inTouchMode Whether we want to be in touch mode. * @return True if the touch mode changed and focus changed was changed as a result */ boolean ensureTouchMode(boolean inTouchMode) { if (mAttachInfo.mInTouchMode == inTouchMode) { return false; } mTouchModeChangedTask.mIsInTouchMode = inTouchMode; mGLHandler.postAndWait(mTouchModeChangedTask); return true; } TouchModeChangedTask mTouchModeChangedTask = new TouchModeChangedTask(); class TouchModeChangedTask extends Task { boolean mIsInTouchMode; @Override public void doTask() { mAttachInfo.mInTouchMode = mIsInTouchMode; mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(mIsInTouchMode); if (mIsInTouchMode) { enterTouchMode(); } else { leaveTouchMode(); } } } private boolean enterTouchMode() { if (mDecor != null && mDecor.hasFocus()) { // note: not relying on mFocusedView here because this could // be when the window is first being added, and mFocused isn't // set yet. final View focused = mDecor.findFocus(); if (focused != null && !focused.isFocusableInTouchMode()) { // There's nothing to focus. Clear and propagate through the // hierarchy, but don't attempt to place new focus. focused.clearFocusInternal(null, true, false); return true; } } return false; } private boolean leaveTouchMode() { if (mDecor != null) { if (mDecor.hasFocus()) { View focusedView = mDecor.findFocus(); if (!(focusedView instanceof ViewGroup)) { // some view has focus, let it keep it return false; } else if (((ViewGroup) focusedView).getDescendantFocusability() != ViewGroup.FOCUS_AFTER_DESCENDANTS) { // some view group has focus, and doesn't prefer its children // over itself for focus, so let them keep it. return false; } } // find the best view to give focus to in this brave new non-touch-mode // world final View focused = focusSearch(null, View.FOCUS_DOWN); if (focused != null) { return focused.requestFocus(View.FOCUS_DOWN); } } return false; } /** * {@inheritDoc} */ public View focusSearch(View focused, int direction) { checkThread(); if (!(mDecor instanceof ViewGroup)) { return null; } return FocusFinder.getInstance().findNextFocus((ViewGroup) mDecor, focused, direction); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); mWindowFocusChangedTask.mHasWindowFocus = hasWindowFocus; mGLHandler.postAndWait(mWindowFocusChangedTask); } WindowFocusChangedTask mWindowFocusChangedTask = new WindowFocusChangedTask(); class WindowFocusChangedTask extends Task { boolean mHasWindowFocus; @Override public void doTask() { mAttachInfo.mHasWindowFocus = mHasWindowFocus; if (mDecor != null) { mDecor.dispatchWindowFocusChanged(mHasWindowFocus); } } } @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); mWindowVisibilityChangedTask.mVisibility = visibility; mGLHandler.postAndWait(mWindowVisibilityChangedTask); } WindowVisibilityChangedTask mWindowVisibilityChangedTask = new WindowVisibilityChangedTask(); class WindowVisibilityChangedTask extends Task { int mVisibility; @Override public void doTask() { if (mDecor != null) { mDecor.dispatchWindowVisibilityChanged(mVisibility); } } } }