/* * Copyright 2013 MicaByte Systems * * 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.micabytes.gfx; import android.content.Context; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.PointF; import android.os.Build; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.widget.Scroller; import com.micabytes.util.GameLog; import org.jetbrains.annotations.NonNls; /** * MicaSurfaceView encapsulates all of the logic for handling 2D game maps. Pass it a * SurfaceListener to receive touch events and a SurfaceRenderer to handle the drawing. */ @SuppressWarnings("MethodReturnAlwaysConstant") public class MicaSurfaceView extends SurfaceView implements SurfaceHolder.Callback, GestureDetector.OnGestureListener { private static final String TAG = MicaSurfaceView.class.getName(); @NonNls private static final String DRAW_THREAD = "drawThread"; private static final int SCALE_MOVE_GUARD = 500; @NonNls public static final String GOOGLE = "google"; @NonNls public static final String ASUS = "asus"; @NonNls public static final String NEXUS_7 = "Nexus 7"; /** * The Game Controller. This where we send UI events other than scroll and pinch-zoom in order to be handled */ private SurfaceListener listener; /** * The Game Renderer. This handles all of the drawing duties to the Surface view */ @SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized") private SurfaceRenderer renderer; // The Touch Handlers private TouchHandler touch; private GestureDetector gesture; private ScaleGestureDetector scaleGesture; private long lastScaleTime; // Rendering Thread private GameSurfaceViewThread thread; //private Runnable threadEvent = null; public MicaSurfaceView(Context context) { super(context); // This ensures that we don't get errors when using it in layout editing if (isInEditMode()) return; touch = new TouchHandler(context); initialize(context); } public MicaSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); // This ensures that we don't get errors when using it in layout editing if (isInEditMode()) return; touch = new TouchHandler(context); initialize(context); } public MicaSurfaceView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // This ensures that we don't get errors when using it in layout editing if (isInEditMode()) return; touch = new TouchHandler(context); initialize(context); } private void initialize(Context context) { // Set SurfaceHolder callback getHolder().addCallback(this); // Initialize touch handlers gesture = new GestureDetector(context, this); scaleGesture = new ScaleGestureDetector(context, new ScaleListener()); // Allow focus setFocusable(true); } /** * Sets the surface view listener */ public void setListener(SurfaceListener surfaceListener) { listener = surfaceListener; } /** * Sets the renderer and creates the rendering thread */ public void setRenderer(SurfaceRenderer r) { renderer = r; } // Return the position of the current view (center) public Point getViewPosition() { Point ret = new Point(); renderer.getViewPosition(ret); return ret; } @SuppressWarnings("unused") public void setViewPort(int w, int h) { renderer.setViewSize(w, h); } public void setViewPosition(Point p) { renderer.setViewPosition(p.x, p.y); } @SuppressWarnings("unused") public void setMapPosition(Point p) { renderer.setMapPosition(p.x, p.y); } @SuppressWarnings("unused") public void centerViewPosition() { Point viewportSize = new Point(); Point sceneSize = renderer.getBackgroundSize(); renderer.getViewSize(viewportSize); int x = (sceneSize.x - viewportSize.x) / 2; int y = (sceneSize.y - viewportSize.y) / 2; renderer.setViewPosition(x, y); } @SuppressWarnings("unused") public Point getViewSize() { Point ret = new Point(); renderer.getViewPosition(ret); return ret; } public float getZoom() { return renderer.getZoom(); } public void setZoom(float z, PointF center) { renderer.zoom(z, center); } @Override public void surfaceCreated(SurfaceHolder holder) { GameLog.d(TAG, "surfaceCreate"); thread = new GameSurfaceViewThread(holder); thread.setName(DRAW_THREAD); thread.setRunning(true); thread.start(); renderer.start(); touch.start(); // Required to ensure thread has focus if (thread != null) thread.onWindowFocusChanged(true); GameLog.d(TAG, "surfaceCreated"); } @Override public void surfaceDestroyed(SurfaceHolder holder) { GameLog.d(TAG, "surfaceDestroying"); touch.stop(); renderer.stop(); thread.setRunning(false); //thread.surfaceDestroyed(); boolean retry = true; while (retry) { try { thread.join(); retry = false; } catch (InterruptedException ignored) { // Repeat until success } } GameLog.d(TAG, "surfaceDestroyed"); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { renderer.setViewSize(width, height); // Recheck scale factor and reset position to prevent out of bounds Point p = new Point(); renderer.getViewPosition(p); setZoom(getZoom(), new PointF(p.x, p.y)); //Point p = new Point(); //this.renderer.getViewPosition(p); renderer.setViewPosition(p.x, p.y); // Debug GameLog.d(TAG, "surfaceChanged; new dimensions: w=" + width + ", h= " + height); // Required to ensure thread has focus if (thread != null) thread.onWindowFocusChanged(true); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (thread != null) thread.onWindowFocusChanged(hasWindowFocus); GameLog.d(TAG, "onWindowFocusChanged"); } // ---------------------------------------------------------------------- /** * The Rendering thread for the MicaSurfaceView */ @SuppressWarnings("ClassExplicitlyExtendsThread") class GameSurfaceViewThread extends Thread { private static final int BUG_DELAY = 500; private final int delay; private final SurfaceHolder surfaceHolder; private boolean running; private boolean hasFocus; GameSurfaceViewThread(SurfaceHolder surface) { setName(GameSurfaceViewThread.class.getName()); surfaceHolder = surface; if (Build.BRAND.equalsIgnoreCase(GOOGLE) && Build.MANUFACTURER.equalsIgnoreCase(ASUS) && Build.MODEL.equalsIgnoreCase(NEXUS_7)) { GameLog.w(TAG, "Sleep 500ms (Device: Asus Nexus 7)"); delay = BUG_DELAY; } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR2) { GameLog.w(TAG, "Sleep 500ms (Handle issue 58385 in Android 4.3)"); // delay = BUG_DELAY; } else { delay = 5; } } public void setRunning(boolean run) { running = run; } @SuppressWarnings({"RefusedBequest", "WhileLoopSpinsOnField", "BusyWait"}) @Override public void run() { try { Thread.sleep(delay); } catch (InterruptedException ignored) { // NOOP } Canvas canvas = null; // This is the rendering loop; it goes until asked to quit. while (running) { try { Thread.sleep(5); // CPU timeout - help keep things cool } catch (InterruptedException ignored) { // NOOP } try { canvas = surfaceHolder.lockCanvas(); if (canvas != null) { synchronized (surfaceHolder) { renderer.draw(canvas); } } } finally { if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas); } } } } public synchronized void onWindowFocusChanged(boolean focus) { hasFocus = focus; if (hasFocus) { notifyAll(); } } } // ---------------------------------------------------------------------- /** * Handle Touch Events */ @SuppressWarnings({"NumericCastThatLosesPrecision"}) @Override public boolean onTouchEvent(@NonNull MotionEvent event) { boolean consumed = gesture.onTouchEvent(event); if (consumed) return true; scaleGesture.onTouchEvent(event); // Calculate actual event position in background view Point point = new Point(); renderer.getViewPosition(point); float zoom = renderer.getZoom(); int x = (int) (point.x + (event.getX() * zoom)); int y = (int) (point.y + (event.getY() * zoom)); // Resolve events switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: listener.onTouchDown(x, y); return touch.down(event); case MotionEvent.ACTION_MOVE: long scaleMoveGuard = SCALE_MOVE_GUARD; if (scaleGesture.isInProgress() || ((System.currentTimeMillis() - lastScaleTime) < scaleMoveGuard)) //noinspection BreakStatement break; return touch.move(event); case MotionEvent.ACTION_UP: listener.onTouchUp(x, y); return touch.onTouchUp(event); case MotionEvent.ACTION_CANCEL: return touch.cancel(event); default: break; } return super.onTouchEvent(event); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return touch.fling(e1, e2, velocityX, velocityY); } @Override public boolean onDown(MotionEvent e) { // NOOP return false; } @Override public void onLongPress(MotionEvent e) { // NOOP } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // NOOP return false; } @Override public void onShowPress(MotionEvent e) { // NOOP } @Override public boolean onSingleTapUp(MotionEvent e) { // NOOP return false; } /** * Scale Listener Used to change the scale factor on the GameSurfaceRenderer */ private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { private final PointF screenFocus = new PointF(); @Override public boolean onScale(ScaleGestureDetector detector) { float scaleFactor = detector.getScaleFactor(); if (scaleFactor != 0.0f) { scaleFactor = 1 / scaleFactor; screenFocus.set(detector.getFocusX(), detector.getFocusY()); renderer.zoom( scaleFactor, screenFocus); invalidate(); } lastScaleTime = System.currentTimeMillis(); return true; } } protected enum TouchState { NO_TOUCH, IN_TOUCH, ON_FLING, IN_FLING } class TouchHandler { @NonNls private static final String TOUCH_THREAD = "touchThread"; // Current Touch State private TouchState state = TouchState.NO_TOUCH; // Point initially touched private final Point touchDown = new Point(0, 0); // View Center onTouchDown private final Point viewCenterAtDown = new Point(0, 0); // View Center onFling private final Point viewCenterAtFling = new Point(); // View Center onFling private final Point viewSizeAtFling = new Point(); // View Center onFling private Point backgroundSizeAtFling = new Point(); // Scroller final Scroller scroller; // Thread for handling private TouchHandlerThread touchThread; TouchHandler(Context context) { scroller = new Scroller(context); } void start() { touchThread = new TouchHandlerThread(this); touchThread.setName(TOUCH_THREAD); touchThread.start(); } @SuppressWarnings("AssignmentToNull") void stop() { touchThread.running = false; touchThread.interrupt(); boolean retry = true; while (retry) { try { touchThread.join(); retry = false; } catch (InterruptedException ignored) { // Wait until done } } touchThread = null; } /** * Handle a down event */ @SuppressWarnings({"SameReturnValue", "BooleanMethodNameMustStartWithQuestion", "NumericCastThatLosesPrecision"}) boolean down(MotionEvent event) { // Cancel rendering suspension renderer.resume(); // Get position synchronized (this) { setState(TouchState.IN_TOUCH); touchDown.x = (int) event.getX(); touchDown.y = (int) event.getY(); Point p = new Point(); renderer.getViewPosition(p); viewCenterAtDown.set(p.x, p.y); } return true; } /** * Handle a move event */ @SuppressWarnings({"NumericCastThatLosesPrecision", "BooleanMethodNameMustStartWithQuestion"}) boolean move(MotionEvent event) { if (getState() == TouchState.IN_TOUCH) { float zoom = renderer.getZoom(); float deltaX = (event.getX() - touchDown.x) * zoom; float deltaY = (event.getY() - touchDown.y) * zoom; float newX = viewCenterAtDown.x - deltaX; float newY = viewCenterAtDown.y - deltaY; renderer.setViewPosition((int) newX, (int) newY); invalidate(); return true; } return false; } /** * Handle an onTouchUp event */ @SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "UnusedParameters", "SameReturnValue"}) boolean onTouchUp(MotionEvent event) { if (getState() == TouchState.IN_TOUCH) { setState(TouchState.NO_TOUCH); } return true; } /** * Handle a cancel event */ @SuppressWarnings({"SameReturnValue", "UnusedParameters"}) boolean cancel(MotionEvent event) { if (getState() == TouchState.IN_TOUCH) { setState(TouchState.NO_TOUCH); } return true; } @SuppressWarnings({"UnusedParameters", "BooleanMethodNameMustStartWithQuestion", "NumericCastThatLosesPrecision", "SameReturnValue"}) boolean fling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { renderer.getViewPosition(viewCenterAtFling); renderer.getViewSize(viewSizeAtFling); synchronized (this) { backgroundSizeAtFling = renderer.getBackgroundSize(); setState(TouchState.ON_FLING); renderer.suspend(); scroller.fling( viewCenterAtFling.x, viewCenterAtFling.y, (int) -velocityX, (int) -velocityY, 0, backgroundSizeAtFling.x - viewSizeAtFling.x, 0, backgroundSizeAtFling.y - viewSizeAtFling.y); if (touchThread != null) touchThread.interrupt(); } return true; } /** * Touch Handler Thread */ @SuppressWarnings({"InnerClassTooDeeplyNested", "ClassExplicitlyExtendsThread"}) class TouchHandlerThread extends Thread { private final TouchHandler touchHandler; boolean running; TouchHandlerThread(TouchHandler t) { touchHandler = t; setName(TOUCH_THREAD); } @SuppressWarnings({"MethodWithMultipleLoops", "RefusedBequest", "WhileLoopSpinsOnField", "BusyWait", "OverlyComplexMethod", "OverlyNestedMethod"}) @Override public void run() { running = true; while (running) { while ((touchHandler.getState() != TouchState.ON_FLING) && (touchHandler.getState() != TouchState.IN_FLING)) { try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException ignored) { // NOOP } if (!running) return; } synchronized (touchHandler) { if (touchHandler.getState() == TouchState.ON_FLING) { touchHandler.setState(TouchState.IN_FLING); } } if (touchHandler.getState() == TouchState.IN_FLING) { //noinspection ProhibitedExceptionCaught try { scroller.computeScrollOffset(); renderer.setViewPosition(scroller.getCurrX(), scroller.getCurrY()); if (scroller.isFinished()) { renderer.resume(); synchronized (touchHandler) { touchHandler.setState(TouchState.NO_TOUCH); //noinspection NestedTryStatement try { //noinspection SleepWhileHoldingLock Thread.sleep(5); } catch (InterruptedException ignored) { // NOOP } } } } // Fix for phone error. catch (ArrayIndexOutOfBoundsException e) { GameLog.logException(e); try { //noinspection MagicNumber Thread.sleep(500); } catch (InterruptedException ignored) { // NOOP } } } } } } private synchronized TouchState getState() { return state; } private synchronized void setState(TouchState st) { state = st; } } }