Android Open Source - view_cache_demo_android Draw View






From Project

Back to project page view_cache_demo_android.

License

The source code is released under:

Apache License

If you think the Android project view_cache_demo_android listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package nz.gen.geek_central.view_cache_demo;
/*//from   w  w  w. ja va2 s . co  m
    Demonstration of how to do smooth scrolling of a complex image by
    careful caching of bitmaps--actual view caching. This view also
    allows custom handling of long-tap and double-tap events.

    Copyright 2011, 2012 by Lawrence D'Oliveiro <ldo@geek-central.gen.nz>.

    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.
*/

import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Bitmap;
import android.view.MotionEvent;
import nz.gen.geek_central.android.useful.BundledSavedState;
import static nz.gen.geek_central.android.useful.Useful.GetTime;

public class DrawView extends android.view.View
  {
    public interface ContextMenuAction
      {
        public void CreateContextMenu
          (
            android.view.ContextMenu TheMenu,
            PointF MouseDown /* in view coords, can use ViewToDraw (below) to get draw coords */
          );
      } /*ContextMenuAction*/

    protected android.content.Context Context;
    protected float ZoomFactor;
    protected PointF ScrollOffset;
      /* range along each axis is [0.0 .. 1.0], such that 0
        corresponds to left/top edge of image being at left/top edge
        of view, 0.5 corresponds to middle of image being in middle of
        view, and 1 corresponds to right/bottom edge of image being at
        right/bottom edge of view */
    protected final float MaxZoomFactor = 32.0f;
    protected final float MinZoomFactor = 1.0f;

    protected Drawer DrawWhat;
    protected boolean UseCaching = true;
    public interface OnTapListener
      {
        public void OnTap
          (
            DrawView TheView,
            PointF Where
          );
      }
    protected OnTapListener
        OnSingleTap = null,
        OnDoubleTap = null;

    private android.os.Vibrator Vibrate;

    protected void Init
      (
        android.content.Context Context
      )
      /* common code for all constructors */
      {
        this.Context = Context;
        ScrollOffset = new PointF(0.5f, 0.5f);
        ZoomFactor = 1.0f;
        setHorizontalFadingEdgeEnabled(true);
        setVerticalFadingEdgeEnabled(true);
        Vibrate =
            (android.os.Vibrator)Context.getSystemService(android.content.Context.VIBRATOR_SERVICE);
      } /*Init*/

    public DrawView
      (
        android.content.Context Context
      )
      {
        super(Context);
        Init(Context);
      } /*DrawView*/

    public DrawView
      (
        android.content.Context Context,
        android.util.AttributeSet Attributes
      )
      {
        this(Context, Attributes, 0);
      } /*DrawView*/

    public DrawView
      (
        android.content.Context Context,
        android.util.AttributeSet Attributes,
        int DefaultStyle
      )
      {
        super(Context, Attributes, DefaultStyle);
        Init(Context);
      } /*DrawView*/

    public void SetContextMenuAction
      (
        ContextMenuAction TheAction
      )
      {
        DoContextMenu = TheAction;
      } /*SetContextMenuAction*/

/*
    Mapping between image coordinates and view coordinates
*/

    public static PointF MapRect
      (
        PointF Pt,
        RectF Src,
        RectF Dst
      )
      /* maps Pt from Src coordinates to Dst coordinates. */
      {
        return
            new PointF
              (
                        (Pt.x - Src.left)
                    /
                        (Src.right - Src.left)
                    *
                        (Dst.right - Dst.left)
                +
                    Dst.left,
                        (Pt.y - Src.top)
                    /
                        (Src.bottom - Src.top)
                    *
                        (Dst.bottom - Dst.top)
                +
                    Dst.top
              );
      } /*MapRect*/

    public PointF GetViewSize
      (
        float ZoomFactor
      )
      /* gets the image bounds in view coordinates, adjusted for the specified
        zoom setting. */
      {
        final RectF DrawBounds = DrawWhat.GetBounds();
        final PointF ViewSize = new PointF(getWidth() * ZoomFactor, getHeight() * ZoomFactor);
        if
          (
                ViewSize.x / ViewSize.y
            >
                (DrawBounds.right - DrawBounds.left) / (DrawBounds.bottom - DrawBounds.top)
          )
          {
          /* leave unused margin at right of too-wide view */
            ViewSize.x =
                    ViewSize.y
                *
                    (DrawBounds.right - DrawBounds.left)
                /
                    (DrawBounds.bottom - DrawBounds.top);
          }
        else
          {
          /* leave unused margin at bottom of too-tall view, if any */
            ViewSize.y =
                    ViewSize.x
                *
                    (DrawBounds.bottom - DrawBounds.top)
                /
                    (DrawBounds.right - DrawBounds.left);
          } /*if*/
        return
            ViewSize;
      } /*GetViewSize*/

    public PointF GetViewSize()
      /* gets the image bounds in view coordinates, adjusted for the current
        zoom setting. */
      {
        return
            GetViewSize(ZoomFactor);
      } /*GetViewSize*/

    public RectF GetViewBounds
      (
        float ZoomFactor
      )
      {
        final PointF ViewSize = GetViewSize(ZoomFactor);
        return
            new RectF(0.0f, 0.0f, ViewSize.x, ViewSize.y);
      } /*GetViewBounds*/

    public RectF GetScrolledViewBounds
      (
        PointF ScrollOffset,
        float ZoomFactor
      )
      /* gets the image bounds in view coordinates, adjusted for the specified
        scroll offset and zoom setting. */
      {
        final PointF ViewSize = GetViewSize(ZoomFactor);
        final RectF ViewBounds = GetViewBounds(ZoomFactor);
        ViewBounds.offset
          (
            /*dx =*/ Math.min(ScrollOffset.x * (getWidth() - ViewSize.x), 0.0f),
            /*dy =*/ Math.min(ScrollOffset.y * (getHeight() - ViewSize.y), 0.0f)
          );
        return
            ViewBounds;
      } /*GetScrolledViewBounds*/

    public RectF GetScrolledViewBounds()
      /* gets the image bounds in view coordinates, adjusted for the current
        scroll offset and zoom setting. */
      {
        return
            GetScrolledViewBounds(ScrollOffset, ZoomFactor);
      } /*GetScrolledViewBounds*/

    public PointF DrawToView
      (
        PointF DrawCoords,
        PointF ScrollOffset,
        float ZoomFactor
      )
      /* maps DrawCoords to view coordinates at the specified scroll offset
        and zoom setting. */
      {
        return
            MapRect
              (
                /*Pt =*/ DrawCoords,
                /*Src =*/ DrawWhat.GetBounds(),
                /*Dst =*/ GetScrolledViewBounds(ScrollOffset, ZoomFactor)
              );
      } /*DrawToView*/

    public PointF DrawToView
      (
        PointF DrawCoords
      )
      /* maps DrawCoords to view coordinates at the current scroll offset
        and zoom setting. */
      {
        return
            MapRect(DrawCoords, DrawWhat.GetBounds(), GetScrolledViewBounds());
      } /*DrawToView*/

    public PointF ViewToDraw
      (
        PointF ViewCoords,
        PointF ScrollOffset,
        float ZoomFactor
      )
      /* maps ViewCoords to draw coordinates at the specified scroll offset
        and zoom setting. */
      {
        return
            MapRect
              (
                /*Pt =*/ ViewCoords,
                /*Src =*/ GetScrolledViewBounds(ScrollOffset, ZoomFactor),
                /*Dst =*/ DrawWhat.GetBounds()
              );
      } /*ViewToDraw*/

    public PointF ViewToDraw
      (
        PointF ViewCoords
      )
      /* maps ViewCoords to draw coordinates at the current scroll offset
        and zoom setting. */
      {
        return
            ViewToDraw(ViewCoords, ScrollOffset, ZoomFactor);
      } /*ViewToDraw*/

    public PointF FindScrollOffset
      (
        PointF DrawCoords,
        PointF ViewCoords,
        float ZoomFactor
      )
      /* computes the necessary scroll offset so DrawCoords maps to ViewCoords
        at the specified zoom setting. */
      {
        final RectF DrawBounds = DrawWhat.GetBounds();
        final PointF ViewSize = GetViewSize(ZoomFactor);
        return
            new PointF
              (
                /*x =*/
                    ViewSize.x != getWidth() ?
                        Math.max
                          (
                            0.0f,
                            Math.min
                              (
                                    (
                                            (DrawCoords.x - DrawBounds.left)
                                        /
                                            (DrawBounds.right - DrawBounds.left)
                                        *
                                            ViewSize.x
                                    -
                                        ViewCoords.x
                                    )
                                /
                                    (ViewSize.x - getWidth()),
                                1.0f
                              )
                          )
                    :
                        0.5f,
                /*y =*/
                    ViewSize.y != getHeight() ?
                        Math.max
                          (
                            0.0f,
                            Math.min
                              (
                                    (
                                            (DrawCoords.y - DrawBounds.top)
                                        /
                                            (DrawBounds.bottom - DrawBounds.top)
                                        *
                                            ViewSize.y
                                    -
                                        ViewCoords.y
                                    )
                                /
                                    (ViewSize.y - getHeight()),
                                1.0f
                              )
                          )
                    :
                        0.5f
              );
      } /*FindScrollOffset*/

/*
    View cache management & drawing
*/

    protected static class ViewCacheBits
      /* state of the view cache */
      {
        public final Bitmap Bits; /* cached part of image */
        public final RectF Bounds;
          /* such that (0, 0) maps to (DrawWhat.GetBounds().left, DrawWhat.GetBounds().top)
            but scaled to view bounds at current zoom */

        public ViewCacheBits
          (
            Bitmap Bits,
            RectF Bounds
          )
          {
            this.Bits = Bits;
            this.Bounds = Bounds;
          } /*ViewCacheBits*/

      } /*ViewCacheBits*/

    protected final float MaxCacheFactor = 2.0f;
      /* how far to cache beyond visible bounds, relative to the bounds */
    protected ViewCacheBits ViewCache = null;
    protected ViewCacheBuilder BuildViewCache = null;

    protected class ViewCacheBuilder extends android.os.AsyncTask<Void, Integer, ViewCacheBits>
      /* background rebuilding of the view cache */
      {
        protected PointF ViewSize;
        protected RectF CacheBounds;

        protected void onPreExecute()
          {
            ViewSize = GetViewSize();
            final RectF ViewBounds = GetScrolledViewBounds();
            final PointF ViewCenter = new PointF(getWidth() / 2.0f, getHeight() / 2.0f);
            CacheBounds = new RectF
              (
                /*left =*/
                    Math.max
                      (
                        ViewCenter.x - getWidth() * MaxCacheFactor / 2.0f - ViewBounds.left,
                        0.0f
                      ),
                /*top =*/
                    Math.max
                      (
                        ViewCenter.y - getHeight() * MaxCacheFactor / 2.0f - ViewBounds.top,
                        0.0f
                      ),
                /*right =*/
                    Math.min
                      (
                        ViewCenter.x + getWidth() * MaxCacheFactor / 2.0f - ViewBounds.left,
                        ViewBounds.right - ViewBounds.left
                      ),
                /*bottom =*/
                    Math.min
                      (
                        ViewCenter.y + getHeight() * MaxCacheFactor / 2.0f - ViewBounds.top,
                        ViewBounds.bottom - ViewBounds.top
                      )
              );
            if (CacheBounds.isEmpty())
              {
              /* can seem to happen, e.g. on orientation change */
                BuildViewCache = null;
                cancel(true);
              } /*if*/
          } /*onPreExecute*/

        protected ViewCacheBits doInBackground
          (
            Void... Unused
          )
          {
            final Bitmap CacheBits =
                Bitmap.createBitmap
                  (
                    /*width =*/ (int)(CacheBounds.right - CacheBounds.left),
                    /*height =*/ (int)(CacheBounds.bottom - CacheBounds.top),
                    /*config =*/ Bitmap.Config.ARGB_8888
                  );
            final android.graphics.Canvas CacheDraw = new android.graphics.Canvas(CacheBits);
            final RectF DestRect = new RectF(0.0f, 0.0f, ViewSize.x, ViewSize.y);
            DestRect.offset(- CacheBounds.left, - CacheBounds.top);
            DrawWhat.Draw(CacheDraw, DestRect); /* this is the time-consuming part */
            CacheBits.prepareToDraw();
            return
                new ViewCacheBits(CacheBits, CacheBounds);
          } /*doInBackground*/

        protected void onCancelled
          (
            ViewCacheBits Result
          )
          {
            Result.Bits.recycle();
          } /*onCancelled*/

        protected void onPostExecute
          (
            ViewCacheBits Result
          )
          {
            DisposeViewCache();
            ViewCache = Result;
            BuildViewCache = null;
            invalidate();
          } /*onPostExecute*/

      } /*ViewCacheBuilder*/

    protected void DisposeViewCache()
      {
        if (ViewCache != null)
          {
            ViewCache.Bits.recycle();
            ViewCache = null;
          } /*if*/
      } /*DisposeViewCache*/

    protected void CancelViewCacheBuild()
      {
        if (BuildViewCache != null)
          {
            BuildViewCache.cancel
              (
                false
                  /* not true to allow onCancelled to recycle bitmap */
              );
            BuildViewCache = null;
          } /*if*/
      } /*CancelViewCacheBuild*/

    public void ForgetViewCache()
      {
        CancelViewCacheBuild();
        DisposeViewCache();
      } /*ForgetViewCache*/

    protected void RebuildViewCache()
      {
        CancelViewCacheBuild();
        BuildViewCache = new ViewCacheBuilder();
        BuildViewCache.execute((Void)null);
      } /*RebuildViewCache*/

    protected void RecenterViewCache()
      /* regenerates the cache as necessary to ensure it completely covers
        currently-visible view. */
      {
        if
          (
                UseCaching
            &&
                BuildViewCache == null
            &&
                ViewCache != null
          )
          {
            final RectF ViewBounds = GetScrolledViewBounds();
            final RectF DestRect = new RectF(ViewCache.Bounds);
            DestRect.offset(ViewBounds.left, ViewBounds.top);
            if
              (
                    DestRect.left > Math.max(0, ViewBounds.left)
                ||
                    DestRect.top > Math.max(0, ViewBounds.top)
                ||
                    DestRect.right < Math.min(getWidth(), ViewBounds.right)
                ||
                    DestRect.bottom < Math.min(getHeight(), ViewBounds.bottom)
              )
              {
              /* cache doesn't completely cover visible part of image */
                RebuildViewCache();
              } /*if*/
          } /*if*/
      } /*RecenterViewCache*/

    @Override
    public void onDraw
      (
        android.graphics.Canvas g
      )
      {
        if (DrawWhat != null)
          {
            final RectF ViewBounds = GetScrolledViewBounds();
            if (ViewCache != null)
              {
              /* cache available, use it */
                final RectF DestRect = new RectF(ViewCache.Bounds);
                DestRect.offset(ViewBounds.left, ViewBounds.top);
              /* Unfortunately, the sample image doesn't look exactly the same
                when drawn offscreen and then copied on-screen, versus being
                drawn directly on-screen: path strokes are slightly thicker
                in the former case. Not sure what to do about this. */
                g.drawBitmap(ViewCache.Bits, null, DestRect, null);
                RecenterViewCache();
              }
            else if (BuildViewCache != null)
              {
              /* cache rebuild in progress, wait for it to finish before actually drawing,
                to avoid CPU contention that slows things down */
              }
            else
              {
              /* do it the slow way */
                DrawWhat.Draw(g, ViewBounds);
                if (UseCaching && BuildViewCache == null)
                  {
                  /* first call, nobody has called RebuildViewCache yet, do it */
                    RebuildViewCache();
                  } /*if*/
              } /*if*/
          } /*if*/
      } /*onDraw*/

    @Override
    public void invalidate()
      {
        RecenterViewCache();
        super.invalidate();
      } /*invalidate*/

/*
    Interaction handling
*/

    protected PointF
        LastMouse1 = null,
        LastMouse2 = null;
    protected MotionEvent
        FirstTap;
    protected int
        Mouse1ID = -1,
        Mouse2ID = -1;
    protected boolean
        MouseMoved = false,
        ExpectDoubleTap = false;
    protected ContextMenuAction
        DoContextMenu = null;

    protected class ScrollAnimator implements Runnable
      {
        final android.view.animation.Interpolator AnimFunction;
        final double StartTime, EndTime;
        final PointF StartScroll, EndScroll;

        public ScrollAnimator
          (
            android.view.animation.Interpolator AnimFunction,
            double StartTime,
            double EndTime,
            PointF StartScroll,
            PointF EndScroll
          )
          {
            this.AnimFunction = AnimFunction;
            this.StartTime = StartTime;
            this.EndTime = EndTime;
            this.StartScroll = StartScroll;
            this.EndScroll = EndScroll;
            CurrentAnim = this;
            post(this);
          } /*ScrollAnimator*/

        public void run()
          {
            if (CurrentAnim == this)
              {
                final double CurrentTime = GetTime();
                final float AnimAmt =
                    AnimFunction.getInterpolation
                      (
                        (float)((CurrentTime - StartTime) / (EndTime - StartTime))
                      );
                ScrollTo
                  (
                    new PointF
                      (
                        StartScroll.x + (EndScroll.x - StartScroll.x) * AnimAmt,
                        StartScroll.y + (EndScroll.y - StartScroll.y) * AnimAmt
                      ),
                    false
                  );
                if (getHandler() != null && CurrentTime < EndTime)
                  /* handler can be null if activity is being destroyed */
                  {
                    post(this);
                  }
                else
                  {
                    CurrentAnim = null;
                  } /*if*/
              } /*if*/
          } /*run*/

      } /*ScrollAnimator*/

    private ScrollAnimator CurrentAnim = null;

    protected final android.view.GestureDetector FlingDetector =
        new android.view.GestureDetector
          (
            Context,
            new android.view.GestureDetector.SimpleOnGestureListener()
              {
                @Override
                public boolean onFling
                  (
                    MotionEvent DownEvent,
                    MotionEvent UpEvent,
                    float XVelocity,
                    float YVelocity
                  )
                  {
                    final RectF ViewBounds = GetScrolledViewBounds();
                    final boolean DoFling =
                            ViewBounds.left < 0 && XVelocity > 0
                        ||
                            ViewBounds.right > getWidth() && XVelocity < 0
                        ||
                            ViewBounds.top < 0 && YVelocity > 0
                        ||
                            ViewBounds.bottom > getHeight() && YVelocity < 0;
                    if (DoFling)
                      {
                        final double CurrentTime = GetTime();
                        final float InitialAttenuate = 2.0f; /* attenuates initial speed */
                        final float FinalAttenuate = 1.0f; /* attenuates duration of scroll */
                        final float ScrollDuration =
                                (float)Math.hypot(XVelocity, YVelocity)
                            /
                                (float)Math.hypot(getWidth(), getHeight())
                            /
                                FinalAttenuate;
                        final PointF MouseUp = new PointF(UpEvent.getX(), UpEvent.getY());
                        final PointF EndScroll =
                            FindScrollOffset
                              (
                                /*DrawCoords =*/ ViewToDraw(MouseUp),
                                /*ViewCoords =*/
                                    new PointF
                                      (
                                            MouseUp.x
                                        +
                                                XVelocity
                                            *
                                                ScrollDuration
                                            /
                                                InitialAttenuate,
                                            MouseUp.y
                                        +
                                                YVelocity
                                            *
                                                ScrollDuration
                                            /
                                                InitialAttenuate
                                      ),
                                /*ZoomFactor =*/ ZoomFactor
                              );
                        new ScrollAnimator
                          (
                            /*AnimFunction =*/ new android.view.animation.DecelerateInterpolator(),
                            /*StartTime =*/ CurrentTime,
                            /*EndTime =*/ CurrentTime + ScrollDuration,
                            /*StartScroll =*/ ScrollOffset,
                            /*EndScroll =*/ EndScroll
                          );
                      } /*if*/
                    return
                        DoFling;
                  } /*onFling*/
              } /*GestureDetector.SimpleOnGestureListener*/
          );

    protected final Runnable LongClicker =
      /* do my own long-click handling, because setOnLongClickListener doesn't seem to work */
        new Runnable()
          {
            public void run()
              {
                showContextMenu();
              /* stop handling cursor/scale movements */
                LastMouse1 = null;
                LastMouse2 = null;
                Mouse1ID = -1;
                Mouse2ID = -1;
              } /*run*/
          } /*Runnable*/;

    @Override
    public boolean onTouchEvent
      (
        MotionEvent TheEvent
      )
      {
        CurrentAnim = null; /* cancel any animation in progress */
        boolean Handled = FlingDetector.onTouchEvent(TheEvent);
        if (!Handled)
          {
            boolean DoRebuild = false;
            switch (TheEvent.getAction() & (1 << MotionEvent.ACTION_POINTER_ID_SHIFT) - 1)
              {
            case MotionEvent.ACTION_DOWN:
                LastMouse1 = new PointF(TheEvent.getX(), TheEvent.getY());
                Mouse1ID = TheEvent.getPointerId(0);
                MouseMoved = false;
                Handled = true;
                postDelayed
                  (
                    LongClicker,
                    android.view.ViewConfiguration.getLongPressTimeout()
                  );
            break;
            case MotionEvent.ACTION_POINTER_DOWN:
                if (!MouseMoved)
                  {
                    getHandler().removeCallbacks(LongClicker);
                    MouseMoved = true;
                  } /*if*/
                ExpectDoubleTap = false;
                  {
                    final int PointerIndex =
                            (TheEvent.getAction() & MotionEvent.ACTION_POINTER_ID_MASK)
                        >>
                            MotionEvent.ACTION_POINTER_ID_SHIFT;
                    final int MouseID = TheEvent.getPointerId(PointerIndex);
                    final PointF MousePos = new PointF
                      (
                        TheEvent.getX(PointerIndex),
                        TheEvent.getY(PointerIndex)
                      );
                    if (LastMouse1 == null)
                      {
                        Mouse1ID = MouseID;
                        LastMouse1 = MousePos;
                      }
                    else if (LastMouse2 == null)
                      {
                        Mouse2ID = MouseID;
                        LastMouse2 = MousePos;
                      } /*if*/
                  }
                Handled = true;
            break;
            case MotionEvent.ACTION_MOVE:
                if (LastMouse1 != null && DrawWhat != null)
                  {
                    final int Mouse1Index = TheEvent.findPointerIndex(Mouse1ID);
                    final int Mouse2Index =
                        LastMouse2 != null ?
                            TheEvent.findPointerIndex(Mouse2ID)
                        :
                            -1;
                    if (Mouse1Index >= 0 || Mouse2Index >= 0)
                      {
                        final PointF ThisMouse1 =
                            Mouse1Index >= 0 ?
                                new PointF
                                  (
                                    TheEvent.getX(Mouse1Index),
                                    TheEvent.getY(Mouse1Index)
                                  )
                            :
                                null;
                        final PointF ThisMouse2 =
                            Mouse2Index >= 0 ?
                                new PointF
                                 (
                                   TheEvent.getX(Mouse2Index),
                                   TheEvent.getY(Mouse2Index)
                                 )
                             :
                                null;
                        if (ThisMouse1 != null || ThisMouse2 != null)
                          {
                            if (ThisMouse1 != null && ThisMouse2 != null)
                              {
                              /* pinch to zoom */
                                final float LastDistance = (float)Math.hypot
                                  (
                                    LastMouse1.x - LastMouse2.x,
                                    LastMouse1.y - LastMouse2.y
                                  );
                                final float ThisDistance = (float)Math.hypot
                                  (
                                    ThisMouse1.x - ThisMouse2.x,
                                    ThisMouse1.y - ThisMouse2.y
                                  );
                                if
                                  (
                                        LastDistance != 0.0f
                                    &&
                                        ThisDistance != 0.0f
                                  )
                                  {
                                    ZoomBy(ThisDistance /  LastDistance);
                                  } /*if*/
                              }
                            else
                              {
                                final PointF ThisMouse =
                                    ThisMouse1 != null ?
                                        ThisMouse2 != null ?
                                            new PointF
                                              (
                                                (ThisMouse1.x + ThisMouse2.x) / 2.0f,
                                                (ThisMouse1.y + ThisMouse2.y) / 2.0f
                                              )
                                        :
                                            ThisMouse1
                                    :
                                        ThisMouse2;
                                final PointF LastMouse =
                                    ThisMouse1 != null ?
                                        ThisMouse2 != null ?
                                            new PointF
                                              (
                                                (LastMouse1.x + LastMouse2.x) / 2.0f,
                                                (LastMouse1.y + LastMouse2.y) / 2.0f
                                              )
                                        :
                                            LastMouse1
                                    :
                                        LastMouse2;
                                if
                                  (
                                        MouseMoved
                                    ||
                                            Math.hypot
                                              (
                                                ThisMouse.x - LastMouse.x,
                                                ThisMouse.y - LastMouse.y
                                              )
                                        >
                                            Math.sqrt
                                              /* actually shouldn't be taking square root, but
                                                value seems quite large */
                                              (
                                                android.view.ViewConfiguration.get(Context)
                                                    .getScaledTouchSlop()
                                              )
                                  )
                                  {
                                    if (!MouseMoved)
                                      {
                                        getHandler().removeCallbacks(LongClicker);
                                        MouseMoved = true;
                                      } /*if*/
                                    ExpectDoubleTap = false;
                                    ScrollTo
                                      (
                                        FindScrollOffset
                                          (
                                            /*DrawCoords =*/ ViewToDraw(LastMouse),
                                            /*ViewCoords =*/ ThisMouse,
                                            /*ZoomFactor =*/ ZoomFactor
                                          ),
                                        false
                                      );
                                  } /*if*/
                              } /*if*/
                            LastMouse1 = ThisMouse1;
                            LastMouse2 = ThisMouse2;
                          } /*if*/
                      } /*if*/
                  } /*if*/
                DoRebuild = true;
                Handled = true;
            break;
            case MotionEvent.ACTION_POINTER_UP:
                if (LastMouse2 != null)
                  {
                    final int PointerIndex =
                            (TheEvent.getAction() & MotionEvent.ACTION_POINTER_ID_MASK)
                        >>
                            MotionEvent.ACTION_POINTER_ID_SHIFT;
                    final int PointerID = TheEvent.getPointerId(PointerIndex);
                    if (PointerID == Mouse1ID)
                      {
                        Mouse1ID = Mouse2ID;
                        LastMouse1 = LastMouse2;
                        Mouse2ID = -1;
                        LastMouse2 = null;
                      }
                    else if (PointerID == Mouse2ID)
                      {
                        Mouse2ID = -1;
                        LastMouse2 = null;
                      } /*if*/
                  } /*if*/
                DoRebuild = true;
                Handled = true;
            break;
            case MotionEvent.ACTION_UP:
                getHandler().removeCallbacks(LongClicker);
                if (LastMouse1 != null && !MouseMoved)
                  {
                    if
                      (
                            ExpectDoubleTap
                        &&
                                TheEvent.getEventTime() - FirstTap.getEventTime()
                            <=
                                android.view.ViewConfiguration.getDoubleTapTimeout()
                        &&
                                Math.hypot
                                  (
                                    TheEvent.getX() - FirstTap.getX(),
                                    TheEvent.getY() - FirstTap.getY()
                                  )
                            <=
                                android.view.ViewConfiguration.get(Context)
                                    .getScaledDoubleTapSlop()
                      )
                      {
                        if (OnDoubleTap != null)
                          {
                            OnDoubleTap.OnTap
                              (
                                this,
                                LastMouse2 != null ?
                                    new PointF
                                      (
                                        (LastMouse1.x + LastMouse2.x) / 2.0f,
                                        (LastMouse1.y + LastMouse2.y) / 2.0f
                                      )
                                :
                                    LastMouse1
                              );
                          } /*if*/
                        ExpectDoubleTap = false;
                      }
                    else
                      {
                        if (OnSingleTap != null)
                          {
                            OnSingleTap.OnTap
                              (
                                this,
                                LastMouse2 != null ?
                                    new PointF
                                      (
                                        (LastMouse1.x + LastMouse2.x) / 2.0f,
                                        (LastMouse1.y + LastMouse2.y) / 2.0f
                                      )
                                :
                                    LastMouse1
                              );
                          } /*if*/
                        FirstTap = MotionEvent.obtain(TheEvent);
                          /* need to make a copy because original object might be reused */
                        ExpectDoubleTap = true;
                      } /*if*/
                  }
                else
                  {
                    ExpectDoubleTap = false;
                  } /*if*/
                LastMouse1 = null;
                LastMouse2 = null;
                Mouse1ID = -1;
                Mouse2ID = -1;
              /* DoRebuild = true; */ /* not for animated scrolling */
                Handled = true;
            break;
              } /*switch*/
            if (UseCaching && DoRebuild && BuildViewCache == null)
              {
              /* try to keep cache up to date to minimize appearance of
                black borders in uncached areas during scrolling */
                RebuildViewCache();
              } /*if*/
          } /*if*/
        return
            Handled;
      } /*onTouchEvent*/

    @Override
    public void onCreateContextMenu
      (
        android.view.ContextMenu TheMenu
      )
      {
        if (DoContextMenu != null)
          {
            Vibrate.vibrate(20);
            DoContextMenu.CreateContextMenu
              (
                TheMenu,
                new PointF(LastMouse1.x, LastMouse1.y)
              );
          } /*if*/
      } /*onCreateContextMenu*/

/*
    Implementation of saving/restoring instance state. Doing this
    allows me to transparently restore scroll/zoom state if system
    needs to kill me while I'm in the background, or on an orientation
    change while I'm in the foreground.

    Notes: View.onSaveInstanceState returns AbsSavedState.EMPTY_STATE,
    and View.onRestoreInstanceState expects to be passed this. Also,
    both superclass methods MUST be called in my overrides (the docs
    don't make this clear).
*/

    @Override
    public android.os.Parcelable onSaveInstanceState()
      {
      /* Instead of saving ScrollOffset directly, I save the image coordinates
        at the centre of the view. This makes for more predictable behaviour
        on an orientation change. But it does mean I cannot properly restore
        the state until my layout dimensions have been assigned. */
        final PointF DrawCenter = ViewToDraw
          (
            new PointF(getWidth() / 2.0f, getHeight() / 2.0f)
          );
        final android.os.Bundle MyState = new android.os.Bundle();
        MyState.putFloat("ScrollX", DrawCenter.x);
        MyState.putFloat("ScrollY", DrawCenter.y);
        MyState.putFloat("ZoomFactor", ZoomFactor);
        return
            new BundledSavedState
              (
                super.onSaveInstanceState(),
                MyState
              );
      } /*onSaveInstanceState*/

    private android.os.Bundle LastSavedState = null;

    @Override
    public void onRestoreInstanceState
      (
        android.os.Parcelable SavedState
      )
      {
        super.onRestoreInstanceState(((BundledSavedState)SavedState).SuperState);
      /* defer rest of restoration to after I have been given layout dimensions */
        LastSavedState = ((BundledSavedState)SavedState).MyState;
      } /*onRestoreInstanceState*/

    @Override
    protected void onLayout
      (
        boolean Changed,
        int Left,
        int Top,
        int Right,
        int Bottom
      )
      /* I just use this as a convenient place to finish restoring my instance state,
        because I know getWidth and getHeight will return nonzero values by this point. */
      {
        super.onLayout(Changed, Left, Top, Right, Bottom);
        if (LastSavedState != null)
          {
          /* finish restoration of saved instance state */
            ZoomFactor = LastSavedState.getFloat("ZoomFactor", 1.0f);
            ScrollOffset = FindScrollOffset
              (
                /*DrawCoords =*/
                    new PointF
                      (
                        LastSavedState.getFloat("ScrollX", 0.0f),
                        LastSavedState.getFloat("ScrollY", 0.0f)
                      ),
                /*ViewCoords =*/ new PointF(getWidth() / 2.0f, getHeight() / 2.0f),
                /*ZoomFactor =*/ ZoomFactor
              );
            invalidate();
            LastSavedState = null;
          } /*if*/
      } /*onLayout*/

/*
    public widget-control methods
*/

    public void SetDrawer
      (
        Drawer DrawWhat
      )
      {
        this.DrawWhat = DrawWhat;
      /* fixme: need to rebuild cache and redraw if I allow user to change image on the fly */
      } /*SetDrawer*/

    public void SetOnSingleTapListener
      (
        OnTapListener OnSingleTap
      )
      {
        this.OnSingleTap = OnSingleTap;
      } /*SetOnSingleTapListener*/

    public void SetOnDoubleTapListener
      (
        OnTapListener OnDoubleTap
      )
      {
        this.OnDoubleTap = OnDoubleTap;
      } /*SetOnDoubleTapListener*/

    public boolean GetUseCaching()
      {
        return
            UseCaching;
      } /*GetUseCaching*/

    public void SetUseCaching
      (
        boolean NewUseCaching
      )
      {
        UseCaching = NewUseCaching;
        if (!UseCaching)
          {
            ForgetViewCache();
          } /*if*/
      } /*SetUseCaching*/

    public float GetZoomFactor()
      {
        return
            ZoomFactor;
      } /*GetZoomFactor*/

    public void ZoomBy
      (
        float Factor
      )
      /* multiplies the current zoom by Factor. */
      {
        final float NewZoomFactor =
            Math.min
              (
                Math.max
                  (
                    ZoomFactor * Math.abs(Factor),
                    MinZoomFactor
                  ),
                MaxZoomFactor
              );
        if (NewZoomFactor != ZoomFactor)
          {
            DisposeViewCache();
            final PointF ViewCenter = new PointF(getWidth() / 2.0f, getHeight() / 2.0f);
            final PointF NewScrollOffset = /* so point of image at ViewCenter stays there */
                FindScrollOffset
                  (
                    /*DrawCoords =*/ ViewToDraw(ViewCenter),
                    /*ViewCoords =*/ ViewCenter,
                    /*ZoomFactor =*/ NewZoomFactor
                  );
            ZoomFactor = NewZoomFactor;
            ScrollTo(NewScrollOffset, false);
            invalidate();
          } /*if*/
      } /*ZoomBy*/

    public void ScrollTo
      (
        PointF NewScrollOffset,
        boolean Animate
      )
      /* sets ScrollOffset to the specified value. */
      {
        if (DrawWhat != null)
          {
            NewScrollOffset = new PointF
              (
                Math.max(0.0f, Math.min(NewScrollOffset.x, 1.0f)),
                Math.max(0.0f, Math.min(NewScrollOffset.y, 1.0f))
              );
            if (ScrollOffset.x != NewScrollOffset.x || ScrollOffset.y != NewScrollOffset.y)
              {
                if (Animate)
                  {
                    final double CurrentTime = GetTime();
                    final float ScrollDuration = 1.0f; /* maybe make this depend on scroll amount in future */
                    new ScrollAnimator
                      (
                        /*AnimFunction =*/ new android.view.animation.AccelerateDecelerateInterpolator(),
                        /*StartTime =*/ CurrentTime,
                        /*EndTime =*/ CurrentTime + ScrollDuration,
                        /*StartScroll =*/ ScrollOffset,
                        /*EndScroll =*/ NewScrollOffset
                      );
                  }
                else
                  {
                    ScrollOffset = NewScrollOffset;
                    invalidate();
                  } /*if*/
              } /*if*/
          } /*if*/
      } /*ScrollTo*/

/*
    Implementing the following (and calling setxxxFadingEdgeEnabled(true)
    in the constructors, above) will cause fading edges to appear.
*/

    protected static final int ScrollScale = 1000; /* arbitrary units */

    @Override
    protected int computeHorizontalScrollExtent()
      {
        return
            Math.round(getWidth() * ScrollScale / GetViewSize().x);
      } /*computeHorizontalScrollExtent*/

    @Override
    protected int computeHorizontalScrollOffset()
      {
        final RectF ViewBounds = GetScrolledViewBounds();
        return
            ViewBounds.right - ViewBounds.left > getWidth() ?
                Math.round
                  (
                        (- ViewBounds.left)
                    *
                        ScrollScale
                    /
                        (ViewBounds.right - ViewBounds.left)
                  )
            :
                0;
      } /*computeHorizontalScrollOffset*/

    @Override
    protected int computeHorizontalScrollRange()
      {
        return
            ScrollScale;
      } /*computeHorizontalScrollRange*/

    @Override
    protected int computeVerticalScrollExtent()
      {
        return
            Math.round(getHeight() * ScrollScale / GetViewSize().y);
      } /*computeVerticalScrollExtent*/

    @Override
    protected int computeVerticalScrollOffset()
      {
        final RectF ViewBounds = GetScrolledViewBounds();
        return
            ViewBounds.bottom - ViewBounds.top > getHeight() ?
                Math.round
                  (
                        (- ViewBounds.top)
                    *
                        ScrollScale
                    /
                        (ViewBounds.bottom - ViewBounds.top)
                  )
            :
                0;
      } /*computeVerticalScrollOffset*/

    @Override
    protected int computeVerticalScrollRange()
      {
        return
            ScrollScale;
      } /*computeVerticalScrollRange*/

  } /*DrawView*/;




Java Source Code List

nz.gen.geek_central.android.useful.BundledSavedState.java
nz.gen.geek_central.android.useful.Useful.java
nz.gen.geek_central.view_cache_demo.DrawView.java
nz.gen.geek_central.view_cache_demo.Drawer.java
nz.gen.geek_central.view_cache_demo.Main.java
nz.gen.geek_central.view_cache_demo.SampleDrawer.java