自定义拖动View

本文介绍了一个自定义的Android视图组件ImageSurfaceView,该组件实现了图像缩放和平移功能,通过SurfaceView进行图像绘制,并利用多线程提高用户体验。文章详细解释了各个类的作用及其工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

public class ImageSurfaceView extends SurfaceView implements SurfaceHolder.Callback, OnGestureListener  {
    private final static String TAG = ImageSurfaceView.class.getSimpleName();

    private InputStreamScene scene;
    private final Touch touch;
    private GestureDetector gestureDectector;
    private ScaleGestureDetector scaleGestureDetector;
    private long lastScaleTime = 0;
    private long SCALE_MOVE_GUARD = 500; // milliseconds after scale to ignore move events

    private DrawThread drawThread;

    //region getters and setters
    public void getViewport(Point p){
        scene.getViewport().getOrigin(p);
    }
    
    public void setViewport(Point viewport){
        scene.getViewport().setOrigin(viewport.x, viewport.y);
    }

    public void setViewportCenter() {
        Point viewportSize = new Point();
        Point sceneSize = scene.getSceneSize();
        scene.getViewport().getSize(viewportSize);

        int x = (sceneSize.x - viewportSize.x) / 2;
        int y = (sceneSize.y - viewportSize.y) / 2;
        scene.getViewport().setOrigin(x, y);
    }

    public void setInputStream(InputStream inputStream) throws IOException {
        scene = new InputStreamScene(inputStream);
    }

    //endregion

    //region extends SurfaceView
    @Override
    public boolean onTouchEvent(MotionEvent me) {
        boolean consumed = gestureDectector.onTouchEvent(me);
        if (consumed)
            return true;
        scaleGestureDetector.onTouchEvent(me);
        switch (me.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: return touch.down(me);
            case MotionEvent.ACTION_MOVE:
                if (scaleGestureDetector.isInProgress() || System.currentTimeMillis()-lastScaleTime<SCALE_MOVE_GUARD)
                    break;
                return touch.move(me);
            case MotionEvent.ACTION_UP: return touch.up(me);
            case MotionEvent.ACTION_CANCEL: return touch.cancel(me);
        }
        return super.onTouchEvent(me);
    }
    //endregion

    //region SurfaceHolder.Callback constructors
    public ImageSurfaceView(Context context) {
        super(context);
        touch = new Touch(context);
        init(context);
    }
    
    public ImageSurfaceView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        touch = new Touch(context);
        init(context);
    }

    public ImageSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        touch = new Touch(context);
        init(context);
    }
    
    private void init(Context context){
        gestureDectector = new GestureDetector(context,this);
        getHolder().addCallback(this);
        scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener());
    }
    //endregion

    //region class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener
    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        private PointF screenFocus = new PointF();
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float scaleFactor = detector.getScaleFactor();
            if (scaleFactor!=0f && scaleFactor!=1.0f){
                scaleFactor = 1/scaleFactor;
                screenFocus.set(detector.getFocusX(),detector.getFocusY());
                scene.getViewport().zoom(
                        scaleFactor,
                        screenFocus);
                invalidate();
            }
            lastScaleTime = System.currentTimeMillis();
            return true;
        }
    }

    //endregion


    //region implements SurfaceHolder.Callback
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        scene.getViewport().setSize(width, height);
        Log.d(TAG,String.format("onSizeChanged(w=%d,h=%d)",width,height));
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        drawThread = new DrawThread(holder);
        drawThread.setName("drawThread");
        drawThread.setRunning(true);
        drawThread.start();
        scene.start();
        touch.start();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        touch.stop();
        scene.stop();
        drawThread.setRunning(false);
        boolean retry = true;
        while (retry) {
            try {
                drawThread.join();
                retry = false;
            } catch (InterruptedException e) {
                // we will try it again and again...
            }
        }
    }
    //endregion

    //region implements OnGestureListener
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return touch.fling( e1, e2, velocityX, velocityY);
    }
    //region the rest are defaults
    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {
    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }
    //endregion

    //endregion

    //region class DrawThread

    class DrawThread extends Thread {
        private SurfaceHolder surfaceHolder;

        private boolean running = false;
        public void setRunning(boolean value){ running = value; }
        
        public DrawThread(SurfaceHolder surfaceHolder){
            this.surfaceHolder = surfaceHolder;
        }
        
        @Override
        public void run() {
            Canvas c;
            while (running) {
                try {
                    // Don't hog the entire CPU
                    Thread.sleep(5);
                } catch (InterruptedException e) {}
                c = null;
                try {
                    c = surfaceHolder.lockCanvas();
                    if (c!=null){
                        synchronized (surfaceHolder) {
                            scene.draw(c);// draw it
                        }
                    }
                } finally {
                    if (c != null) {
                        surfaceHolder.unlockCanvasAndPost(c);
                    }
                }
            }        
        }
    }
    //endregion

    //region class Touch

    enum TouchState {UNTOUCHED,IN_TOUCH,START_FLING,IN_FLING};
    class Touch {
        TouchState state = TouchState.UNTOUCHED;
        /** Where on the view did we initially touch */
        final Point viewDown = new Point(0,0);
        /** What was the coordinates of the viewport origin? */
        final Point viewportOriginAtDown = new Point(0,0);
        
        final Scroller scroller;
        
        TouchThread touchThread;
        
        Touch(Context context){
            scroller = new Scroller(context);
        }
        
        void start(){
            touchThread = new TouchThread(this);
            touchThread.setName("touchThread");
            touchThread.start();
        }
        
        void stop(){
            touchThread.running = false;
            touchThread.interrupt();

            boolean retry = true;
            while (retry) {
                try {
                    touchThread.join();
                    retry = false;
                } catch (InterruptedException e) {
                    // we will try it again and again...
                }
            }
            touchThread = null;
        }
        
        Point fling_viewOrigin = new Point();
        Point fling_viewSize = new Point();
        Point fling_sceneSize = new Point();
        boolean fling( MotionEvent e1, MotionEvent e2, float velocityX, float velocityY){
            scene.getViewport().getOrigin(fling_viewOrigin);
            scene.getViewport().getSize(fling_viewSize);
            scene.getSceneSize(fling_sceneSize);

            synchronized(this){
                state = TouchState.START_FLING;
                scene.setSuspend(true);
                scroller.fling(
                    fling_viewOrigin.x,
                    fling_viewOrigin.y,
                    (int)-velocityX,
                    (int)-velocityY,
                    0, 
                    fling_sceneSize.x-fling_viewSize.x, 
                    0,
                    fling_sceneSize.y-fling_viewSize.y);
                touchThread.interrupt();
            }
//            Log.d(TAG,String.format("scroller.fling(%d,%d,%d,%d,%d,%d,%d,%d)",
//                    fling_viewOrigin.x,
//                    fling_viewOrigin.y,
//                    (int)-velocityX,
//                    (int)-velocityY,
//                    0, 
//                    fling_sceneSize.x-fling_viewSize.x,
//                    0,
//                    fling_sceneSize.y-fling_viewSize.y));
            return true;
        }
        boolean down(MotionEvent event){
            scene.setSuspend(false);    // If we were suspended because of a fling
            synchronized(this){
                state = TouchState.IN_TOUCH;
                viewDown.x = (int) event.getX();
                viewDown.y = (int) event.getY();
                Point p = new Point();
                scene.getViewport().getOrigin(p);
                viewportOriginAtDown.set(p.x,p.y);
            }
            return true;
        }
        
        boolean move(MotionEvent event){
            if (state==TouchState.IN_TOUCH){
                float zoom = scene.getViewport().getZoom();
                float deltaX = zoom * ((float)(event.getX()-viewDown.x));
                float deltaY = zoom * ((float)(event.getY()-viewDown.y));
                float newX = ((float)(viewportOriginAtDown.x - deltaX));
                float newY = ((float)(viewportOriginAtDown.y - deltaY));
                
                scene.getViewport().setOrigin((int)newX, (int)newY);
                invalidate();
            }
            return true;
        }
        
        boolean up(MotionEvent event){
            if (state==TouchState.IN_TOUCH){
                state = TouchState.UNTOUCHED;
            }
            return true;
        }
        
        boolean cancel(MotionEvent event){
            if (state==TouchState.IN_TOUCH){
                state = TouchState.UNTOUCHED;
            }
            return true;
        }
        
        class TouchThread extends Thread {
            final Touch touch;
            boolean running = false;
            void setRunning(boolean value){ running = value; }
            
            TouchThread(Touch touch){ this.touch = touch; }
            @Override
            public void run() {
                running=true;
                while(running){
                    while(touch.state!=TouchState.START_FLING && touch.state!=TouchState.IN_FLING){
                        try {
                            Thread.sleep(Integer.MAX_VALUE);
                        } catch (InterruptedException e) {}
                        if (!running)
                            return;
                    }
                    synchronized (touch) {
                        if (touch.state==TouchState.START_FLING){
                            touch.state = TouchState.IN_FLING;
                        }
                    }
                    if (touch.state==TouchState.IN_FLING){
                        scroller.computeScrollOffset();
                        scene.getViewport().setOrigin(scroller.getCurrX(), scroller.getCurrY());
                        if (scroller.isFinished()){
                            scene.setSuspend(false);
                            synchronized (touch) {
                                touch.state = TouchState.UNTOUCHED;
                                try{
                                    Thread.sleep(5);
                                } catch (InterruptedException e) {}
                            }
                        }
                    }
                }
            }
        }
    }
    //endregion

}



public class InputStreamScene extends Scene {
    private static final String TAG=InputStreamScene.class.getSimpleName();
    
    private static final boolean DEBUG = false;
    private static final BitmapFactory.Options options = new BitmapFactory.Options();

    /** What is the downsample size for the sample image?  1=1/2, 2=1/4 3=1/8, etc */
    private static final int DOWN_SAMPLE_SHIFT = 2;

    /** How many bytes does one pixel use? */
    private final int BYTES_PER_PIXEL = 4;

    /** What percent of total memory should we use for the cache? The bigger the cache,
     * the longer it takes to read -- 1.2 secs for 25%, 600ms for 10%, 500ms for 5%.
     * User experience seems to be best for smaller values. 
     */
    private int percent = 5; // Above 25 and we get OOMs

    private BitmapRegionDecoder decoder;
    private Bitmap sampleBitmap;

    static {
        options.inPreferredConfig = Bitmap.Config.RGB_565;
    }

    public InputStreamScene(InputStream inputStream) throws IOException {
        BitmapFactory.Options tmpOptions = new BitmapFactory.Options();

        this.decoder = BitmapRegionDecoder.newInstance(inputStream, false);

        // Grab the bounds for the scene dimensions
        tmpOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, tmpOptions);
        setSceneSize(tmpOptions.outWidth, tmpOptions.outHeight);

        // Create the sample image
        tmpOptions.inJustDecodeBounds = false;
        tmpOptions.inSampleSize = (1<< DOWN_SAMPLE_SHIFT);
        sampleBitmap = BitmapFactory.decodeStream(inputStream, null, tmpOptions);

        initialize();
    }

    @Override
    protected Bitmap fillCache(Rect origin) {
        Bitmap bitmap = null;
        if (decoder!=null)
            bitmap = decoder.decodeRegion( origin, options );
        return bitmap;
    }

    private static Paint red = new Paint();
    static{
        red.setColor(Color.RED);
        red.setStrokeWidth(5L);
    }
    @Override
    protected void drawSampleRectIntoBitmap(Bitmap bitmap, Rect rectOfSample) {
        if (bitmap!=null){
            Canvas c = new Canvas(bitmap);
            int left   = (rectOfSample.left>> DOWN_SAMPLE_SHIFT);
            int top    = (rectOfSample.top>> DOWN_SAMPLE_SHIFT);
            int right  = left + (rectOfSample.width()>> DOWN_SAMPLE_SHIFT);
            int bottom = top + (rectOfSample.height()>> DOWN_SAMPLE_SHIFT);
            Rect srcRect = new Rect( left, top, right, bottom );
            Rect identity= new Rect(0,0,c.getWidth(),c.getHeight());
            c.drawBitmap(
                sampleBitmap,
                srcRect,
                identity,
                null
                );
//            c.drawLine(0L,0L,c.getWidth(),c.getHeight(),red);
        }
    }

//    @Override
//    protected Rect calculateCacheWindow(Rect viewportRect) {
//        // Simplest implementation
//        return viewportRect;
//    }

    private Rect calculatedCacheWindowRect = new Rect();
    @Override
    protected Rect calculateCacheWindow(Rect viewportRect) {
        long bytesToUse = Runtime.getRuntime().maxMemory() * percent / 100;
        Point size = getSceneSize();

        int vw = viewportRect.width();
        int vh = viewportRect.height();
        
        // Calculate the max size of the margins to fit in our memory budget
        int tw=0;
        int th=0;
        int mw = tw;
        int mh = th;
        while((vw+tw) * (vh+th) * BYTES_PER_PIXEL < bytesToUse){
            mw = tw++;
            mh = th++;
        }
        
        // Trim the margins if they're too big.
        if (vw+mw > size.x) // viewport width + margin width > width of the image
            mw = Math.max(0, size.x-vw);
        if (vh+mh > size.y) // viewport height + margin height > height of the image
            mh = Math.max(0, size.y-vh);
        
        // Figure out the left & right based on the margin. We assume our viewportRect
        // is <= our size. If that's not the case, then this logic breaks.
        int left = viewportRect.left - (mw>>1);
        int right = viewportRect.right + (mw>>1);
        if (left<0){
            right = right - left; // Add's the overage on the left side back to the right
            left = 0;
        }
        if (right>size.x){
            left = left - (right-size.x); // Adds overage on right side back to left
            right = size.x;
        }

        // Figure out the top & bottom based on the margin. We assume our viewportRect
        // is <= our size. If that's not the case, then this logic breaks.
        int top = viewportRect.top - (mh>>1); 
        int bottom = viewportRect.bottom + (mh>>1);
        if (top<0){
            bottom = bottom - top; // Add's the overage on the top back to the bottom
            top = 0;
        }
        if (bottom>size.y){
            top = top - (bottom-size.y); // Adds overage on bottom back to top
            bottom = size.y;
        }
        
        // Set the origin based on our new calculated values.
        calculatedCacheWindowRect.set(left, top, right, bottom);
        if (DEBUG) Log.d(TAG,"new cache.originRect = "+calculatedCacheWindowRect.toShortString()+" size="+size.toString());
        return calculatedCacheWindowRect;
    }

    @Override
    protected void fillCacheOutOfMemoryError(OutOfMemoryError error) {
        if (percent>0)
            percent -= 1;
        Log.e(TAG,String.format("caught oom -- cache now at %d percent.",percent));
    }

    @Override
    protected void drawComplete(Canvas canvas) {
        // TODO Auto-generated method stub
        
    }
}

public abstract class Scene {
    private final String TAG = "Scene";

    private final static int MINIMUM_PIXELS_IN_VIEW = 50;

    /** The size of the Scene */
    private Point size = new Point();
    /** The viewport */
    private final Viewport viewport = new Viewport();
    /** The cache */
    private final Cache cache = new Cache();
    
    //region [gs]etSceneSize
    /** Set the size of the scene */
    public void setSceneSize(int width, int height){
        size.set(width, height);
    }
    /** Returns a Point representing the size of the scene. Don't modify the returned Point! */
    public Point getSceneSize(){
        return size;
    }
    /** Set the passed-in point to the size of the scene */
    public void getSceneSize(Point point){
        point.set(size.x, size.y);
    }
    //endregion

    //region getViewport()
    public Viewport getViewport(){return viewport;}
    //endregion

    //region initialize/start/stop/suspend/invalidate the cache
    /** Initializes the cache */
    public void initialize(){
        if (cache.getState()==CacheState.UNINITIALIZED){
            synchronized(cache){
                cache.setState(CacheState.INITIALIZED);
            }
        }
    }
    /** Starts the cache thread */
    public void start(){
        cache.start();
    }
    /** Stops the cache thread */
    public void stop(){
        cache.stop();
    }
    /** 
     * Suspends or unsuspends the cache thread. This can be
     * used to temporarily stop the cache from updating
     * during a fling event.
     * @param suspend True to suspend the cache. False to unsuspend.
     */
    public void setSuspend(boolean suspend){
        if (suspend) {
            synchronized(cache){
                cache.setState(CacheState.SUSPEND);
            }
        } else {
            if (cache.getState()==CacheState.SUSPEND) {
                synchronized(cache){
                    cache.setState(CacheState.INITIALIZED);
                }
            }
        }
    }
    /** Invalidate the cache. This causes it to refill */
    @SuppressWarnings("unused")
    public void invalidate(){
        cache.invalidate();
    }
    //endregion

    //region void draw(Canvas c)
    /**
     * Draw the scene to the canvas. This operation fills the canvas with
     * the bitmap referenced by the viewport's location within the Scene.
     * If the cache already has the data (and is not suspended), then the
     * high resolution bitmap from the cache is used. If it's not available,
     * then the lower resolution bitmap from the sample is used.
     */
    public void draw(Canvas c){
        viewport.draw(c);
    }
    //endregion

    //region protected abstract
    /**
     * This method must return a high resolution Bitmap that the Scene 
     * will use to fill out the viewport bitmap upon request. This bitmap
     * is normally larger than the viewport so that the viewport can be
     * scrolled without having to refresh the cache. This method runs
     * on a thread other than the UI thread, and it is not under a lock, so
     * it is expected that this method can run for a long time (seconds?). 
     * @param rectOfCache The Rect representing the area of the Scene that
     * the Scene wants cached.
     * @return the Bitmap representing the requested area of the larger bitmap
     */
    protected abstract Bitmap fillCache(Rect rectOfCache);
    /**
     * The memory allocation you just did in fillCache caused an OutOfMemoryError.
     * You can attempt to recover. Experience shows that when we get an 
     * OutOfMemoryError, we're pretty hosed and are going down. For instance, if
     * we're trying to decode a bitmap region with
     * {@link android.graphics.BitmapRegionDecoder} and we run out of memory, 
     * we're going to die somewhere in the C code with a SIGSEGV. 
     * @param error The OutOfMemoryError exception data
     */
    protected abstract void fillCacheOutOfMemoryError( OutOfMemoryError error );
    /**
     * Calculate the Rect of the cache's window based on the current viewportRect.
     * The returned Rect must at least contain the viewportRect, but it can be
     * larger if the system believes a bitmap of the returned size will fit into
     * memory. This function must be fast as it happens while the cache lock is held.
     * @param viewportRect The returned must be able to contain this Rect
     * @return The Rect that will be used to fill the cache
     */
    protected abstract Rect calculateCacheWindow(Rect viewportRect);
    /**
     * This method fills the passed-in bitmap with sample data. This function must
     * return as fast as possible so it shouldn't have to do any IO at all -- the
     * quality of the user experience rests on the speed of this function.
     * @param bitmap The Bitmap to fill
     * @param rectOfSample Rectangle within the Scene that this bitmap represents.
     */
    protected abstract void drawSampleRectIntoBitmap(Bitmap bitmap, Rect rectOfSample);
    /**
     * The Cache is done drawing the bitmap -- time to add the finishing touches
     * @param canvas a canvas on which to draw
     */
    protected abstract void drawComplete(Canvas canvas);
    //endregion

    //region class Viewport

    public class Viewport {
        /** The bitmap of the current viewport */
        Bitmap bitmap = null;
        /** A Rect that defines where the Viewport is within the scene */
        final Rect window = new Rect(0,0,0,0);
        float zoom = 1.0f;

        public void setOrigin(int x, int y){
            synchronized(this){
                int w = window.width();
                int h = window.height();
    
                // check bounds
                if (x < 0)
                    x = 0;
    
                if (y < 0)
                    y = 0;
    
                if (x + w > size.x)
                    x = size.x - w;
    
                if (y + h > size.y)
                    y = size.y - h;
    
                window.set(x, y, x+w, y+h);
            }
        }
        public void setSize( int w, int h ){
            synchronized (this) {
                if (bitmap !=null){
                    bitmap.recycle();
                    bitmap = null;
                }
                bitmap = Bitmap.createBitmap(w, h, Config.RGB_565);
                window.set(
                        window.left,
                        window.top,
                        window.left + w,
                        window.top + h);
            }
        }
        public void getOrigin(Point p){
            synchronized (this) {
                p.set(window.left, window.top);
            }
        }
        public void getSize(Point p){
            synchronized (this) {
                p.x = window.width();
                p.y = window.height();
            }
        }
        public void getPhysicalSize(Point p){
            synchronized (this){
                p.x = getPhysicalWidth();
                p.y = getPhysicalHeight();
            }
        }
        public int getPhysicalWidth(){
            return bitmap.getWidth();
        }
        public int getPhysicalHeight(){
            return bitmap.getHeight();
        }
        public float getZoom(){
            return zoom;
        }
        public void zoom(float factor, PointF screenFocus){
            if (factor!=1.0){

                PointF screenSize = new PointF(bitmap.getWidth(),bitmap.getHeight());
                PointF sceneSize = new PointF(getSceneSize());
                float screenWidthToHeight = screenSize.x / screenSize.y;
                float screenHeightToWidth = screenSize.y / screenSize.x;
                synchronized (this){
                    float newZoom = zoom * factor;
                    RectF w1 = new RectF(window);
                    RectF w2 = new RectF();
                    PointF sceneFocus = new PointF(
                            w1.left + (screenFocus.x/screenSize.x)*w1.width(),
                            w1.top + (screenFocus.y/screenSize.y)*w1.height()
                    );
                    float w2Width = getPhysicalWidth() * newZoom;
                    if (w2Width > sceneSize.x){
                        w2Width = sceneSize.x;
                        newZoom = w2Width / getPhysicalWidth();
                    }
                    if (w2Width < MINIMUM_PIXELS_IN_VIEW){
                        w2Width = MINIMUM_PIXELS_IN_VIEW;
                        newZoom = w2Width / getPhysicalWidth();
                    }
                    float w2Height = w2Width * screenHeightToWidth;
                    if (w2Height > sceneSize.y){
                        w2Height = sceneSize.y;
                        w2Width = w2Height * screenWidthToHeight;
                        newZoom = w2Width / getPhysicalWidth();
                    }
                    if (w2Height < MINIMUM_PIXELS_IN_VIEW){
                        w2Height = MINIMUM_PIXELS_IN_VIEW;
                        w2Width = w2Height * screenWidthToHeight;
                        newZoom = w2Width / getPhysicalWidth();
                    }
                    w2.left = sceneFocus.x - ((screenFocus.x/screenSize.x) * w2Width);
                    w2.top = sceneFocus.y - ((screenFocus.y/screenSize.y) * w2Height);
                    if (w2.left<0)
                        w2.left=0;
                    if (w2.top<0)
                        w2.top=0;
                    w2.right = w2.left+w2Width;
                    w2.bottom= w2.top+w2Height;
                    if (w2.right>sceneSize.x){
                        w2.right=sceneSize.x;
                        w2.left=w2.right-w2Width;
                    }
                    if (w2.bottom>sceneSize.y){
                        w2.bottom=sceneSize.y;
                        w2.top=w2.bottom-w2Height;
                    }
                    window.set((int)w2.left,(int)w2.top,(int)w2.right,(int)w2.bottom);
                    zoom = newZoom;
//                    Log.d(TAG,String.format(
//                            "f=%.2f, z=%.2f, scrf(%.0f,%.0f), scnf(%.0f,%.0f) w1s(%.0f,%.0f) w2s(%.0f,%.0f) w1(%.0f,%.0f,%.0f,%.0f) w2(%.0f,%.0f,%.0f,%.0f)",
//                            factor,
//                            zoom,
//                            screenFocus.x,
//                            screenFocus.y,
//                            sceneFocus.x,
//                            sceneFocus.y,
//                            w1.width(),w1.height(),
//                            w2Width, w2Height,
//                            w1.left,w1.top,w1.right,w1.bottom,
//                            w2.left,w2.top,w2.right,w2.bottom
//                            ));
                }
            }
        }
        void draw(Canvas c){
            cache.update(this);
            synchronized (this){
                if (c!=null && bitmap !=null){
                    c.drawBitmap(bitmap, 0F, 0F, null);
                    drawComplete(c);
                }
            }
        }
    }
    //endregion

    //region class Cache

    private enum CacheState {UNINITIALIZED,INITIALIZED,START_UPDATE,IN_UPDATE,READY,SUSPEND}
    /**
     * Keep track of the cached bitmap
     */
    private class Cache {
        /** A Rect that defines where the Cache is within the scene */
        final Rect window = new Rect(0,0,0,0);
        /** The bitmap of the current cache */
        Bitmap bitmapRef = null;
        CacheState state = CacheState.UNINITIALIZED;

        void setState(CacheState newState){
            if (Debug.isDebuggerConnected())
                Log.i(TAG,String.format("cacheState old=%s new=%s",state.toString(),newState.toString()));
            state = newState;
        }
        CacheState getState(){ return state; }
        
        /** Our load from disk thread */
        CacheThread cacheThread;
        
        void start(){
            if (cacheThread!=null){
                cacheThread.setRunning(false);
                cacheThread.interrupt();
                cacheThread = null;
            }
            cacheThread = new CacheThread(this);
            cacheThread.setName("cacheThread");
            cacheThread.start();
        }
        
        void stop(){
            cacheThread.running = false;
            cacheThread.interrupt();

            boolean retry = true;
            while (retry) {
                try {
                    cacheThread.join();
                    retry = false;
                } catch (InterruptedException e) {
                    // we will try it again and again...
                }
            }
            cacheThread = null;
        }
        void invalidate(){
            synchronized(this){
                setState(CacheState.INITIALIZED);
                cacheThread.interrupt();
            }
        }
        
        /** Fill the bitmap with the part of the scene referenced by the viewport Rect */
        void update(Viewport viewport){
            Bitmap bitmap = null;    // If this is null at the bottom, then load from the sample
            synchronized(this){
                switch(getState()){
                case UNINITIALIZED:
                    // nothing can be done -- should never get here
                    return;
                case INITIALIZED:
                    // time to cache some data
                    setState(CacheState.START_UPDATE);
                    cacheThread.interrupt();
                    break;
                case START_UPDATE:
                    // I already told the thread to start
                    break;
                case IN_UPDATE:
                    // Already reading some data, just use the sample 
                    break;
                case SUSPEND:
                    // Loading from cache suspended.
                    break;
                case READY:
                    // I have some data to show
                    if (bitmapRef==null){
                        // Start the cache off right
                        if (Debug.isDebuggerConnected())
                            Log.d(TAG,"bitmapRef is null");
                        setState(CacheState.START_UPDATE);
                        cacheThread.interrupt();
                    } else if (!window.contains(viewport.window)){
                        if (Debug.isDebuggerConnected())
                            Log.d(TAG,"viewport not in cache");
                        setState(CacheState.START_UPDATE);
                        cacheThread.interrupt();
                    } else {
                        // Happy case -- the cache already contains the Viewport
                        bitmap = bitmapRef;
                    }
                    break;
                }
            }
            if (bitmap==null)
                loadSampleIntoViewport();
            else
                loadBitmapIntoViewport(bitmap);
        }
        
        void loadBitmapIntoViewport(Bitmap bitmap){
            if (bitmap!=null){
                synchronized(viewport){
                    int left   = viewport.window.left - window.left;
                    int top    = viewport.window.top  - window.top;
                    int right  = left + viewport.window.width();
                    int bottom = top  + viewport.window.height();
                    viewport.getPhysicalSize(dstSize);
                    srcRect.set( left, top, right, bottom );
                    dstRect.set(0, 0, dstSize.x, dstSize.y);
                    Canvas c = new Canvas(viewport.bitmap);
                    c.drawColor(Color.BLACK);
                    c.drawBitmap(
                            bitmap,
                            srcRect,
                            dstRect,
                            null);
//                    try {
//                        FileOutputStream fos = new FileOutputStream("/sdcard/viewport.png");
//                        viewport.bitmap.compress(Bitmap.CompressFormat.PNG, 99, fos);
//                        Thread.sleep(1000);
//                    } catch  (Exception e){
//                        System.out.print(e.getMessage());
//                    }
                }
            }
        }
        final Rect srcRect = new Rect(0,0,0,0);
        final Rect dstRect = new Rect(0,0,0,0);
        final Point dstSize = new Point();
        
        void loadSampleIntoViewport(){
            if (getState()!=CacheState.UNINITIALIZED){
                synchronized(viewport){
                    drawSampleRectIntoBitmap(
                        viewport.bitmap,
                        viewport.window
                        );
                }
            }
        }
    }
    //endregion

    //region class CacheThread
    /**
     * <p>The CacheThread's job is to wait until the {@link Cache#state} is 
     * {@link CacheState#START_UPDATE} and then update the {@link Cache} given
     * the current {@link Viewport#window}. It does not want to hold the cache
     * lock during the call to {@link Scene#fillCache(Rect)} because the call 
     * can take a long time. If we hold the lock, the user experience is very 
     * jumpy.</p>
     * <p>The CacheThread and the {@link Cache} work hand in hand, both using the 
     * cache itself to synchronize on and using the {@link Cache#state}. 
     * The {@link Cache} is free to update any part of the cache object as long 
     * as it holds the lock. The CacheThread is careful to make sure that it is
     * the {@link Cache#state} is {@link CacheState#IN_UPDATE} as it updates
     * the {@link Cache}. It locks and unlocks the cache all along the way, but
     * makes sure that the cache is not locked when it calls 
     * {@link Scene#fillCache(Rect)}. 
     */
    class CacheThread extends Thread {
        final Cache cache;
        boolean running = false;
        void setRunning(boolean value){ running = value; }
        
        CacheThread(Cache cache){ this.cache = cache; }
        
        @Override
        public void run() {
            running=true;
            Rect viewportRect = new Rect(0,0,0,0);
            while(running){
                while(running && cache.getState()!=CacheState.START_UPDATE)
                    try {
                        // Sleep until we have something to do
                        Thread.sleep(Integer.MAX_VALUE);
                    } catch (InterruptedException ignored) {}
                if (!running)
                    return;
                long start = System.currentTimeMillis();
                boolean cont = false;
                synchronized (cache) {
                    if (cache.getState()==CacheState.START_UPDATE){
                        cache.setState(CacheState.IN_UPDATE);
                        cache.bitmapRef = null;
                        cont = true;
                    }
                }
                if (cont){
                    synchronized(viewport){
                        viewportRect.set(viewport.window);
                    }
                    synchronized (cache) {
                        if (cache.getState()==CacheState.IN_UPDATE)
                            //cache.setWindowRect(viewportRect);
                            cache.window.set(calculateCacheWindow(viewportRect));
                        else
                            cont = false;
                    }
                    if (cont){
                        try{
                            Bitmap bitmap = fillCache(cache.window);
                            if (bitmap!=null){
                                synchronized (cache){
                                    if (cache.getState()==CacheState.IN_UPDATE){
                                        cache.bitmapRef = bitmap;
                                        cache.setState(CacheState.READY);
                                    } else {
                                        Log.w(TAG,"fillCache operation aborted");
                                    }
                                }
                            }
                            long done = System.currentTimeMillis();
                            if (Debug.isDebuggerConnected())
                                Log.d(TAG,String.format("fillCache in %dms",done-start));
                        } catch (OutOfMemoryError e){
                                    Log.d(TAG,"CacheThread out of memory");
                            /*
                             *  Attempt to recover. Experience shows that if we
                             *  do get an OutOfMemoryError, we're pretty hosed and are going down.
                             */
                            synchronized (cache){
                                fillCacheOutOfMemoryError(e);
                                if (cache.getState()==CacheState.IN_UPDATE){
                                    cache.setState(CacheState.START_UPDATE);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    //endregion
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值