diff --git a/app/src/main/java/com/mithrilmania/blocktopograph/CreateWorldActivity.java b/app/src/main/java/com/mithrilmania/blocktopograph/CreateWorldActivity.java index bbc2443d..ad943df2 100644 --- a/app/src/main/java/com/mithrilmania/blocktopograph/CreateWorldActivity.java +++ b/app/src/main/java/com/mithrilmania/blocktopograph/CreateWorldActivity.java @@ -64,6 +64,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { mBinding = DataBindingUtil.setContentView(this, R.layout.activity_create_world); mToolTipsManager = new ToolTipsManager(); Log.logFirebaseEvent(this, Log.CustomFirebaseEvent.CREATE_WORLD_OPEN); + mBinding.scroll.post(() -> mBinding.scroll.scrollTo(0, 0)); } public void onClickPositiveButton(View view) { diff --git a/app/src/main/java/com/mithrilmania/blocktopograph/map/MapFragment.java b/app/src/main/java/com/mithrilmania/blocktopograph/map/MapFragment.java index 3ed55173..298c34d3 100644 --- a/app/src/main/java/com/mithrilmania/blocktopograph/map/MapFragment.java +++ b/app/src/main/java/com/mithrilmania/blocktopograph/map/MapFragment.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.content.Context; import android.content.DialogInterface; +import android.content.res.Configuration; import android.os.AsyncTask; import android.os.Bundle; import android.text.Editable; @@ -212,7 +213,7 @@ private void moveCameraToPlayer(View view) { Snackbar.make(mBinding.tileView, getString(R.string.something_at_xyz_dim_float, getString(R.string.player), - playerPos.x, playerPos.y, playerPos.z, playerPos.dimension.name), + playerPos.x, playerPos.y, playerPos.z), Snackbar.LENGTH_SHORT) .setAction("Action", null).show(); @@ -251,7 +252,7 @@ private void moveCameraToSpawn(View view) { Snackbar.make(mBinding.tileView, getString(R.string.something_at_xyz_dim_int, getString(R.string.spawn), - spawnPos.x, spawnPos.y, spawnPos.z, spawnPos.dimension.name), + spawnPos.x, spawnPos.y, spawnPos.z), Snackbar.LENGTH_SHORT) .setAction("Action", null).show(); @@ -287,14 +288,31 @@ private void closeFloatPane() { .newInstance(mBinding.selectionBoard.getSelection(), this::doSelectionBasedEdit); trans.add(R.id.float_window_container, fragment); - setUpSelectionMenu(); mFloatingFragment = fragment; + setUpSelectionMenu(); } else mFloatingFragment = null; trans.commit(); } } + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (mFloatingFragment != null) { + FloatPaneFragment fragment; + if (mFloatingFragment instanceof AdvancedLocatorFragment) { + fragment = AdvancedLocatorFragment.create(world, this::frameTo); + } else if (mFloatingFragment instanceof SelectionMenuFragment) { + fragment = SelectionMenuFragment + .newInstance(mBinding.selectionBoard.getSelection(), this::doSelectionBasedEdit); + } else return; + closeFloatPane(); + openFloatPane(fragment); + setUpSelectionMenu(); + } + } + /** * Set up selection menu and connect it with selection board. * @@ -517,20 +535,14 @@ Create tile(=bitmap) provider tileView.getMarkerLayout().setMarkerTapListener(new MarkerLayout.MarkerTapListener() { @Override public void onMarkerTap(View view, int tapX, int tapY) { - if (!(view instanceof MarkerImageView)) { - Log.d(this, "Markertaplistener found a marker that is not a MarkerImageView! " + view.toString()); - return; - } + if (!(view instanceof MarkerImageView)) return; final AbstractMarker marker = ((MarkerImageView) view).getMarkerHook(); - if (marker == null) { - Log.d(this, "abstract marker is null! " + view.toString()); - return; - } + if (marker == null) return; AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder - .setTitle(String.format(getString(R.string.marker_info), marker.getNamedBitmapProvider().getBitmapDisplayName(), marker.getNamedBitmapProvider().getBitmapDataName(), marker.x, marker.y, marker.z, marker.dimension)) + .setTitle(String.format(getString(R.string.marker_info), marker.getNamedBitmapProvider().getBitmapDisplayName(), marker.x, marker.y, marker.z)) .setItems(getMarkerTapOptions(), new DialogInterface.OnClickListener() { @SuppressWarnings("RedundantCast") public void onClick(DialogInterface dialog, int which) { @@ -848,7 +860,7 @@ private void onLongPressed(@NotNull MotionEvent event) { } AlertDialog alertDialog = new AlertDialog.Builder(new ContextThemeWrapper(activity, R.style.AppTheme_Floating)) - .setTitle(getString(R.string.postion_2D_floats_with_chunkpos, worldX, worldZ, chunkXint, chunkZint, dim.name)) + .setTitle(getString(R.string.postion_2D_floats_with_chunkpos, worldX, worldZ, chunkXint, chunkZint)) .setItems(getLongClickOptions(), (dialog, which) -> { @@ -874,6 +886,8 @@ private void onLongPressed(@NotNull MotionEvent event) { .setCancelable(true) .setNegativeButton(android.R.string.cancel, null) .show(); + alertDialog.getListView().post(() -> + alertDialog.getListView().smoothScrollToPositionFromTop(4, 40)); Window window = alertDialog.getWindow(); if (window != null) { window.setBackgroundDrawable(getResources().getDrawable(R.drawable.bg_dialog_transparent)); @@ -1069,7 +1083,7 @@ private void onChooseTeleportPlayer(float worldX, float worldZ, Dimension dim, V (int) newX, (int) newY, (int) newZ, dim); Snackbar.make(container, - String.format(getString(R.string.teleported_player_to_xyz_dim), newX, newY, newZ, dim.name), + getString(R.string.teleported_player_to_xyz_dim, newX, newY, newZ), Snackbar.LENGTH_LONG) .setAction("Action", null).show(); } else { @@ -1430,8 +1444,7 @@ public void onClick(DialogInterface dialog, int whichButton) { playerKey, playerPos.x, playerPos.y, - playerPos.z, - playerPos.dimension.name), + playerPos.z), Snackbar.LENGTH_LONG) .setAction("Action", null).show(); diff --git a/app/src/main/java/com/mithrilmania/blocktopograph/map/MapTileView.java b/app/src/main/java/com/mithrilmania/blocktopograph/map/MapTileView.java index 977aa521..80446d7f 100644 --- a/app/src/main/java/com/mithrilmania/blocktopograph/map/MapTileView.java +++ b/app/src/main/java/com/mithrilmania/blocktopograph/map/MapTileView.java @@ -61,6 +61,16 @@ public boolean onTouchEvent(MotionEvent event) { return false; } + @Override + public boolean onSingleTapConfirmed(MotionEvent event) { + if (super.onSingleTapConfirmed(event)) return true; + if (mOnLongPressListener != null) { + mOnLongPressListener.onLongPressed(event); + return true; + } + return false; + } + /** * Sets the long press callback. * diff --git a/app/src/main/java/com/mithrilmania/blocktopograph/map/edit/SearchAndReplaceFragment.java b/app/src/main/java/com/mithrilmania/blocktopograph/map/edit/SearchAndReplaceFragment.java index 9ba6528c..a1467186 100644 --- a/app/src/main/java/com/mithrilmania/blocktopograph/map/edit/SearchAndReplaceFragment.java +++ b/app/src/main/java/com/mithrilmania/blocktopograph/map/edit/SearchAndReplaceFragment.java @@ -1,6 +1,7 @@ package com.mithrilmania.blocktopograph.map.edit; import android.app.Dialog; +import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.LayoutInflater; @@ -25,6 +26,7 @@ import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.databinding.DataBindingUtil; import androidx.fragment.app.DialogFragment; @@ -101,7 +103,13 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mBinding.ok.setOnClickListener(this::onClickOk); mBinding.help.setOnClickListener(this::onClickHelpMain); mToolTipsManager = new ToolTipsManager(); - return mBinding.getRoot(); + View root = mBinding.getRoot(); + Dialog dialog = getDialog(); + if (dialog instanceof AlertDialog) { + ((AlertDialog) dialog).setView(root); + } + mBinding.scroll.post(() -> mBinding.scroll.scrollTo(0, 0)); + return root; } private void onClickHelpMain(@NotNull View view) { @@ -257,8 +265,11 @@ private void onClickOk(View view) { @NonNull @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - dialog.setTitle(R.string.map_edit_func_snr); + Context context = requireContext(); + AlertDialog dialog = new AlertDialog.Builder(context) + //.setView(onCreateView(LayoutInflater.from(context), null, savedInstanceState)) + .setTitle(R.string.map_edit_func_snr) + .create(); return dialog; } diff --git a/app/src/main/java/com/mithrilmania/blocktopograph/map/renderer/SatelliteRenderer.java b/app/src/main/java/com/mithrilmania/blocktopograph/map/renderer/SatelliteRenderer.java index 37a2de8b..3253a6b8 100644 --- a/app/src/main/java/com/mithrilmania/blocktopograph/map/renderer/SatelliteRenderer.java +++ b/app/src/main/java/com/mithrilmania/blocktopograph/map/renderer/SatelliteRenderer.java @@ -6,7 +6,6 @@ import android.graphics.Rect; import com.mithrilmania.blocktopograph.Log; - import com.mithrilmania.blocktopograph.WorldData; import com.mithrilmania.blocktopograph.chunk.Chunk; import com.mithrilmania.blocktopograph.chunk.Version; @@ -30,6 +29,7 @@ public void renderToBitmap(Chunk chunk, Canvas canvas, Dimension dimension, int for (x = 0, tX = pX; x < 16; x++, tX += pW) { y = chunk.getHeightMapValue(x, z); + if (y == 0) continue; color = getColumnColour(chunk, x, y, z, (x == 0) ? (west ? dataW.getHeightMapValue(dimension.chunkW - 1, z) : y)//chunk edge diff --git a/app/src/main/java/com/mithrilmania/blocktopograph/map/selection/SelectionMenuFragment.java b/app/src/main/java/com/mithrilmania/blocktopograph/map/selection/SelectionMenuFragment.java index 0f6c4818..7d7a72a2 100644 --- a/app/src/main/java/com/mithrilmania/blocktopograph/map/selection/SelectionMenuFragment.java +++ b/app/src/main/java/com/mithrilmania/blocktopograph/map/selection/SelectionMenuFragment.java @@ -12,6 +12,7 @@ import android.widget.EditText; import android.widget.Toast; +import com.github.florent37.expansionpanel.ExpansionLayout; import com.mithrilmania.blocktopograph.Log; import com.mithrilmania.blocktopograph.R; import com.mithrilmania.blocktopograph.databinding.FragSelMenuBinding; @@ -62,9 +63,17 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c mBinding.content.applyButton.setOnClickListener(this::onApply); mBinding.content.funcLampshade.setOnClickListener(this::onChooseLampshade); mBinding.content.funcSnr.setOnClickListener(this::onChooseSnr); + mBinding.expansionLayout.post(() -> mBinding.expansionLayout.scrollTo(0, 0)); + if (mBinding.scroll != null) + mBinding.expansionLayout.addListener(this::onExpansionChanged); return mBinding.getRoot(); } + private void onExpansionChanged(ExpansionLayout layout, boolean expanded) { + mBinding.scroll.setScrollY(mBinding.scroll.getChildAt(0).getMeasuredHeight()); + mBinding.scroll.post(() -> mBinding.scroll.smoothScrollTo(0, 0)); + } + @Override public void onDestroyView() { super.onDestroyView(); diff --git a/app/src/main/java/com/mithrilmania/blocktopograph/map/selection/SelectionView.java b/app/src/main/java/com/mithrilmania/blocktopograph/map/selection/SelectionView.java index 41a85d3c..bd5a09e5 100644 --- a/app/src/main/java/com/mithrilmania/blocktopograph/map/selection/SelectionView.java +++ b/app/src/main/java/com/mithrilmania/blocktopograph/map/selection/SelectionView.java @@ -30,8 +30,6 @@ public class SelectionView extends FrameLayout { - public static final int MIN_DIST_DRAGGERS = 50; - public static final int HALF_MIN_DIST_DRAGGERS = MIN_DIST_DRAGGERS / 2; /** * The view being dragged now. */ @@ -115,6 +113,19 @@ public class SelectionView extends FrameLayout { */ private float BUTTON_TO_BOUND_MIN_DIST; + /** + * The minimal non-auto-scroll distance from touched point to screen boundary. + * + *

+ * Once it's exceed the tileView would be scrolled automatically. + *

+ */ + private int MIN_DIST_TO_SCREEN_BOUND; + + private int MIN_DIST_DRAGGERS; + + private int HALF_MIN_DIST_DRAGGERS; + /** * Runnable used to frequently alter selection while user dragging a adjust button. */ @@ -160,6 +171,9 @@ private void init(Context context) { mPaint.setColor(Color.argb(0x80, 0, 0, 0)); setWillNotDraw(false);// Otherwise `onDraw` won't be called. BUTTON_TO_BOUND_MIN_DIST = UiUtil.dpToPx(context, 72); + MIN_DIST_TO_SCREEN_BOUND = UiUtil.dpToPxInt(context, 100); + MIN_DIST_DRAGGERS = UiUtil.dpToPxInt(context, 50); + HALF_MIN_DIST_DRAGGERS = MIN_DIST_DRAGGERS / 2; mDragger = null; } @@ -385,15 +399,35 @@ private void onMove() { if (mSelectionChangedListener != null) mSelectionChangedListener.onSelectionChanged(mSelectionRect); - // Move the tileView as well. + // Should we move the underlying tileView as well? + // If touched point is near the moving direction (not the dragger position) + // then we scroll. + + int sw = getMeasuredWidth(); + int sh = getMeasuredHeight(); + int minw = Math.max(MIN_DIST_TO_SCREEN_BOUND, sw / 8); + int minh = Math.max(MIN_DIST_TO_SCREEN_BOUND, sh / 8); + switch (draggerId) { case R.id.left: case R.id.right: - tileView.setScrollX((int) (tileView.getScrollX() + movement)); + // (Moving right and near right bound) or + // (Moving left and near left bound) + if (mDragDirection > 0 && sw - mDragCurrentPos < minw + || (mDragDirection < 0 && mDragCurrentPos < minw) + ) + tileView.setScrollX((int) (tileView.getScrollX() + movement)); + else + requestLayout(); break; case R.id.top: case R.id.bottom: - tileView.setScrollY((int) (tileView.getScrollY() + movement)); + if (mDragDirection > 0 && sh - mDragCurrentPos < minh + || (mDragDirection < 0 && mDragCurrentPos < minh) + ) + tileView.setScrollY((int) (tileView.getScrollY() + movement)); + else + requestLayout(); break; } } diff --git a/app/src/main/res/layout/activity_create_world.xml b/app/src/main/res/layout/activity_create_world.xml index 35affb4e..466d7ccd 100644 --- a/app/src/main/res/layout/activity_create_world.xml +++ b/app/src/main/res/layout/activity_create_world.xml @@ -10,10 +10,10 @@ android:padding="6dp"> + android:layout_weight="1"> - + android:layout_weight="0.5"> - - - - - + + + android:layout_margin="4dp" + android:background="@drawable/bg_float_transparent" + app:expansion_headerIndicator="@id/headerIndicator" + app:expansion_layout="@id/expansionLayout" + app:expansion_toggleOnClick="true"> + - + - - + + + + + - + - + + diff --git a/app/src/main/res/layout/frag_serach_and_replace.xml b/app/src/main/res/layout/frag_serach_and_replace.xml index 1ef86992..32f51db9 100644 --- a/app/src/main/res/layout/frag_serach_and_replace.xml +++ b/app/src/main/res/layout/frag_serach_and_replace.xml @@ -9,20 +9,27 @@ + android:orientation="vertical"> + + + android:layout_weight="1.0" + android:paddingLeft="10dp" + android:paddingRight="10dp"> + android:layout_height="match_parent"> diff --git a/app/src/main/res/values-v21/styles.xml b/app/src/main/res/values-v21/styles.xml index 05c42771..140451bb 100644 --- a/app/src/main/res/values-v21/styles.xml +++ b/app/src/main/res/values-v21/styles.xml @@ -1,4 +1,4 @@ -> + + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6fd1e2a8..0ac1215c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -115,17 +115,14 @@ 该区块的NBT %1$s - ~ %2$s - (%3$d; - %4$d; - %5$d) - [%6$s]. + (%2$d, + %3$d, + %4$d) - 已传送玩家到(%1$.2f; - %2$.2f; - %3$.2f) - [%4$s] + 已传送玩家到(%1$.1f, + %2$.1f, + %3$.1f) 传送玩家失败 找不到或者无法编辑本地玩家数据 @@ -197,17 +194,15 @@ 玩家位置 出生点 - %1$s在: - (%2$.2f; - %3$.2f; - %4$.2f) - [%5$s]. + %1$s + (%2$.1f, + %3$.1f, + %4$.1f) - %1$s在: - (%2$d; - %3$d; - %4$d) - [%5$s]. + %1$s + (%2$d, + %3$d, + %4$d). 没有有效的地图 寻找玩家中…… @@ -256,11 +251,10 @@ 创造 冒险 - 位置:(%1$.2f; - %2$.2f) - 区块:(%3$d; + (%1$.1f, + %2$.1f) + 区块(%3$d, %4$d) - [%5$s]. 传送! 前往出生点 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 399ef796..9fea9af0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -141,16 +141,13 @@ NBT chunk data %1$s - ~ %2$s - (%3$d; - %4$d; - %5$d) - [%6$s]. - Teleported the player to: - (%1$.2f; - %2$.2f; - %3$.2f) - [%4$s]. + (%2$d, + %3$d, + %4$d) + Teleported the player to + (%1$.1f, + %2$.1f, + %3$.1f) Failed to teleport player. Failed to find/edit local player data. @@ -240,17 +237,15 @@ Player Spawn - %1$s at: - (%2$.2f; - %3$.2f; - %4$.2f) - [%5$s]. + %1$s + (%2$.1f, + %3$.1f, + %4$.1f) - %1$s at: - (%2$d; - %3$d; - %4$d) - [%5$s]. + %1$s at, + (%2$d, + %3$d, + %4$d) No map available. Searching for markers… Failed to retrieve player data. @@ -300,11 +295,10 @@ Creative Adventure - Pos: (%1$.2f; - %2$.2f) - Chunk: (%3$d; - %4$d) - [%5$s]. + (%1$.1f, + %2$.1f) + Chunk(%3$d, + %4$d) Teleport! Go to spawn No worlds found. diff --git a/leveldb/build.gradle b/leveldb/build.gradle index d67792f7..5a82a2ce 100644 --- a/leveldb/build.gradle +++ b/leveldb/build.gradle @@ -7,7 +7,7 @@ android { minSdkVersion 16 targetSdkVersion 28 ndk { - abiFilters 'armeabi-v7a', 'x86_64','x86', 'arm64-v8a' + abiFilters 'armeabi-v7a', 'x86_64','x86', 'arm64-v8a' } } diff --git a/tileview/src/main/java/com/qozix/tileview/TileView.java b/tileview/src/main/java/com/qozix/tileview/TileView.java index 325eaac7..b2bbf057 100644 --- a/tileview/src/main/java/com/qozix/tileview/TileView.java +++ b/tileview/src/main/java/com/qozix/tileview/TileView.java @@ -41,7 +41,7 @@ * 5. Arbitrary coordinate systems. * 6. Tappable hot spots. * 7. Path drawing. - * + *

* A minimal implementation might look like this: * *

{@code
@@ -49,7 +49,7 @@
  * tileView.setSize( 3000, 5000 );
  * tileView.addDetailLevel( 1.0f, "path/to/tiles/%d-%d.jpg" );
  * }
- * + *

* A more advanced implementation might look like: * *

{@code
@@ -66,935 +66,934 @@
  * }
*/ public class TileView extends ZoomPanLayout implements - ZoomPanLayout.ZoomPanListener, - TileCanvasViewGroup.TileRenderListener, - DetailLevelManager.DetailLevelChangeListener { - - protected static final int DEFAULT_TILE_SIZE = 256; - - private DetailLevelManager mDetailLevelManager = new DetailLevelManager(); - private CoordinateTranslater mCoordinateTranslater = new CoordinateTranslater(); - private HotSpotManager mHotSpotManager = new HotSpotManager(); - - private TileCanvasViewGroup mTileCanvasViewGroup; - private CompositePathView mCompositePathView; - private ScalingLayout mScalingLayout; - private MarkerLayout mMarkerLayout; - private CalloutLayout mCalloutLayout; - - private RenderThrottleHandler mRenderThrottleHandler; - - private boolean mShouldRenderWhilePanning = false; - private boolean mShouldUpdateDetailLevelWhileZooming = false; - - /** - * Constructor to use when creating a TileView from code. - * - * @param context The Context the TileView is running in, through which it can access the current theme, resources, etc. - */ - public TileView( Context context ) { - this( context, null ); - } - - public TileView( Context context, AttributeSet attrs ) { - this( context, attrs, 0 ); - } - - public TileView( Context context, AttributeSet attrs, int defStyleAttr ) { - - super( context, attrs, defStyleAttr ); - - mTileCanvasViewGroup = new TileCanvasViewGroup( context ); - addView( mTileCanvasViewGroup ); - - mCompositePathView = new CompositePathView( context ); - addView( mCompositePathView ); - - mScalingLayout = new ScalingLayout( context ); - addView( mScalingLayout ); - - mMarkerLayout = new MarkerLayout( context ); - addView( mMarkerLayout ); - - mCalloutLayout = new CalloutLayout( context ); - addView( mCalloutLayout ); - - mDetailLevelManager.setDetailLevelChangeListener( this ); - mTileCanvasViewGroup.setTileRenderListener( this ); - addZoomPanListener( this ); - - mRenderThrottleHandler = new RenderThrottleHandler( this ); - - requestRender(); - - } - - /** - * Returns the DetailLevelManager instance used by the TileView to coordinate DetailLevels. - * - * @return The DetailLevelManager instance. - */ - public DetailLevelManager getDetailLevelManager() { - return mDetailLevelManager; - } - - /** - * Returns the CoordinateTranslater instance used by the TileView to manage abritrary coordinate - * systems. - * - * @return The CoordinateTranslater instance. - */ - public CoordinateTranslater getCoordinateTranslater() { - return mCoordinateTranslater; - } - - /** - * Returns the HotSpotManager instance used by the TileView to detect and react to touch events - * that intersect a user-defined region. - * - * @return The HotSpotManager instance. - */ - public HotSpotManager getHotSpotManager() { - return mHotSpotManager; - } - - /** - * Returns the CompositePathView instance used by the TileView to draw and scale paths. - * - * @return The CompositePathView instance. - */ - public CompositePathView getCompositePathView() { - return mCompositePathView; - } - - /** - * Returns the TileCanvasViewGroup instance used by the TileView to manage tile bitmap rendering. - * - * @return The TileCanvasViewGroup instance. - */ - public TileCanvasViewGroup getTileCanvasViewGroup() { - return mTileCanvasViewGroup; - } - - /** - * Returns the MakerLayout instance used by the TileView to position and display Views used - * as markers. - * - * @return The MarkerLayout instance. - */ - public MarkerLayout getMarkerLayout() { - return mMarkerLayout; - } - - /** - * Returns the CalloutLayout instance used by the TileView to position and display Views used - * as callouts. - * - * @return The CalloutLayout instance. - */ - public CalloutLayout getCalloutLayout() { - return mCalloutLayout; - } - - /** - * Returns the ScalingLayout instance used by the TileView to allow insertion of arbitrary - * Views and ViewGroups that will scale visually with the TileView. - * - * @return The ScalingLayout instance. - */ - public ScalingLayout getScalingLayout() { - return mScalingLayout; - } - - /** - * Add a ViewGroup to the TileView at a z-index above tiles and paths but beneath - * markers and callouts. The ViewGroup will be laid out to the full dimensions of the largest - * detail level, and will scale with the TileView. - * Note that only the drawing surface of the view is scaled, other operations that depend - * on dimensions are not (e.g., hit areas, invalidation tests). - * - * @param viewGroup The ViewGroup to be added to the TileView, that will scale visually. - */ - public void addScalingViewGroup( ViewGroup viewGroup ) { - mScalingLayout.addView( viewGroup ); - } - - /** - * Request that the current tile set is re-examined and re-drawn. - * The request is added to a queue and is not guaranteed to be processed at any particular - * time, and will never be handled immediately. - */ - public void requestRender() { - mTileCanvasViewGroup.requestRender(); - } - - /** - * While all render operation requests are queued and batched, this method provides an additional - * throttle layer, so that any subsequent invocations cancel and pending invocations. - * - * This is useful when requesting in a stream fashion, either in a loop or in response to a - * progressive action like an animation or touch move. - */ - public void requestThrottledRender() { - mRenderThrottleHandler.submit(); - } - - /** - * If flinging, defer render, otherwise request now. - * If a render operation starts at the beginning of a fling, a stutter can occur. - */ - protected void requestSafeRender() { - if( isFlinging() ) { - requestThrottledRender(); - } else { - requestRender(); - } - } - - /** - * Notify the TileView that it may stop rendering tiles. The rendering thread will be - * sent an interrupt request, but no guarantee is provided when the request will be responded to. - */ - public void cancelRender() { - mTileCanvasViewGroup.cancelRender(); - } - - /** - * Notify the TileView that it should continue to render any pending tiles, but should not - * accept new render tasks. - */ - public void suppressRender() { - mTileCanvasViewGroup.suppressRender(); - } - - /** - * Notify the TileView that it should resume tiles rendering. - */ - public void resumeRender() { - mTileCanvasViewGroup.resumeRender(); - } - - /** - * Sets a custom class to perform the getBitmap operation when tile bitmaps are requested for - * tile images only. - * By default, a BitmapDecoder implementation is provided that renders bitmaps from the context's - * Assets, but alternative implementations could be used that fetch images via HTTP, or from the - * SD card, or resources, SVG, etc. - * - * @param bitmapProvider A class instance that implements BitmapProvider, and must define a getBitmap method, which accepts a String file name and a Context object, and returns a Bitmap - */ - public void setBitmapProvider( BitmapProvider bitmapProvider ) { - mTileCanvasViewGroup.setBitmapProvider( bitmapProvider ); - } - - /** - * Defines whether tile bitmaps should be rendered using an AlphaAnimation - * - * @param enabled True if the TileView should render tiles with fade transitions - */ - public void setTransitionsEnabled( boolean enabled ) { - mTileCanvasViewGroup.setTransitionsEnabled( enabled ); - } - - /** - * Instructs Tile instances to recycle (or not). This can be useful if using a caching system - * that re-uses bitmaps and expects them to not have been recycled. - * - * The default value is true. - * - * @deprecated This value is no longer considered - bitmaps are always recycled when they're no longer used. - * @param shouldRecycleBitmaps True if bitmaps should call Bitmap.recycle when they are removed from view. - */ - public void setShouldRecycleBitmaps( boolean shouldRecycleBitmaps ) { - mTileCanvasViewGroup.setShouldRecycleBitmaps( shouldRecycleBitmaps ); - } - - /** - * Defines the total size, in pixels, of the tile set at 100% scale. - * The TileView wills pan within it's layout dimensions, with the content (scrollable) - * size defined by this method. - * - * @param width Total width of the tiled set. - * @param height Total height of the tiled set. - */ - @Override - public void setSize( int width, int height ) { - super.setSize( width, height ); - mDetailLevelManager.setSize( width, height ); - mCoordinateTranslater.setSize( width, height ); - } - - /** - * Register a tile set to be used for a particular detail level. - * Each tile set to be used must be registered using this method, - * and at least one tile set must be registered for the TileView to render any tiles. - * - * @param detailScale Scale at which the TileView should use the tiles in this set. - * @param data An arbitrary object of any type that is passed to the BitmapProvider for each tile on this level. - */ - public void addDetailLevel( float detailScale, Object data ) { - addDetailLevel( detailScale, data, DEFAULT_TILE_SIZE, DEFAULT_TILE_SIZE ); - } - - /** - * Register a tile set to be used for a particular detail level. - * Each tile set to be used must be registered using this method, - * and at least one tile set must be registered for the TileView to render any tiles. - * - * @param detailScale Scale at which the TileView should use the tiles in this set. - * @param data An arbitrary object of any type that is passed to the (Adapter|Decoder) for each tile on this level. - * @param tileWidth Size of each tiled column. - * @param tileHeight Size of each tiled row. - */ - public void addDetailLevel( float detailScale, Object data, int tileWidth, int tileHeight ) { - mDetailLevelManager.addDetailLevel( detailScale, data, tileWidth, tileHeight ); - } - - /** - * Register a tile set to be used for a particular detail level. - * Each tile set to be used must be registered using this method, - * and at least one tile set must be registered for the TileView to render any tiles. - * - * @param detailScale Scale at which the TileView should use the tiles in this set. - * @param data An arbitrary object of any type that is passed to the (Adapter|Decoder) for each tile on this level. - * @param tileWidth Size of each tiled column. - * @param tileHeight Size of each tiled row. - * @param levelType Type of level, detail-levels can have the same scale but different looks. - */ - public void addDetailLevel(float detailScale, Object data, int tileWidth, int tileHeight, DetailLevelManager.LevelType levelType ) { - mDetailLevelManager.addDetailLevel( detailScale, data, tileWidth, tileHeight, levelType ); - } - - /** - * Pads the viewport by the number of pixels passed. e.g., setViewportPadding( 100 ) instructs the - * TileView to interpret it's actual viewport offset by 100 pixels in each direction (top, left, - * right, bottom), so more tiles will qualify for "visible" status when intersections are calculated. - * - * @param padding The number of pixels to pad the viewport by - */ - public void setViewportPadding( int padding ) { - mDetailLevelManager.setViewportPadding( padding ); - } - - /** - * Register a set of offset points to use when calculating position within the TileView. - * Any type of coordinate system can be used (any type of lat/lng, percentile-based, etc), - * and all positioned are calculated relatively. If relative bounds are defined, position parameters - * received by TileView methods will be translated to the the appropriate pixel value. - * To remove this process, use undefineBounds. - * - * @param left The left edge of the rectangle used when calculating position. - * @param top The top edge of the rectangle used when calculating position. - * @param right The right edge of the rectangle used when calculating position. - * @param bottom The bottom edge of the rectangle used when calculating position. - */ - public void defineBounds( double left, double top, double right, double bottom ) { - mCoordinateTranslater.setBounds( left, top, right, bottom ); - } - - /** - * Unregisters arbitrary bounds and coordinate system. After invoking this method, - * TileView methods that receive position method parameters will use pixel values, - * relative to the TileView's registered size (at 1.0f scale). - */ - public void undefineBounds() { - mCoordinateTranslater.unsetBounds(); - } - - /** - * Scrolls (instantly) the TileView to the x and y positions provided. The is an overload - * of scrollTo( int x, int y ) that accepts doubles; if the TileView has relative bounds defined, - * those relative doubles will be converted to absolute pixel positions. - * - * @param x The relative x position to move to. - * @param y The relative y position to move to. - */ - public void scrollTo( double x, double y ) { - scrollTo( - mCoordinateTranslater.translateAndScaleX( x, getScale() ), - mCoordinateTranslater.translateAndScaleY( y, getScale() ) - ); - } - - /** - * Scrolls (instantly) the TileView to the x and y positions provided, - * then centers the viewport to the position. - * - * @param x The relative x position to move to. - * @param y The relative y position to move to. - */ - public void scrollToAndCenter( double x, double y ) { - scrollToAndCenter( - mCoordinateTranslater.translateAndScaleX( x, getScale() ), - mCoordinateTranslater.translateAndScaleY( y, getScale() ) - ); - } - - /** - * Scrolls (with animation) the TileView to the relative x and y positions provided. - * - * @param x The relative x position to move to. - * @param y The relative y position to move to. - */ - public void slideTo( double x, double y ) { - slideTo( - mCoordinateTranslater.translateAndScaleX( x, getScale() ), - mCoordinateTranslater.translateAndScaleY( y, getScale() ) - ); - } - - /** - * Scrolls (with animation) the TileView to the x and y positions provided, - * then centers the viewport to the position. - * - * @param x The relative x position to move to. - * @param y The relative y position to move to. - */ - public void slideToAndCenter( double x, double y ) { - slideToAndCenter( - mCoordinateTranslater.translateAndScaleX( x, getScale() ), - mCoordinateTranslater.translateAndScaleY( y, getScale() ) - ); - } - - /** - * Scrolls and scales (with animation) the TileView to the specified x, y and scale provided. - * The TileView will be centered to the coordinates passed. - * - * @param x The relative x position to move to. - * @param y The relative y position to move to. - * @param scale The scale the TileView should be at when the animation is complete. - */ - public void slideToAndCenterWithScale( double x, double y, float scale ) { - slideToAndCenterWithScale( - mCoordinateTranslater.translateAndScaleX( x, scale ), - mCoordinateTranslater.translateAndScaleY( y, scale ), - scale - ); - } - - /** - * Markers added to this TileView will have anchor logic applied on the values provided here. - * E.g., setMarkerAnchorPoints(-0.5f, -1.0f) will have markers centered horizontally, and aligned - * along the bottom edge to the y value supplied. - * - * Anchor values assigned to individual markers will override these default values. - * - * @param anchorX The x-axis position of a marker will be offset by a number equal to the width of the marker multiplied by this value. - * @param anchorY The y-axis position of a marker will be offset by a number equal to the height of the marker multiplied by this value. - */ - public void setMarkerAnchorPoints( Float anchorX, Float anchorY ) { - mMarkerLayout.setAnchors( anchorX, anchorY ); - } - - /** - * Add a marker to the the TileView. The marker can be any View. - * No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters. - * - * @param view View instance to be added to the TileView. - * @param x Relative x position the View instance should be positioned at. - * @param y Relative y position the View instance should be positioned at. - * @param anchorX The x-axis position of a marker will be offset by a number equal to the width of the marker multiplied by this value. - * @param anchorY The y-axis position of a marker will be offset by a number equal to the height of the marker multiplied by this value. - * @return The View instance added to the TileView. - */ - public View addMarker( View view, double x, double y, Float anchorX, Float anchorY ) { - return mMarkerLayout.addMarker( view, - mCoordinateTranslater.translateX( x ), - mCoordinateTranslater.translateY( y ), - anchorX, anchorY - ); - } - - /** - * Removes a marker View from the TileView's view tree. - * - * @param view The marker View to be removed. - */ - public void removeMarker( View view ) { - mMarkerLayout.removeMarker( view ); - } - - /** - * Moves an existing marker to another position. - * - * @param view The marker View to be repositioned. - * @param x Relative x position the View instance should be positioned at. - * @param y Relative y position the View instance should be positioned at. - */ - public void moveMarker( View view, double x, double y ) { - mMarkerLayout.moveMarker( view, - mCoordinateTranslater.translateX( x ), - mCoordinateTranslater.translateY( y ) ); - } - - /** - * Scroll the TileView so that the View passed is centered in the viewport. - * - * @param view The View marker that the TileView should center on. - * @param shouldAnimate True if the movement should use a transition effect. - */ - public void moveToMarker( View view, boolean shouldAnimate ) { - if( mMarkerLayout.indexOfChild( view ) == -1 ) { - throw new IllegalStateException( "The view passed is not an existing marker" ); - } - ViewGroup.LayoutParams params = view.getLayoutParams(); - if( params instanceof MarkerLayout.LayoutParams ) { - MarkerLayout.LayoutParams anchorLayoutParams = (MarkerLayout.LayoutParams) params; - int scaledX = FloatMathHelper.scale( anchorLayoutParams.x, getScale() ); - int scaledY = FloatMathHelper.scale( anchorLayoutParams.y, getScale() ); - if( shouldAnimate ) { - slideToAndCenter( scaledX, scaledY ); - } else { - scrollToAndCenter( scaledX, scaledY ); - } - } - } - - /** - * Register a MarkerTapListener for the TileView instance (rather than on a single marker view). - * Unlike standard touch events attached to marker View's (e.g., View.OnClickListener), - * MarkerTapListener.onMarkerTapEvent does not consume the touch event, so will not interfere - * with scrolling. - * - * @param markerTapListener Listener to be added to the TileView's list of MarkerTapListener. - */ - public void setMarkerTapListener( MarkerLayout.MarkerTapListener markerTapListener ) { - mMarkerLayout.setMarkerTapListener( markerTapListener ); - } - - /** - * Add a callout to the the TileView. The callout can be any View. - * No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both - * width and height, and positioned according to the x and y values supplied. - * Callout views will always be positioned at the top of the view tree (at the highest z-index), - * and will always be removed during any touch event that is not consumed by the callout View. - * - * @param view View instance to be added to the TileView. - * @param x Relative x position the View instance should be positioned at. - * @param y Relative y position the View instance should be positioned at. - * @param anchorX The x-axis position of a callout view will be offset by a number equal to the width of the callout view multiplied by this value. - * @param anchorY The y-axis position of a callout view will be offset by a number equal to the height of the callout view multiplied by this value. - * @return The View instance added to the TileView. - */ - public View addCallout( View view, double x, double y, Float anchorX, Float anchorY ) { - return mCalloutLayout.addMarker( view, - mCoordinateTranslater.translateX( x ), - mCoordinateTranslater.translateY( y ), - anchorX, anchorY - ); - } - - /** - * Removes a callout View from the TileView. - * - * @param view The callout View to be removed. - */ - public void removeCallout( View view ) { - mCalloutLayout.removeMarker( view ); - } - - /** - * Register a HotSpot that should fire a listener when a touch event occurs that intersects the - * Region defined by the HotSpot. - * - * The HotSpot virtually moves and scales with the TileView. - * - * @param hotSpot The hotspot that is tested against touch events that occur on the TileView. - * @return The HotSpot instance added. - */ - public HotSpot addHotSpot( HotSpot hotSpot ) { - mHotSpotManager.addHotSpot( hotSpot ); - return hotSpot; - } - - /** - * Register a HotSpot that should fire a listener when a touch event occurs that intersects the - * Region defined by the HotSpot. - * - * The HotSpot virtually moves and scales with the TileView. - * - * @param positions (List) List of paired doubles that represents the region. - * @return HotSpot the hotspot created with this method. - */ - public HotSpot addHotSpot( List positions, HotSpot.HotSpotTapListener listener ) { - Path path = mCoordinateTranslater.pathFromPositions( positions, true ); - RectF bounds = new RectF(); - path.computeBounds( bounds, true ); - Rect rect = new Rect(); - bounds.round( rect ); - Region clip = new Region( rect ); - HotSpot hotSpot = new HotSpot(); - hotSpot.setPath( path, clip ); - hotSpot.setHotSpotTapListener( listener ); - return addHotSpot( hotSpot ); - } - - /** - * Remove a HotSpot registered with addHotSpot. - * - * @param hotSpot The HotSpot instance to remove. - */ - public void removeHotSpot( HotSpot hotSpot ) { - mHotSpotManager.removeHotSpot( hotSpot ); - } - - /** - * Register a HotSpotTapListener with the TileView. This listener will fire if any registered - * HotSpot's region intersects a Tap event. - * - * @param hotSpotTapListener The listener to be added. - */ - public void setHotSpotTapListener( HotSpot.HotSpotTapListener hotSpotTapListener ) { - mHotSpotManager.setHotSpotTapListener( hotSpotTapListener ); - } - - /** - * Register a DrawablePath that will be drawn on a layer above the tiles, but below markers. - * The Path will be scaled with the TileView, but will always be as wide as the stroke set - * for the Paint instance associated with the DrawablePath. - * - * @param drawablePath DrawablePath instance to be drawn by the TileView. - * @return The DrawablePath instance passed to the TileView. - */ - public CompositePathView.DrawablePath drawPath( CompositePathView.DrawablePath drawablePath ) { - return mCompositePathView.addPath( drawablePath ); - } - - /** - * Register a Path and Paint that will be drawn on a layer above the tiles, but below markers. - * The Path will be scaled with the TileView, but will always be as wide as the stroke set - * for the Paint. - * - * @param positions List of doubles that represent the points of the Path. - * @param paint The Paint instance that defines the style of the drawn path. - * @return The DrawablePath instance passed to the TileView. - */ - public CompositePathView.DrawablePath drawPath( List positions, Paint paint ) { - Path path = mCoordinateTranslater.pathFromPositions( positions, false ); - return mCompositePathView.addPath( path, paint ); - } - - /** - * Removes a DrawablePath from the TileView's registry. This path will no longer be drawn by the - * TileView. - * - * @param drawablePath The DrawablePath instance to be removed. - */ - public void removePath( CompositePathView.DrawablePath drawablePath ) { - mCompositePathView.removePath( drawablePath ); - } - - /** - * Returns the Paint instance used by the CompositePathView by default. This can be modified for - * future Path paint operations. - * - * @return The Paint instance used by default. - */ - public Paint getDefaultPathPaint() { - return mCompositePathView.getDefaultPaint(); - } - - /** - * Recycles bitmap image files, prevents path drawing, and clears pending Handler messages, - * appropriate for Activity.onPause. - */ - public void pause() { - mRenderThrottleHandler.clear(); - mDetailLevelManager.invalidateAll(); - setWillNotDraw( true ); - } - - /** - * Clear tile image files and remove all views, appropriate for Activity.onDestroy. - * After invoking this method, the TileView instance should be removed from any view trees, - * and references to it should be set to null. - */ - public void destroy() { - pause(); - mTileCanvasViewGroup.destroy(); - mCompositePathView.clear(); - removeAllViews(); - } - - /** - * Restore visible state (generally after a call to pause). - * Appropriate for Activity.onResume. - */ - public void resume() { - setWillNotDraw( false ); - updateViewport(); - mTileCanvasViewGroup.updateTileSet( mDetailLevelManager.getCurrentDetailLevel() ); - requestRender(); - requestLayout(); - } - - /** - * Allows the TileView to render tiles while panning. - * - * @param shouldRender True if it should render while panning. - */ - public void setShouldRenderWhilePanning( boolean shouldRender ) { - mShouldRenderWhilePanning = shouldRender; - int buffer = shouldRender ? TileCanvasViewGroup.FAST_RENDER_BUFFER : TileCanvasViewGroup.DEFAULT_RENDER_BUFFER; - mTileCanvasViewGroup.setRenderBuffer( buffer ); - } - - /** - * By default, when a zoom begins, the current {@link DetailLevel} is locked so it is used to - * provide tiles until the zoom ends. This ensures that the {@link TileView} is updated - * consistently. - *

- * However, a zoom out may require a lot of tiles of the locked {@code DetailLevel} to be rendered. - * In worst case, it can cause {@link OutOfMemoryError}. - * Then, disabling the {@code DetailLevel} lock is a bandage to that issue. Using - * {@code setShouldUpdateDetailLevelWhileZooming( true )} is not advised unless you have that issue. - *

- * - * @param shouldUpdate True if it should lock {@link DetailLevel} when a zoom begins. - */ - public void setShouldUpdateDetailLevelWhileZooming( boolean shouldUpdate ) { - mShouldUpdateDetailLevelWhileZooming = shouldUpdate; - } - - /** - * Allows the use of a custom {@link DetailLevelManager}. - *

- * For example, to change the logic of {@link DetailLevel} choice for a given scale, you - * declare your own {@code DetailLevelMangerCustom} that extends {@link DetailLevelManager} : - *

{@code
-   * private class DetailLevelManagerCustom extends DetailLevelManager{
-   *  @literal @Override
-   *   public DetailLevel getDetailLevelForScale(){
-   *     // your logic here
-   *   }
-   * }
-   * }
-   * 
- * Then you should use {@code TileView.setDetailLevelManager} before other method calls, especially - * {@code TileView.setSize} and {@code TileView.addDetailLevel}. - *

- * - * @param manager The DetailLevelManager instance used. - */ - public void setDetailLevelManager( DetailLevelManager manager ) { - mDetailLevelManager = manager; - mDetailLevelManager.setDetailLevelChangeListener(this); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - mCalloutLayout.removeAllViews(); - return super.onTouchEvent( event ); - } - - @Override - protected void onLayout( boolean changed, int l, int t, int r, int b ) { - super.onLayout( changed, l, t, r, b ); - updateViewport(); - requestRender(); - } - - protected void updateViewport() { - int left = getScrollX(); - int top = getScrollY(); - int right = left + getWidth(); - int bottom = top + getHeight(); - mDetailLevelManager.updateViewport( left, top, right, bottom ); - } - - @Override - protected void onScrollChanged( int l, int t, int oldl, int oldt ) { - super.onScrollChanged( l, t, oldl, oldt ); - updateViewport(); - if( mShouldRenderWhilePanning ) { - requestRender(); - } else { - requestThrottledRender(); - } - } - - @Override - public void onScaleChanged( float scale, float previous ) { - super.onScaleChanged( scale, previous ); - mDetailLevelManager.setScale( scale ); - mHotSpotManager.setScale( scale ); - mTileCanvasViewGroup.setScale( scale ); - mScalingLayout.setScale( scale ); - mCompositePathView.setScale( scale ); - mMarkerLayout.setScale( scale ); - mCalloutLayout.setScale( scale ); - } - - @Override - public void onPanBegin( int x, int y, Origination origin ) { - - } - - @Override - public void onPanUpdate( int x, int y, Origination origin ) { - - } - - @Override - public void onPanEnd( int x, int y, Origination origin ) { - requestRender(); - } - - @Override - public void onZoomBegin( float scale, Origination origin ) { - if ( origin == null ) { - mTileCanvasViewGroup.suppressRender(); - } - mDetailLevelManager.setScale( scale ); - } - - @Override - public void onZoomUpdate( float scale, Origination origin ) { - - } - - @Override - public void onZoomEnd( float scale, Origination origin ) { - if ( origin == null ) { - mTileCanvasViewGroup.resumeRender(); - } - mDetailLevelManager.setScale( scale ); - requestRender(); - } - - @Override - public void onDetailLevelChanged( DetailLevel detailLevel ) { - requestRender(); - mTileCanvasViewGroup.updateTileSet( detailLevel ); - } - - @Override - public boolean onSingleTapConfirmed( MotionEvent event ) { - int x = getScrollX() + (int) event.getX() - getOffsetX(); - int y = getScrollY() + (int) event.getY() - getOffsetY(); - mMarkerLayout.processHit( x, y ); - mHotSpotManager.processHit( x, y ); - return super.onSingleTapConfirmed( event ); - } - - @Override - public void onRenderStart() { + ZoomPanLayout.ZoomPanListener, + TileCanvasViewGroup.TileRenderListener, + DetailLevelManager.DetailLevelChangeListener { + + protected static final int DEFAULT_TILE_SIZE = 256; + + private DetailLevelManager mDetailLevelManager = new DetailLevelManager(); + private CoordinateTranslater mCoordinateTranslater = new CoordinateTranslater(); + private HotSpotManager mHotSpotManager = new HotSpotManager(); + + private TileCanvasViewGroup mTileCanvasViewGroup; + private CompositePathView mCompositePathView; + private ScalingLayout mScalingLayout; + private MarkerLayout mMarkerLayout; + private CalloutLayout mCalloutLayout; + + private RenderThrottleHandler mRenderThrottleHandler; + + private boolean mShouldRenderWhilePanning = false; + private boolean mShouldUpdateDetailLevelWhileZooming = false; + + /** + * Constructor to use when creating a TileView from code. + * + * @param context The Context the TileView is running in, through which it can access the current theme, resources, etc. + */ + public TileView(Context context) { + this(context, null); + } + + public TileView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TileView(Context context, AttributeSet attrs, int defStyleAttr) { + + super(context, attrs, defStyleAttr); + + mTileCanvasViewGroup = new TileCanvasViewGroup(context); + addView(mTileCanvasViewGroup); + + mCompositePathView = new CompositePathView(context); + addView(mCompositePathView); + + mScalingLayout = new ScalingLayout(context); + addView(mScalingLayout); + + mMarkerLayout = new MarkerLayout(context); + addView(mMarkerLayout); + + mCalloutLayout = new CalloutLayout(context); + addView(mCalloutLayout); + + mDetailLevelManager.setDetailLevelChangeListener(this); + mTileCanvasViewGroup.setTileRenderListener(this); + addZoomPanListener(this); + + mRenderThrottleHandler = new RenderThrottleHandler(this); + + requestRender(); + + } + + /** + * Returns the DetailLevelManager instance used by the TileView to coordinate DetailLevels. + * + * @return The DetailLevelManager instance. + */ + public DetailLevelManager getDetailLevelManager() { + return mDetailLevelManager; + } + + /** + * Allows the use of a custom {@link DetailLevelManager}. + *

+ * For example, to change the logic of {@link DetailLevel} choice for a given scale, you + * declare your own {@code DetailLevelMangerCustom} that extends {@link DetailLevelManager} : + *

{@code
+     * private class DetailLevelManagerCustom extends DetailLevelManager{
+     *  @literal @Override
+     *   public DetailLevel getDetailLevelForScale(){
+     *     // your logic here
+     *   }
+     * }
+     * }
+     * 
+ * Then you should use {@code TileView.setDetailLevelManager} before other method calls, especially + * {@code TileView.setSize} and {@code TileView.addDetailLevel}. + *

+ * + * @param manager The DetailLevelManager instance used. + */ + public void setDetailLevelManager(DetailLevelManager manager) { + mDetailLevelManager = manager; + mDetailLevelManager.setDetailLevelChangeListener(this); + } - } + /** + * Returns the CoordinateTranslater instance used by the TileView to manage abritrary coordinate + * systems. + * + * @return The CoordinateTranslater instance. + */ + public CoordinateTranslater getCoordinateTranslater() { + return mCoordinateTranslater; + } + + /** + * Returns the HotSpotManager instance used by the TileView to detect and react to touch events + * that intersect a user-defined region. + * + * @return The HotSpotManager instance. + */ + public HotSpotManager getHotSpotManager() { + return mHotSpotManager; + } - @Override - public void onRenderCancelled() { + /** + * Returns the CompositePathView instance used by the TileView to draw and scale paths. + * + * @return The CompositePathView instance. + */ + public CompositePathView getCompositePathView() { + return mCompositePathView; + } - } + /** + * Returns the TileCanvasViewGroup instance used by the TileView to manage tile bitmap rendering. + * + * @return The TileCanvasViewGroup instance. + */ + public TileCanvasViewGroup getTileCanvasViewGroup() { + return mTileCanvasViewGroup; + } - @Override - public void onRenderComplete() { + /** + * Returns the MakerLayout instance used by the TileView to position and display Views used + * as markers. + * + * @return The MarkerLayout instance. + */ + public MarkerLayout getMarkerLayout() { + return mMarkerLayout; + } - } + /** + * Returns the CalloutLayout instance used by the TileView to position and display Views used + * as callouts. + * + * @return The CalloutLayout instance. + */ + public CalloutLayout getCalloutLayout() { + return mCalloutLayout; + } + + /** + * Returns the ScalingLayout instance used by the TileView to allow insertion of arbitrary + * Views and ViewGroups that will scale visually with the TileView. + * + * @return The ScalingLayout instance. + */ + public ScalingLayout getScalingLayout() { + return mScalingLayout; + } + + /** + * Add a ViewGroup to the TileView at a z-index above tiles and paths but beneath + * markers and callouts. The ViewGroup will be laid out to the full dimensions of the largest + * detail level, and will scale with the TileView. + * Note that only the drawing surface of the view is scaled, other operations that depend + * on dimensions are not (e.g., hit areas, invalidation tests). + * + * @param viewGroup The ViewGroup to be added to the TileView, that will scale visually. + */ + public void addScalingViewGroup(ViewGroup viewGroup) { + mScalingLayout.addView(viewGroup); + } + + /** + * Request that the current tile set is re-examined and re-drawn. + * The request is added to a queue and is not guaranteed to be processed at any particular + * time, and will never be handled immediately. + */ + public void requestRender() { + mTileCanvasViewGroup.requestRender(); + } + + /** + * While all render operation requests are queued and batched, this method provides an additional + * throttle layer, so that any subsequent invocations cancel and pending invocations. + *

+ * This is useful when requesting in a stream fashion, either in a loop or in response to a + * progressive action like an animation or touch move. + */ + public void requestThrottledRender() { + mRenderThrottleHandler.submit(); + } + + /** + * If flinging, defer render, otherwise request now. + * If a render operation starts at the beginning of a fling, a stutter can occur. + */ + protected void requestSafeRender() { + if (isFlinging()) { + requestThrottledRender(); + } else { + requestRender(); + } + } + + /** + * Notify the TileView that it may stop rendering tiles. The rendering thread will be + * sent an interrupt request, but no guarantee is provided when the request will be responded to. + */ + public void cancelRender() { + mTileCanvasViewGroup.cancelRender(); + } + + /** + * Notify the TileView that it should continue to render any pending tiles, but should not + * accept new render tasks. + */ + public void suppressRender() { + mTileCanvasViewGroup.suppressRender(); + } - private static class RenderThrottleHandler extends Handler { + /** + * Notify the TileView that it should resume tiles rendering. + */ + public void resumeRender() { + mTileCanvasViewGroup.resumeRender(); + } - private static final int MESSAGE = 0; - private static final int RENDER_THROTTLE_TIMEOUT = 100; + /** + * Sets a custom class to perform the getBitmap operation when tile bitmaps are requested for + * tile images only. + * By default, a BitmapDecoder implementation is provided that renders bitmaps from the context's + * Assets, but alternative implementations could be used that fetch images via HTTP, or from the + * SD card, or resources, SVG, etc. + * + * @param bitmapProvider A class instance that implements BitmapProvider, and must define a getBitmap method, which accepts a String file name and a Context object, and returns a Bitmap + */ + public void setBitmapProvider(BitmapProvider bitmapProvider) { + mTileCanvasViewGroup.setBitmapProvider(bitmapProvider); + } - private final WeakReference mTileViewWeakReference; + /** + * Defines whether tile bitmaps should be rendered using an AlphaAnimation + * + * @param enabled True if the TileView should render tiles with fade transitions + */ + public void setTransitionsEnabled(boolean enabled) { + mTileCanvasViewGroup.setTransitionsEnabled(enabled); + } - public RenderThrottleHandler( TileView tileView ) { - super(); - mTileViewWeakReference = new WeakReference( tileView ); + /** + * Instructs Tile instances to recycle (or not). This can be useful if using a caching system + * that re-uses bitmaps and expects them to not have been recycled. + *

+ * The default value is true. + * + * @param shouldRecycleBitmaps True if bitmaps should call Bitmap.recycle when they are removed from view. + * @deprecated This value is no longer considered - bitmaps are always recycled when they're no longer used. + */ + public void setShouldRecycleBitmaps(boolean shouldRecycleBitmaps) { + mTileCanvasViewGroup.setShouldRecycleBitmaps(shouldRecycleBitmaps); } + /** + * Defines the total size, in pixels, of the tile set at 100% scale. + * The TileView wills pan within it's layout dimensions, with the content (scrollable) + * size defined by this method. + * + * @param width Total width of the tiled set. + * @param height Total height of the tiled set. + */ @Override - public void handleMessage( Message msg ) { - TileView tileView = mTileViewWeakReference.get(); - if( tileView != null ) { - tileView.requestSafeRender(); - } + public void setSize(int width, int height) { + super.setSize(width, height); + mDetailLevelManager.setSize(width, height); + mCoordinateTranslater.setSize(width, height); + } + + /** + * Register a tile set to be used for a particular detail level. + * Each tile set to be used must be registered using this method, + * and at least one tile set must be registered for the TileView to render any tiles. + * + * @param detailScale Scale at which the TileView should use the tiles in this set. + * @param data An arbitrary object of any type that is passed to the BitmapProvider for each tile on this level. + */ + public void addDetailLevel(float detailScale, Object data) { + addDetailLevel(detailScale, data, DEFAULT_TILE_SIZE, DEFAULT_TILE_SIZE); + } + + /** + * Register a tile set to be used for a particular detail level. + * Each tile set to be used must be registered using this method, + * and at least one tile set must be registered for the TileView to render any tiles. + * + * @param detailScale Scale at which the TileView should use the tiles in this set. + * @param data An arbitrary object of any type that is passed to the (Adapter|Decoder) for each tile on this level. + * @param tileWidth Size of each tiled column. + * @param tileHeight Size of each tiled row. + */ + public void addDetailLevel(float detailScale, Object data, int tileWidth, int tileHeight) { + mDetailLevelManager.addDetailLevel(detailScale, data, tileWidth, tileHeight); + } + + /** + * Register a tile set to be used for a particular detail level. + * Each tile set to be used must be registered using this method, + * and at least one tile set must be registered for the TileView to render any tiles. + * + * @param detailScale Scale at which the TileView should use the tiles in this set. + * @param data An arbitrary object of any type that is passed to the (Adapter|Decoder) for each tile on this level. + * @param tileWidth Size of each tiled column. + * @param tileHeight Size of each tiled row. + * @param levelType Type of level, detail-levels can have the same scale but different looks. + */ + public void addDetailLevel(float detailScale, Object data, int tileWidth, int tileHeight, DetailLevelManager.LevelType levelType) { + mDetailLevelManager.addDetailLevel(detailScale, data, tileWidth, tileHeight, levelType); + } + + /** + * Pads the viewport by the number of pixels passed. e.g., setViewportPadding( 100 ) instructs the + * TileView to interpret it's actual viewport offset by 100 pixels in each direction (top, left, + * right, bottom), so more tiles will qualify for "visible" status when intersections are calculated. + * + * @param padding The number of pixels to pad the viewport by + */ + public void setViewportPadding(int padding) { + mDetailLevelManager.setViewportPadding(padding); + } + + /** + * Register a set of offset points to use when calculating position within the TileView. + * Any type of coordinate system can be used (any type of lat/lng, percentile-based, etc), + * and all positioned are calculated relatively. If relative bounds are defined, position parameters + * received by TileView methods will be translated to the the appropriate pixel value. + * To remove this process, use undefineBounds. + * + * @param left The left edge of the rectangle used when calculating position. + * @param top The top edge of the rectangle used when calculating position. + * @param right The right edge of the rectangle used when calculating position. + * @param bottom The bottom edge of the rectangle used when calculating position. + */ + public void defineBounds(double left, double top, double right, double bottom) { + mCoordinateTranslater.setBounds(left, top, right, bottom); + } + + /** + * Unregisters arbitrary bounds and coordinate system. After invoking this method, + * TileView methods that receive position method parameters will use pixel values, + * relative to the TileView's registered size (at 1.0f scale). + */ + public void undefineBounds() { + mCoordinateTranslater.unsetBounds(); + } + + /** + * Scrolls (instantly) the TileView to the x and y positions provided. The is an overload + * of scrollTo( int x, int y ) that accepts doubles; if the TileView has relative bounds defined, + * those relative doubles will be converted to absolute pixel positions. + * + * @param x The relative x position to move to. + * @param y The relative y position to move to. + */ + public void scrollTo(double x, double y) { + scrollTo( + mCoordinateTranslater.translateAndScaleX(x, getScale()), + mCoordinateTranslater.translateAndScaleY(y, getScale()) + ); + } + + /** + * Scrolls (instantly) the TileView to the x and y positions provided, + * then centers the viewport to the position. + * + * @param x The relative x position to move to. + * @param y The relative y position to move to. + */ + public void scrollToAndCenter(double x, double y) { + scrollToAndCenter( + mCoordinateTranslater.translateAndScaleX(x, getScale()), + mCoordinateTranslater.translateAndScaleY(y, getScale()) + ); + } + + /** + * Scrolls (with animation) the TileView to the relative x and y positions provided. + * + * @param x The relative x position to move to. + * @param y The relative y position to move to. + */ + public void slideTo(double x, double y) { + slideTo( + mCoordinateTranslater.translateAndScaleX(x, getScale()), + mCoordinateTranslater.translateAndScaleY(y, getScale()) + ); } - public void clear() { - if( hasMessages( MESSAGE ) ) { - removeMessages( MESSAGE ); - } + /** + * Scrolls (with animation) the TileView to the x and y positions provided, + * then centers the viewport to the position. + * + * @param x The relative x position to move to. + * @param y The relative y position to move to. + */ + public void slideToAndCenter(double x, double y) { + slideToAndCenter( + mCoordinateTranslater.translateAndScaleX(x, getScale()), + mCoordinateTranslater.translateAndScaleY(y, getScale()) + ); } - public void submit() { - clear(); - sendEmptyMessageDelayed( MESSAGE, RENDER_THROTTLE_TIMEOUT ); + /** + * Scrolls and scales (with animation) the TileView to the specified x, y and scale provided. + * The TileView will be centered to the coordinates passed. + * + * @param x The relative x position to move to. + * @param y The relative y position to move to. + * @param scale The scale the TileView should be at when the animation is complete. + */ + public void slideToAndCenterWithScale(double x, double y, float scale) { + slideToAndCenterWithScale( + mCoordinateTranslater.translateAndScaleX(x, scale), + mCoordinateTranslater.translateAndScaleY(y, scale), + scale + ); } - } - /** - * Object used to keep some data when a configuration change happens and the activity is - * re-created. - * It's boiler-plate but this is how to save View state. - */ - private static class SavedState extends BaseSavedState { - /* This will store the current scale and position */ - float mScale; - int mSavedCenterX; - int mSavedCenterY; + /** + * Markers added to this TileView will have anchor logic applied on the values provided here. + * E.g., setMarkerAnchorPoints(-0.5f, -1.0f) will have markers centered horizontally, and aligned + * along the bottom edge to the y value supplied. + *

+ * Anchor values assigned to individual markers will override these default values. + * + * @param anchorX The x-axis position of a marker will be offset by a number equal to the width of the marker multiplied by this value. + * @param anchorY The y-axis position of a marker will be offset by a number equal to the height of the marker multiplied by this value. + */ + public void setMarkerAnchorPoints(Float anchorX, Float anchorY) { + mMarkerLayout.setAnchors(anchorX, anchorY); + } + + /** + * Add a marker to the the TileView. The marker can be any View. + * No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both width and height, and positioned based on the parameters. + * + * @param view View instance to be added to the TileView. + * @param x Relative x position the View instance should be positioned at. + * @param y Relative y position the View instance should be positioned at. + * @param anchorX The x-axis position of a marker will be offset by a number equal to the width of the marker multiplied by this value. + * @param anchorY The y-axis position of a marker will be offset by a number equal to the height of the marker multiplied by this value. + * @return The View instance added to the TileView. + */ + public View addMarker(View view, double x, double y, Float anchorX, Float anchorY) { + return mMarkerLayout.addMarker(view, + mCoordinateTranslater.translateX(x), + mCoordinateTranslater.translateY(y), + anchorX, anchorY + ); + } + + /** + * Removes a marker View from the TileView's view tree. + * + * @param view The marker View to be removed. + */ + public void removeMarker(View view) { + mMarkerLayout.removeMarker(view); + } + + /** + * Moves an existing marker to another position. + * + * @param view The marker View to be repositioned. + * @param x Relative x position the View instance should be positioned at. + * @param y Relative y position the View instance should be positioned at. + */ + public void moveMarker(View view, double x, double y) { + mMarkerLayout.moveMarker(view, + mCoordinateTranslater.translateX(x), + mCoordinateTranslater.translateY(y)); + } + + /** + * Scroll the TileView so that the View passed is centered in the viewport. + * + * @param view The View marker that the TileView should center on. + * @param shouldAnimate True if the movement should use a transition effect. + */ + public void moveToMarker(View view, boolean shouldAnimate) { + if (mMarkerLayout.indexOfChild(view) == -1) { + throw new IllegalStateException("The view passed is not an existing marker"); + } + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof MarkerLayout.LayoutParams) { + MarkerLayout.LayoutParams anchorLayoutParams = (MarkerLayout.LayoutParams) params; + int scaledX = FloatMathHelper.scale(anchorLayoutParams.x, getScale()); + int scaledY = FloatMathHelper.scale(anchorLayoutParams.y, getScale()); + if (shouldAnimate) { + slideToAndCenter(scaledX, scaledY); + } else { + scrollToAndCenter(scaledX, scaledY); + } + } + } + + /** + * Register a MarkerTapListener for the TileView instance (rather than on a single marker view). + * Unlike standard touch events attached to marker View's (e.g., View.OnClickListener), + * MarkerTapListener.onMarkerTapEvent does not consume the touch event, so will not interfere + * with scrolling. + * + * @param markerTapListener Listener to be added to the TileView's list of MarkerTapListener. + */ + public void setMarkerTapListener(MarkerLayout.MarkerTapListener markerTapListener) { + mMarkerLayout.setMarkerTapListener(markerTapListener); + } + + /** + * Add a callout to the the TileView. The callout can be any View. + * No LayoutParams are required; the View will be laid out using WRAP_CONTENT for both + * width and height, and positioned according to the x and y values supplied. + * Callout views will always be positioned at the top of the view tree (at the highest z-index), + * and will always be removed during any touch event that is not consumed by the callout View. + * + * @param view View instance to be added to the TileView. + * @param x Relative x position the View instance should be positioned at. + * @param y Relative y position the View instance should be positioned at. + * @param anchorX The x-axis position of a callout view will be offset by a number equal to the width of the callout view multiplied by this value. + * @param anchorY The y-axis position of a callout view will be offset by a number equal to the height of the callout view multiplied by this value. + * @return The View instance added to the TileView. + */ + public View addCallout(View view, double x, double y, Float anchorX, Float anchorY) { + return mCalloutLayout.addMarker(view, + mCoordinateTranslater.translateX(x), + mCoordinateTranslater.translateY(y), + anchorX, anchorY + ); + } + + /** + * Removes a callout View from the TileView. + * + * @param view The callout View to be removed. + */ + public void removeCallout(View view) { + mCalloutLayout.removeMarker(view); + } + + /** + * Register a HotSpot that should fire a listener when a touch event occurs that intersects the + * Region defined by the HotSpot. + *

+ * The HotSpot virtually moves and scales with the TileView. + * + * @param hotSpot The hotspot that is tested against touch events that occur on the TileView. + * @return The HotSpot instance added. + */ + public HotSpot addHotSpot(HotSpot hotSpot) { + mHotSpotManager.addHotSpot(hotSpot); + return hotSpot; + } + + /** + * Register a HotSpot that should fire a listener when a touch event occurs that intersects the + * Region defined by the HotSpot. + *

+ * The HotSpot virtually moves and scales with the TileView. + * + * @param positions (List) List of paired doubles that represents the region. + * @return HotSpot the hotspot created with this method. + */ + public HotSpot addHotSpot(List positions, HotSpot.HotSpotTapListener listener) { + Path path = mCoordinateTranslater.pathFromPositions(positions, true); + RectF bounds = new RectF(); + path.computeBounds(bounds, true); + Rect rect = new Rect(); + bounds.round(rect); + Region clip = new Region(rect); + HotSpot hotSpot = new HotSpot(); + hotSpot.setPath(path, clip); + hotSpot.setHotSpotTapListener(listener); + return addHotSpot(hotSpot); + } + + /** + * Remove a HotSpot registered with addHotSpot. + * + * @param hotSpot The HotSpot instance to remove. + */ + public void removeHotSpot(HotSpot hotSpot) { + mHotSpotManager.removeHotSpot(hotSpot); + } - SavedState( Parcelable superState ) { - super( superState ); + /** + * Register a HotSpotTapListener with the TileView. This listener will fire if any registered + * HotSpot's region intersects a Tap event. + * + * @param hotSpotTapListener The listener to be added. + */ + public void setHotSpotTapListener(HotSpot.HotSpotTapListener hotSpotTapListener) { + mHotSpotManager.setHotSpotTapListener(hotSpotTapListener); } - private SavedState( Parcel in ) { - super( in ); - mScale = in.readFloat(); - mSavedCenterX = in.readInt(); - mSavedCenterY = in.readInt(); + /** + * Register a DrawablePath that will be drawn on a layer above the tiles, but below markers. + * The Path will be scaled with the TileView, but will always be as wide as the stroke set + * for the Paint instance associated with the DrawablePath. + * + * @param drawablePath DrawablePath instance to be drawn by the TileView. + * @return The DrawablePath instance passed to the TileView. + */ + public CompositePathView.DrawablePath drawPath(CompositePathView.DrawablePath drawablePath) { + return mCompositePathView.addPath(drawablePath); + } + + /** + * Register a Path and Paint that will be drawn on a layer above the tiles, but below markers. + * The Path will be scaled with the TileView, but will always be as wide as the stroke set + * for the Paint. + * + * @param positions List of doubles that represent the points of the Path. + * @param paint The Paint instance that defines the style of the drawn path. + * @return The DrawablePath instance passed to the TileView. + */ + public CompositePathView.DrawablePath drawPath(List positions, Paint paint) { + Path path = mCoordinateTranslater.pathFromPositions(positions, false); + return mCompositePathView.addPath(path, paint); + } + + /** + * Removes a DrawablePath from the TileView's registry. This path will no longer be drawn by the + * TileView. + * + * @param drawablePath The DrawablePath instance to be removed. + */ + public void removePath(CompositePathView.DrawablePath drawablePath) { + mCompositePathView.removePath(drawablePath); + } + + /** + * Returns the Paint instance used by the CompositePathView by default. This can be modified for + * future Path paint operations. + * + * @return The Paint instance used by default. + */ + public Paint getDefaultPathPaint() { + return mCompositePathView.getDefaultPaint(); + } + + /** + * Recycles bitmap image files, prevents path drawing, and clears pending Handler messages, + * appropriate for Activity.onPause. + */ + public void pause() { + mRenderThrottleHandler.clear(); + mDetailLevelManager.invalidateAll(); + setWillNotDraw(true); + } + + /** + * Clear tile image files and remove all views, appropriate for Activity.onDestroy. + * After invoking this method, the TileView instance should be removed from any view trees, + * and references to it should be set to null. + */ + public void destroy() { + pause(); + mTileCanvasViewGroup.destroy(); + mCompositePathView.clear(); + removeAllViews(); + } + + /** + * Restore visible state (generally after a call to pause). + * Appropriate for Activity.onResume. + */ + public void resume() { + setWillNotDraw(false); + updateViewport(); + mTileCanvasViewGroup.updateTileSet(mDetailLevelManager.getCurrentDetailLevel()); + requestRender(); + requestLayout(); + } + + /** + * Allows the TileView to render tiles while panning. + * + * @param shouldRender True if it should render while panning. + */ + public void setShouldRenderWhilePanning(boolean shouldRender) { + mShouldRenderWhilePanning = shouldRender; + int buffer = shouldRender ? TileCanvasViewGroup.FAST_RENDER_BUFFER : TileCanvasViewGroup.DEFAULT_RENDER_BUFFER; + mTileCanvasViewGroup.setRenderBuffer(buffer); + } + + /** + * By default, when a zoom begins, the current {@link DetailLevel} is locked so it is used to + * provide tiles until the zoom ends. This ensures that the {@link TileView} is updated + * consistently. + *

+ * However, a zoom out may require a lot of tiles of the locked {@code DetailLevel} to be rendered. + * In worst case, it can cause {@link OutOfMemoryError}. + * Then, disabling the {@code DetailLevel} lock is a bandage to that issue. Using + * {@code setShouldUpdateDetailLevelWhileZooming( true )} is not advised unless you have that issue. + *

+ * + * @param shouldUpdate True if it should lock {@link DetailLevel} when a zoom begins. + */ + public void setShouldUpdateDetailLevelWhileZooming(boolean shouldUpdate) { + mShouldUpdateDetailLevelWhileZooming = shouldUpdate; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + mCalloutLayout.removeAllViews(); + return super.onTouchEvent(event); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + updateViewport(); + requestRender(); + } + + protected void updateViewport() { + int left = getScrollX(); + int top = getScrollY(); + int right = left + getWidth(); + int bottom = top + getHeight(); + mDetailLevelManager.updateViewport(left, top, right, bottom); + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + updateViewport(); + if (mShouldRenderWhilePanning) { + requestRender(); + } else { + requestThrottledRender(); + } } @Override - public void writeToParcel( Parcel out, int flags ) { - super.writeToParcel( out, flags ); - out.writeFloat( mScale ); - out.writeInt( mSavedCenterX ); - out.writeInt( mSavedCenterY ); - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - public SavedState createFromParcel( Parcel in ) { - return new SavedState( in ); - } - - public SavedState[] newArray( int size ) { - return new SavedState[ size ]; - } - }; - } - - /** - * The default {@code super.onSaveInstanceState} and {@code onRestoreInstanceState} don't - * restore the position on the map as expected (if the instance of {@link TileView} remains the - * same). For this reason and if a new {@link TileView} instance is created, we have to save - * the current scale and position on the map, to restore them later when the {@link TileView} is - * recreated. - */ - @Override - public Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - SavedState ss = new SavedState( superState ); - ss.mScale = getScale(); - ss.mSavedCenterX = getScrollX() + getHalfWidth(); - ss.mSavedCenterY = getScrollY() + getHalfHeight(); - return ss; - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - final SavedState ss = (SavedState) state; - super.onRestoreInstanceState( ss.getSuperState() ); - setScale( ss.mScale ); - post(new Runnable() { - @Override - public void run() { - scrollToAndCenter(ss.mSavedCenterX, ss.mSavedCenterY); - } - }); - } + public void onScaleChanged(float scale, float previous) { + super.onScaleChanged(scale, previous); + mDetailLevelManager.setScale(scale); + mHotSpotManager.setScale(scale); + mTileCanvasViewGroup.setScale(scale); + mScalingLayout.setScale(scale); + mCompositePathView.setScale(scale); + mMarkerLayout.setScale(scale); + mCalloutLayout.setScale(scale); + } + + @Override + public void onPanBegin(int x, int y, Origination origin) { + + } + + @Override + public void onPanUpdate(int x, int y, Origination origin) { + + } + + @Override + public void onPanEnd(int x, int y, Origination origin) { + requestRender(); + } + + @Override + public void onZoomBegin(float scale, Origination origin) { + if (origin == null) { + mTileCanvasViewGroup.suppressRender(); + } + mDetailLevelManager.setScale(scale); + } + + @Override + public void onZoomUpdate(float scale, Origination origin) { + + } + + @Override + public void onZoomEnd(float scale, Origination origin) { + if (origin == null) { + mTileCanvasViewGroup.resumeRender(); + } + mDetailLevelManager.setScale(scale); + requestRender(); + } + + @Override + public void onDetailLevelChanged(DetailLevel detailLevel) { + requestRender(); + mTileCanvasViewGroup.updateTileSet(detailLevel); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent event) { + int x = getScrollX() + (int) event.getX() - getOffsetX(); + int y = getScrollY() + (int) event.getY() - getOffsetY(); + if (mMarkerLayout.processHit(x, y)) return true; + if (mHotSpotManager.processHit(x, y)) return true; + return super.onSingleTapConfirmed(event); + } + + @Override + public void onRenderStart() { + + } + + @Override + public void onRenderCancelled() { + + } + + @Override + public void onRenderComplete() { + + } + + /** + * The default {@code super.onSaveInstanceState} and {@code onRestoreInstanceState} don't + * restore the position on the map as expected (if the instance of {@link TileView} remains the + * same). For this reason and if a new {@link TileView} instance is created, we have to save + * the current scale and position on the map, to restore them later when the {@link TileView} is + * recreated. + */ + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.mScale = getScale(); + ss.mSavedCenterX = getScrollX() + getHalfWidth(); + ss.mSavedCenterY = getScrollY() + getHalfHeight(); + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + final SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setScale(ss.mScale); + post(new Runnable() { + @Override + public void run() { + scrollToAndCenter(ss.mSavedCenterX, ss.mSavedCenterY); + } + }); + } + + private static class RenderThrottleHandler extends Handler { + + private static final int MESSAGE = 0; + private static final int RENDER_THROTTLE_TIMEOUT = 100; + + private final WeakReference mTileViewWeakReference; + + public RenderThrottleHandler(TileView tileView) { + super(); + mTileViewWeakReference = new WeakReference(tileView); + } + + @Override + public void handleMessage(Message msg) { + TileView tileView = mTileViewWeakReference.get(); + if (tileView != null) { + tileView.requestSafeRender(); + } + } + + public void clear() { + if (hasMessages(MESSAGE)) { + removeMessages(MESSAGE); + } + } + + public void submit() { + clear(); + sendEmptyMessageDelayed(MESSAGE, RENDER_THROTTLE_TIMEOUT); + } + } + + /** + * Object used to keep some data when a configuration change happens and the activity is + * re-created. + * It's boiler-plate but this is how to save View state. + */ + private static class SavedState extends BaseSavedState { + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + /* This will store the current scale and position */ + float mScale; + int mSavedCenterX; + int mSavedCenterY; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + mScale = in.readFloat(); + mSavedCenterX = in.readInt(); + mSavedCenterY = in.readInt(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeFloat(mScale); + out.writeInt(mSavedCenterX); + out.writeInt(mSavedCenterY); + } + } } \ No newline at end of file diff --git a/tileview/src/main/java/com/qozix/tileview/hotspots/HotSpotManager.java b/tileview/src/main/java/com/qozix/tileview/hotspots/HotSpotManager.java index 07994358..b275a98d 100644 --- a/tileview/src/main/java/com/qozix/tileview/hotspots/HotSpotManager.java +++ b/tileview/src/main/java/com/qozix/tileview/hotspots/HotSpotManager.java @@ -7,59 +7,63 @@ public class HotSpotManager { - private float mScale = 1; + private float mScale = 1; - private HotSpot.HotSpotTapListener mHotSpotTapListener; - private LinkedList mHotSpots = new LinkedList(); + private HotSpot.HotSpotTapListener mHotSpotTapListener; + private LinkedList mHotSpots = new LinkedList(); - public float getScale() { - return mScale; - } + public float getScale() { + return mScale; + } - public void setScale( float scale ) { - mScale = scale; - } + public void setScale(float scale) { + mScale = scale; + } - public void addHotSpot( HotSpot hotSpot ) { - mHotSpots.add( hotSpot ); - } + public void addHotSpot(HotSpot hotSpot) { + mHotSpots.add(hotSpot); + } - public void removeHotSpot( HotSpot hotSpot ) { - mHotSpots.remove( hotSpot ); - } + public void removeHotSpot(HotSpot hotSpot) { + mHotSpots.remove(hotSpot); + } - public void setHotSpotTapListener( HotSpot.HotSpotTapListener hotSpotTapListener ) { - mHotSpotTapListener = hotSpotTapListener; - } + public void setHotSpotTapListener(HotSpot.HotSpotTapListener hotSpotTapListener) { + mHotSpotTapListener = hotSpotTapListener; + } - public void clear() { - mHotSpots.clear(); - } + public void clear() { + mHotSpots.clear(); + } - private HotSpot getMatch( int x, int y ) { - int scaledX = FloatMathHelper.unscale( x, mScale ); - int scaledY = FloatMathHelper.unscale( y, mScale ); - Iterator iterator = mHotSpots.descendingIterator(); - while( iterator.hasNext() ) { - HotSpot hotSpot = iterator.next(); - if( hotSpot.contains( scaledX, scaledY ) ) { - return hotSpot; - } + private HotSpot getMatch(int x, int y) { + int scaledX = FloatMathHelper.unscale(x, mScale); + int scaledY = FloatMathHelper.unscale(y, mScale); + Iterator iterator = mHotSpots.descendingIterator(); + while (iterator.hasNext()) { + HotSpot hotSpot = iterator.next(); + if (hotSpot.contains(scaledX, scaledY)) { + return hotSpot; + } + } + return null; } - return null; - } - public void processHit( int x, int y ) { - HotSpot hotSpot = getMatch( x, y ); - if( hotSpot != null ) { - HotSpot.HotSpotTapListener spotListener = hotSpot.getHotSpotTapListener(); - if( spotListener != null ) { - spotListener.onHotSpotTap( hotSpot, x, y ); - } - if( mHotSpotTapListener != null ) { - mHotSpotTapListener.onHotSpotTap( hotSpot, x, y ); - } + public boolean processHit(int x, int y) { + boolean ret = false; + HotSpot hotSpot = getMatch(x, y); + if (hotSpot != null) { + HotSpot.HotSpotTapListener spotListener = hotSpot.getHotSpotTapListener(); + if (spotListener != null) { + spotListener.onHotSpotTap(hotSpot, x, y); + ret = true; + } + if (mHotSpotTapListener != null) { + mHotSpotTapListener.onHotSpotTap(hotSpot, x, y); + ret = true; + } + } + return ret; } - } } diff --git a/tileview/src/main/java/com/qozix/tileview/markers/MarkerLayout.java b/tileview/src/main/java/com/qozix/tileview/markers/MarkerLayout.java index 86cdfff2..3b9ac4ae 100644 --- a/tileview/src/main/java/com/qozix/tileview/markers/MarkerLayout.java +++ b/tileview/src/main/java/com/qozix/tileview/markers/MarkerLayout.java @@ -9,261 +9,263 @@ public class MarkerLayout extends ViewGroup { - private float mScale = 1; - - private float mAnchorX; - private float mAnchorY; - - private MarkerTapListener mMarkerTapListener; - - public MarkerLayout( Context context ) { - super( context ); - setClipChildren( false ); - } - - /** - * Sets the anchor values used by this ViewGroup if it's children do not - * have anchor values supplied directly (via individual LayoutParams). - * - * @param aX x-axis anchor value (offset computed by multiplying this value by the child's width). - * @param aY y-axis anchor value (offset computed by multiplying this value by the child's height). - */ - public void setAnchors( float aX, float aY ) { - mAnchorX = aX; - mAnchorY = aY; - requestLayout(); - } - - /** - * Sets the scale (0-1) of the MarkerLayout. - * - * @param scale The new value of the MarkerLayout scale. - */ - public void setScale( float scale ) { - mScale = scale; - requestLayout(); - } - - /** - * Retrieves the current scale of the MarkerLayout. - * - * @return The current scale of the MarkerLayout. - */ - public float getScale() { - return mScale; - } - - public View addMarker( View view, int x, int y, Float aX, Float aY ) { - LayoutParams layoutParams = new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, x, y, aX, aY ); - return addMarker( view, layoutParams ); - } - - public View addMarker( View view, LayoutParams params ) { - addView( view, params ); - return view; - } - - public void moveMarker( View view, int x, int y ) { - LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); - layoutParams.x = x; - layoutParams.y = y; - moveMarker( view, layoutParams ); - } - - public void moveMarker( View view, LayoutParams params ) { - if( indexOfChild( view ) > -1 ) { - view.setLayoutParams( params ); - requestLayout(); - } - } - - public void removeMarker( View view ) { - removeView( view ); - } - - public void setMarkerTapListener( MarkerTapListener markerTapListener ) { - mMarkerTapListener = markerTapListener; - } - - private View getViewFromTap( int x, int y ) { - for( int i = getChildCount() - 1; i >= 0; i-- ) { - View child = getChildAt( i ); - LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); - Rect hitRect = layoutParams.getHitRect(); - if( hitRect.contains( x, y ) ) { - return child; - } - } - return null; - } - - public void processHit( int x, int y ) { - if( mMarkerTapListener != null ) { - View view = getViewFromTap( x, y ); - if( view != null ) { - mMarkerTapListener.onMarkerTap( view, x, y ); - } - } - } - - @Override - protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec ) { - measureChildren( widthMeasureSpec, heightMeasureSpec ); - for( int i = 0; i < getChildCount(); i++ ) { - View child = getChildAt( i ); - if( child.getVisibility() != GONE ) { - MarkerLayout.LayoutParams layoutParams = (MarkerLayout.LayoutParams) child.getLayoutParams(); - // get anchor offsets - float widthMultiplier = (layoutParams.anchorX == null) ? mAnchorX : layoutParams.anchorX; - float heightMultiplier = (layoutParams.anchorY == null) ? mAnchorY : layoutParams.anchorY; - // actual sizes of children - int actualWidth = child.getMeasuredWidth(); - int actualHeight = child.getMeasuredHeight(); - // offset dimensions by anchor values - float widthOffset = actualWidth * widthMultiplier; - float heightOffset = actualHeight * heightMultiplier; - // get offset position - int scaledX = FloatMathHelper.scale( layoutParams.x, mScale ); - int scaledY = FloatMathHelper.scale( layoutParams.y, mScale ); - // save computed values - layoutParams.mLeft = (int) (scaledX + widthOffset); - layoutParams.mTop = (int) (scaledY + heightOffset); - layoutParams.mRight = layoutParams.mLeft + actualWidth; - layoutParams.mBottom = layoutParams.mTop + actualHeight; - } - } - int availableWidth = MeasureSpec.getSize( widthMeasureSpec ); - int availableHeight = MeasureSpec.getSize( heightMeasureSpec ); - setMeasuredDimension( availableWidth, availableHeight ); - } - - @Override - protected void onLayout( boolean changed, int l, int t, int r, int b ) { - for( int i = 0; i < getChildCount(); i++ ) { - View child = getChildAt( i ); - if( child.getVisibility() != GONE ) { - LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); - child.layout( layoutParams.mLeft, layoutParams.mTop, layoutParams.mRight, layoutParams.mBottom ); - } - } - } - - @Override - protected ViewGroup.LayoutParams generateDefaultLayoutParams() { - return new LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0 ); - } + private float mScale = 1; - @Override - protected boolean checkLayoutParams( ViewGroup.LayoutParams layoutParams ) { - return layoutParams instanceof LayoutParams; - } + private float mAnchorX; + private float mAnchorY; - @Override - protected ViewGroup.LayoutParams generateLayoutParams( ViewGroup.LayoutParams layoutParams ) { - return new LayoutParams( layoutParams ); - } + private MarkerTapListener mMarkerTapListener; - /** - * Per-child layout information associated with AnchorLayout. - */ - public static class LayoutParams extends ViewGroup.LayoutParams { + public MarkerLayout(Context context) { + super(context); + setClipChildren(false); + } /** - * The absolute left position of the child in pixels. + * Sets the anchor values used by this ViewGroup if it's children do not + * have anchor values supplied directly (via individual LayoutParams). + * + * @param aX x-axis anchor value (offset computed by multiplying this value by the child's width). + * @param aY y-axis anchor value (offset computed by multiplying this value by the child's height). */ - public int x = 0; + public void setAnchors(float aX, float aY) { + mAnchorX = aX; + mAnchorY = aY; + requestLayout(); + } /** - * The absolute right position of the child in pixels. + * Retrieves the current scale of the MarkerLayout. + * + * @return The current scale of the MarkerLayout. */ - public int y = 0; + public float getScale() { + return mScale; + } /** - * Float value to determine the child's horizontal offset. - * This float is multiplied by the child's width. - * If null, the containing AnchorLayout's anchor values will be used. + * Sets the scale (0-1) of the MarkerLayout. + * + * @param scale The new value of the MarkerLayout scale. */ - public Float anchorX = null; + public void setScale(float scale) { + mScale = scale; + requestLayout(); + } - /** - * Float value to determine the child's vertical offset. - * This float is multiplied by the child's height. - * If null, the containing AnchorLayout's anchor values will be used. - */ - public Float anchorY = null; - - private int mTop; - private int mLeft; - private int mBottom; - private int mRight; - - private Rect mHitRect; - - private Rect getHitRect() { - if( mHitRect == null ) { - mHitRect = new Rect(); - } - mHitRect.left = mLeft; - mHitRect.top = mTop; - mHitRect.right = mRight; - mHitRect.bottom = mBottom; - return mHitRect; + public View addMarker(View view, int x, int y, Float aX, Float aY) { + LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, x, y, aX, aY); + return addMarker(view, layoutParams); } - /** - * Copy constructor. - * - * @param source LayoutParams instance to copy properties from. - */ - public LayoutParams( ViewGroup.LayoutParams source ) { - super( source ); + public View addMarker(View view, LayoutParams params) { + addView(view, params); + return view; } - /** - * Creates a new set of layout parameters with the specified values. - * - * @param width Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. - * @param height Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. - */ - public LayoutParams( int width, int height ) { - super( width, height ); + public void moveMarker(View view, int x, int y) { + LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); + layoutParams.x = x; + layoutParams.y = y; + moveMarker(view, layoutParams); } - /** - * Creates a new set of layout parameters with the specified values. - * - * @param width Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. - * @param height Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. - * @param left Sets the absolute x value of the view's position in pixels. - * @param top Sets the absolute y value of the view's position in pixels. - */ - public LayoutParams( int width, int height, int left, int top ) { - super( width, height ); - x = left; - y = top; + public void moveMarker(View view, LayoutParams params) { + if (indexOfChild(view) > -1) { + view.setLayoutParams(params); + requestLayout(); + } } - /** - * Creates a new set of layout parameters with the specified values. - * - * @param width Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. - * @param height Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. - * @param left Sets the absolute x value of the view's position in pixels. - * @param top Sets the absolute y value of the view's position in pixels. - * @param anchorLeft Sets the relative horizontal offset of the view (multiplied by the view's width). - * @param anchorTop Sets the relative vertical offset of the view (multiplied by the view's height). - */ - public LayoutParams( int width, int height, int left, int top, Float anchorLeft, Float anchorTop ) { - super( width, height ); - x = left; - y = top; - anchorX = anchorLeft; - anchorY = anchorTop; + public void removeMarker(View view) { + removeView(view); + } + + public void setMarkerTapListener(MarkerTapListener markerTapListener) { + mMarkerTapListener = markerTapListener; + } + + private View getViewFromTap(int x, int y) { + for (int i = getChildCount() - 1; i >= 0; i--) { + View child = getChildAt(i); + LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); + Rect hitRect = layoutParams.getHitRect(); + if (hitRect.contains(x, y)) { + return child; + } + } + return null; + } + + public boolean processHit(int x, int y) { + if (mMarkerTapListener != null) { + View view = getViewFromTap(x, y); + if (view != null) { + mMarkerTapListener.onMarkerTap(view, x, y); + return true; + } + } + return false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + measureChildren(widthMeasureSpec, heightMeasureSpec); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + MarkerLayout.LayoutParams layoutParams = (MarkerLayout.LayoutParams) child.getLayoutParams(); + // get anchor offsets + float widthMultiplier = (layoutParams.anchorX == null) ? mAnchorX : layoutParams.anchorX; + float heightMultiplier = (layoutParams.anchorY == null) ? mAnchorY : layoutParams.anchorY; + // actual sizes of children + int actualWidth = child.getMeasuredWidth(); + int actualHeight = child.getMeasuredHeight(); + // offset dimensions by anchor values + float widthOffset = actualWidth * widthMultiplier; + float heightOffset = actualHeight * heightMultiplier; + // get offset position + int scaledX = FloatMathHelper.scale(layoutParams.x, mScale); + int scaledY = FloatMathHelper.scale(layoutParams.y, mScale); + // save computed values + layoutParams.mLeft = (int) (scaledX + widthOffset); + layoutParams.mTop = (int) (scaledY + heightOffset); + layoutParams.mRight = layoutParams.mLeft + actualWidth; + layoutParams.mBottom = layoutParams.mTop + actualHeight; + } + } + int availableWidth = MeasureSpec.getSize(widthMeasureSpec); + int availableHeight = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(availableWidth, availableHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + LayoutParams layoutParams = (LayoutParams) child.getLayoutParams(); + child.layout(layoutParams.mLeft, layoutParams.mTop, layoutParams.mRight, layoutParams.mBottom); + } + } + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams layoutParams) { + return layoutParams instanceof LayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams layoutParams) { + return new LayoutParams(layoutParams); + } + + public interface MarkerTapListener { + void onMarkerTap(View view, int x, int y); } - } + /** + * Per-child layout information associated with AnchorLayout. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + + /** + * The absolute left position of the child in pixels. + */ + public int x = 0; + + /** + * The absolute right position of the child in pixels. + */ + public int y = 0; + + /** + * Float value to determine the child's horizontal offset. + * This float is multiplied by the child's width. + * If null, the containing AnchorLayout's anchor values will be used. + */ + public Float anchorX = null; + + /** + * Float value to determine the child's vertical offset. + * This float is multiplied by the child's height. + * If null, the containing AnchorLayout's anchor values will be used. + */ + public Float anchorY = null; + + private int mTop; + private int mLeft; + private int mBottom; + private int mRight; + + private Rect mHitRect; + + /** + * Copy constructor. + * + * @param source LayoutParams instance to copy properties from. + */ + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + /** + * Creates a new set of layout parameters with the specified values. + * + * @param width Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. + * @param height Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. + */ + public LayoutParams(int width, int height) { + super(width, height); + } + + /** + * Creates a new set of layout parameters with the specified values. + * + * @param width Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. + * @param height Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. + * @param left Sets the absolute x value of the view's position in pixels. + * @param top Sets the absolute y value of the view's position in pixels. + */ + public LayoutParams(int width, int height, int left, int top) { + super(width, height); + x = left; + y = top; + } + + /** + * Creates a new set of layout parameters with the specified values. + * + * @param width Information about how wide the view wants to be. This should generally be WRAP_CONTENT or a fixed value. + * @param height Information about how tall the view wants to be. This should generally be WRAP_CONTENT or a fixed value. + * @param left Sets the absolute x value of the view's position in pixels. + * @param top Sets the absolute y value of the view's position in pixels. + * @param anchorLeft Sets the relative horizontal offset of the view (multiplied by the view's width). + * @param anchorTop Sets the relative vertical offset of the view (multiplied by the view's height). + */ + public LayoutParams(int width, int height, int left, int top, Float anchorLeft, Float anchorTop) { + super(width, height); + x = left; + y = top; + anchorX = anchorLeft; + anchorY = anchorTop; + } + + private Rect getHitRect() { + if (mHitRect == null) { + mHitRect = new Rect(); + } + mHitRect.left = mLeft; + mHitRect.top = mTop; + mHitRect.right = mRight; + mHitRect.bottom = mBottom; + return mHitRect; + } - public interface MarkerTapListener { - void onMarkerTap( View view, int x, int y ); - } + } } diff --git a/tileview/src/main/java/com/qozix/tileview/widgets/ZoomPanLayout.java b/tileview/src/main/java/com/qozix/tileview/widgets/ZoomPanLayout.java index 7dfb2413..92b1adad 100644 --- a/tileview/src/main/java/com/qozix/tileview/widgets/ZoomPanLayout.java +++ b/tileview/src/main/java/com/qozix/tileview/widgets/ZoomPanLayout.java @@ -3,7 +3,6 @@ import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; -import androidx.core.view.ViewCompat; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; @@ -19,959 +18,969 @@ import java.lang.ref.WeakReference; import java.util.HashSet; +import androidx.core.view.ViewCompat; + /** * ZoomPanLayout extends ViewGroup to provide support for scrolling and zooming. * Fling, drag, pinch and double-tap events are supported natively. - * + *

* Children of ZoomPanLayout are laid out to the sizes provided by setSize, * and will always be positioned at 0,0. */ public class ZoomPanLayout extends ViewGroup implements - GestureDetector.OnGestureListener, - GestureDetector.OnDoubleTapListener, - ScaleGestureDetector.OnScaleGestureListener, - TouchUpGestureDetector.OnTouchUpListener { - - private static final int DEFAULT_ZOOM_PAN_ANIMATION_DURATION = 400; - - private int mBaseWidth; - private int mBaseHeight; - private int mScaledWidth; - private int mScaledHeight; - - private float mScale = 1; - - private float mMinScale = 0; - private float mMaxScale = 1; - - private int mOffsetX; - private int mOffsetY; - - private float mEffectiveMinScale = 0; - private boolean mShouldLoopScale = true; - - private boolean mIsFlinging; - private boolean mIsDragging; - private boolean mIsScaling; - private boolean mIsSliding; - - private int mAnimationDuration = DEFAULT_ZOOM_PAN_ANIMATION_DURATION; - - private HashSet mZoomPanListeners = new HashSet(); - - private Scroller mScroller; - private ZoomPanAnimator mZoomPanAnimator; - - private ScaleGestureDetector mScaleGestureDetector; - private GestureDetector mGestureDetector; - private TouchUpGestureDetector mTouchUpGestureDetector; - private MinimumScaleMode mMinimumScaleMode = MinimumScaleMode.FILL; - - /** - * Constructor to use when creating a ZoomPanLayout from code. - * - * @param context The Context the ZoomPanLayout is running in, through which it can access the current theme, resources, etc. - */ - public ZoomPanLayout( Context context ) { - this( context, null ); - } - - public ZoomPanLayout( Context context, AttributeSet attrs ) { - this( context, attrs, 0 ); - } - - public ZoomPanLayout( Context context, AttributeSet attrs, int defStyleAttr ) { - super( context, attrs, defStyleAttr ); - setWillNotDraw( false ); - setClipChildren( false ); - mScroller = new Scroller( context ); - mGestureDetector = new GestureDetector( context, this ); - mScaleGestureDetector = new ScaleGestureDetector( context, this ); - mTouchUpGestureDetector = new TouchUpGestureDetector( this ); - } - - @Override - protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec ) { - // the container's children should be the size provided by setSize - // don't use measureChildren because that grabs the child's LayoutParams - int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( mScaledWidth, MeasureSpec.EXACTLY ); - int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( mScaledHeight, MeasureSpec.EXACTLY ); - for( int i = 0; i < getChildCount(); i++){ - View child = getChildAt( i ); - child.measure( childWidthMeasureSpec, childHeightMeasureSpec ); - } - // but the layout itself should report normal (on screen) dimensions - int width = MeasureSpec.getSize( widthMeasureSpec ); - int height = MeasureSpec.getSize( heightMeasureSpec ); - width = resolveSize( width, widthMeasureSpec ); - height = resolveSize( height, heightMeasureSpec ); - setMeasuredDimension( width, height ); - } - - /* - ZoomPanChildren will always be laid out with the scaled dimenions - what is visible during - scroll operations. Thus, a RelativeLayout added as a child that had views within it using - rules like ALIGN_PARENT_RIGHT would function as expected; similarly, an ImageView would be - stretched between the visible edges. - If children further operate on scale values, that should be accounted for - in the child's logic (see ScalingLayout). - */ - @Override - protected void onLayout( boolean changed, int l, int t, int r, int b ) { - final int width = getWidth(); - final int height = getHeight(); - - mOffsetX = mScaledWidth >= width ? 0 : width / 2 - mScaledWidth / 2; - mOffsetY = mScaledHeight >= height ? 0 : height / 2 - mScaledHeight / 2; - - for( int i = 0; i < getChildCount(); i++ ) { - View child = getChildAt( i ); - if( child.getVisibility() != GONE ) { - child.layout( mOffsetX, mOffsetY, mScaledWidth + mOffsetX, mScaledHeight + mOffsetY ); - } - } - calculateMinimumScaleToFit(); - constrainScrollToLimits(); - } - - /** - * Determines whether the ZoomPanLayout should limit it's minimum scale to no less than what - * would be required to fill it's container. - * - * @param shouldScaleToFit True to limit minimum scale, false to allow arbitrary minimum scale. - */ - public void setShouldScaleToFit( boolean shouldScaleToFit ) { - setMinimumScaleMode(shouldScaleToFit ? MinimumScaleMode.FILL : MinimumScaleMode.NONE); - } - - /** - * Sets the minimum scale mode - * - * @param minimumScaleMode The minimum scale mode - */ - public void setMinimumScaleMode( MinimumScaleMode minimumScaleMode ) { - mMinimumScaleMode = minimumScaleMode; - calculateMinimumScaleToFit(); - } - - /** - * Determines whether the ZoomPanLayout should go back to minimum scale after a double-tap at - * maximum scale. - * - * @param shouldLoopScale True to allow going back to minimum scale, false otherwise. - */ - public void setShouldLoopScale( boolean shouldLoopScale ) { - mShouldLoopScale = shouldLoopScale; - } - - /** - * Set minimum and maximum mScale values for this ZoomPanLayout. - * Note that if minimumScaleMode is set to {@link MinimumScaleMode#FIT} or {@link MinimumScaleMode#FILL}, the minimum value set here will be ignored - * Default values are 0 and 1. - * - * @param min Minimum scale the ZoomPanLayout should accept. - * @param max Maximum scale the ZoomPanLayout should accept. - */ - public void setScaleLimits( float min, float max ) { - mMinScale = min; - mMaxScale = max; - setScale( mScale ); - } - - /** - * Sets the size (width and height) of the ZoomPanLayout - * as it should be rendered at a scale of 1f (100%). - * - * @param width Width of the underlying image, not the view or viewport. - * @param height Height of the underlying image, not the view or viewport. - */ - public void setSize( int width, int height ) { - mBaseWidth = width; - mBaseHeight = height; - updateScaledDimensions(); - calculateMinimumScaleToFit(); - constrainScrollToLimits(); - requestLayout(); - } - - /** - * Returns the base (not scaled) width of the underlying composite image. - * - * @return The base (not scaled) width of the underlying composite image. - */ - public int getBaseWidth() { - return mBaseWidth; - } - - /** - * Returns the base (not scaled) height of the underlying composite image. - * - * @return The base (not scaled) height of the underlying composite image. - */ - public int getBaseHeight() { - return mBaseHeight; - } - - /** - * Returns the scaled width of the underlying composite image. - * - * @return The scaled width of the underlying composite image. - */ - public int getScaledWidth() { - return mScaledWidth; - } - - /** - * Returns the scaled height of the underlying composite image. - * - * @return The scaled height of the underlying composite image. - */ - public int getScaledHeight() { - return mScaledHeight; - } - - /** - * Sets the scale (0-1) of the ZoomPanLayout. - * - * @param scale The new value of the ZoomPanLayout scale. - */ - public void setScale( float scale ) { - scale = getConstrainedDestinationScale( scale ); - if( mScale != scale ) { - float previous = mScale; - mScale = scale; - updateScaledDimensions(); - constrainScrollToLimits(); - onScaleChanged( scale, previous ); - invalidate(); - } - } - - /** - * Retrieves the current scale of the ZoomPanLayout. - * - * @return The current scale of the ZoomPanLayout. - */ - public float getScale() { - return mScale; - } - - /** - * Returns the horizontal distance children are offset if the content is scaled smaller than width. - * - * @return - */ - public int getOffsetX() { - return mOffsetX; - } - - /** - * Return the vertical distance children are offset if the content is scaled smaller than height. - * - * @return - */ - public int getOffsetY() { - return mOffsetY; - } - - /** - * Returns whether the ZoomPanLayout is currently being flung. - * - * @return true if the ZoomPanLayout is currently flinging, false otherwise. - */ - public boolean isFlinging() { - return mIsFlinging; - } - - /** - * Returns whether the ZoomPanLayout is currently being dragged. - * - * @return true if the ZoomPanLayout is currently dragging, false otherwise. - */ - public boolean isDragging() { - return mIsDragging; - } - - /** - * Returns whether the ZoomPanLayout is currently operating a scroll tween. - * - * @return True if the ZoomPanLayout is currently scrolling, false otherwise. - */ - public boolean isSliding() { - return mIsSliding; - } - - /** - * Returns whether the ZoomPanLayout is currently operating a scale tween. - * - * @return True if the ZoomPanLayout is currently scaling, false otherwise. - */ - public boolean isScaling() { - return mIsScaling; - } - - /** - * Returns the Scroller instance used to manage dragging and flinging. - * - * @return The Scroller instance use to manage dragging and flinging. - */ - public Scroller getScroller() { - return mScroller; - } - - /** - * Returns the duration zoom and pan animations will use. - * - * @return The duration zoom and pan animations will use. - */ - public int getAnimationDuration() { - return mAnimationDuration; - } - - /** - * Set the duration zoom and pan animation will use. - * - * @param animationDuration The duration animations will use. - */ - public void setAnimationDuration( int animationDuration ) { - mAnimationDuration = animationDuration; - if( mZoomPanAnimator != null ) { - mZoomPanAnimator.setDuration( mAnimationDuration ); - } - } - - /** - * Adds a ZoomPanListener to the ZoomPanLayout, which will receive notification of actions - * relating to zoom and pan events. - * - * @param zoomPanListener ZoomPanListener implementation to add. - * @return True when the listener set did not already contain the Listener, false otherwise. - */ - public boolean addZoomPanListener( ZoomPanListener zoomPanListener ) { - return mZoomPanListeners.add( zoomPanListener ); - } - - /** - * Removes a ZoomPanListener from the ZoomPanLayout - * - * @param listener ZoomPanListener to remove. - * @return True if the Listener was removed, false otherwise. - */ - public boolean removeZoomPanListener( ZoomPanListener listener ) { - return mZoomPanListeners.remove( listener ); - } - - /** - * Scrolls and centers the ZoomPanLayout to the x and y values provided. - * - * @param x Horizontal destination point. - * @param y Vertical destination point. - */ - public void scrollToAndCenter( int x, int y ) { - scrollTo( x - getHalfWidth(), y - getHalfHeight() ); - } - - /** - * Set the scale of the ZoomPanLayout while maintaining the current center point. - * - * @param scale The new value of the ZoomPanLayout scale. - */ - public void setScaleFromCenter( float scale ) { - setScaleFromPosition( getHalfWidth(), getHalfHeight(), scale ); - } - - /** - * Scrolls the ZoomPanLayout to the x and y values provided using scrolling animation. - * - * @param x Horizontal destination point. - * @param y Vertical destination point. - */ - public void slideTo( int x, int y ) { - getAnimator().animatePan( x, y ); - } - - /** - * Scrolls and centers the ZoomPanLayout to the x and y values provided using scrolling animation. - * - * @param x Horizontal destination point. - * @param y Vertical destination point. - */ - public void slideToAndCenter( int x, int y ) { - slideTo( x - getHalfWidth(), y - getHalfHeight() ); - } - - /** - * Animates the ZoomPanLayout to the scale provided, and centers the viewport to the position - * supplied. - * - * @param x Horizontal destination point. - * @param y Vertical destination point. - * @param scale The final scale value the ZoomPanLayout should animate to. - */ - public void slideToAndCenterWithScale( int x, int y, float scale ) { - getAnimator().animateZoomPan( x - getHalfWidth(), y - getHalfHeight(), scale ); - } - - /** - * Scales the ZoomPanLayout with animated progress, without maintaining scroll position. - * - * @param destination The final scale value the ZoomPanLayout should animate to. - */ - public void smoothScaleTo( float destination ) { - getAnimator().animateZoom( destination ); - } - - /** - * Animates the ZoomPanLayout to the scale provided, while maintaining position determined by - * the focal point provided. - * - * @param focusX The horizontal focal point to maintain, relative to the screen (as supplied by MotionEvent.getX). - * @param focusY The vertical focal point to maintain, relative to the screen (as supplied by MotionEvent.getY). - * @param scale The final scale value the ZoomPanLayout should animate to. - */ - public void smoothScaleFromFocalPoint( int focusX, int focusY, float scale ) { - scale = getConstrainedDestinationScale( scale ); - if( scale == mScale ) { - return; - } - int x = getOffsetScrollXFromScale( focusX, scale, mScale ); - int y = getOffsetScrollYFromScale( focusY, scale, mScale ); - getAnimator().animateZoomPan( x, y, scale ); - } - - /** - * Animate the scale of the ZoomPanLayout while maintaining the current center point. - * - * @param scale The final scale value the ZoomPanLayout should animate to. - */ - public void smoothScaleFromCenter( float scale ) { - smoothScaleFromFocalPoint( getHalfWidth(), getHalfHeight(), scale ); - } - - /** - * Provide this method to be overriden by subclasses, e.g., onScrollChanged. - */ - public void onScaleChanged( float currentScale, float previousScale ) { - // noop - } - - private float getConstrainedDestinationScale( float scale ) { - scale = Math.max( scale, mEffectiveMinScale ); - scale = Math.min( scale, mMaxScale ); - return scale; - } - - private void constrainScrollToLimits() { - int x = getScrollX(); - int y = getScrollY(); - int constrainedX = getConstrainedScrollX( x ); - int constrainedY = getConstrainedScrollY( y ); - if( x != constrainedX || y != constrainedY ) { - scrollTo( constrainedX, constrainedY ); - } - } - - private void updateScaledDimensions() { - mScaledWidth = FloatMathHelper.scale( mBaseWidth, mScale ); - mScaledHeight = FloatMathHelper.scale( mBaseHeight, mScale ); - } - - protected ZoomPanAnimator getAnimator() { - if( mZoomPanAnimator == null ) { - mZoomPanAnimator = new ZoomPanAnimator( this ); - mZoomPanAnimator.setDuration( mAnimationDuration ); - } - return mZoomPanAnimator; - } - - private int getOffsetScrollXFromScale( int offsetX, float destinationScale, float currentScale ) { - int scrollX = getScrollX() + offsetX; - float deltaScale = destinationScale / currentScale; - return (int) (scrollX * deltaScale) - offsetX; - } - - private int getOffsetScrollYFromScale( int offsetY, float destinationScale, float currentScale ) { - int scrollY = getScrollY() + offsetY; - float deltaScale = destinationScale / currentScale; - return (int) (scrollY * deltaScale) - offsetY; - } - - public void setScaleFromPosition( int offsetX, int offsetY, float scale ) { - scale = getConstrainedDestinationScale( scale ); - if( scale == mScale ) { - return; - } - int x = getOffsetScrollXFromScale( offsetX, scale, mScale ); - int y = getOffsetScrollYFromScale( offsetY, scale, mScale ); - - setScale( scale ); - - x = getConstrainedScrollX( x ); - y = getConstrainedScrollY( y ); - - scrollTo( x, y ); - } - - @Override - public boolean canScrollHorizontally( int direction ) { - int position = getScrollX(); - return direction > 0 ? position < getScrollLimitX() : direction < 0 && position > 0; - } - - @Override - public boolean onTouchEvent( MotionEvent event ) { - boolean gestureIntercept = mGestureDetector.onTouchEvent( event ); - boolean scaleIntercept = mScaleGestureDetector.onTouchEvent( event ); - boolean touchIntercept = mTouchUpGestureDetector.onTouchEvent( event ); - return gestureIntercept || scaleIntercept || touchIntercept || super.onTouchEvent( event ); - } - - @Override - public void scrollTo( int x, int y ) { - x = getConstrainedScrollX( x ); - y = getConstrainedScrollY( y ); - super.scrollTo( x, y ); - } - - private void calculateMinimumScaleToFit() { - float minimumScaleX = getWidth() / (float) mBaseWidth; - float minimumScaleY = getHeight() / (float) mBaseHeight; - float recalculatedMinScale = calculatedMinScale(minimumScaleX, minimumScaleY); - if( recalculatedMinScale != mEffectiveMinScale ) { - mEffectiveMinScale = recalculatedMinScale; - if( mScale < mEffectiveMinScale ){ - setScale( mEffectiveMinScale ); - } - } - } - - private float calculatedMinScale( float minimumScaleX, float minimumScaleY ) { - switch( mMinimumScaleMode ) { - case FILL: return Math.max( minimumScaleX, minimumScaleY ); - case FIT: return Math.min( minimumScaleX, minimumScaleY ); - } - - return mMinScale; - } - - protected int getHalfWidth() { - return FloatMathHelper.scale( getWidth(), 0.5f ); - } - - protected int getHalfHeight() { - return FloatMathHelper.scale( getHeight(), 0.5f ); - } + GestureDetector.OnGestureListener, + GestureDetector.OnDoubleTapListener, + ScaleGestureDetector.OnScaleGestureListener, + TouchUpGestureDetector.OnTouchUpListener { + + private static final int DEFAULT_ZOOM_PAN_ANIMATION_DURATION = 400; + + private int mBaseWidth; + private int mBaseHeight; + private int mScaledWidth; + private int mScaledHeight; + + private float mScale = 1; + + private float mMinScale = 0; + private float mMaxScale = 1; + + private int mOffsetX; + private int mOffsetY; + + private float mEffectiveMinScale = 0; + private boolean mShouldLoopScale = true; + + private boolean mIsFlinging; + private boolean mIsDragging; + private boolean mIsScaling; + private boolean mIsSliding; + + private int mAnimationDuration = DEFAULT_ZOOM_PAN_ANIMATION_DURATION; + + private HashSet mZoomPanListeners = new HashSet(); + + private Scroller mScroller; + private ZoomPanAnimator mZoomPanAnimator; + + private ScaleGestureDetector mScaleGestureDetector; + private GestureDetector mGestureDetector; + private TouchUpGestureDetector mTouchUpGestureDetector; + private MinimumScaleMode mMinimumScaleMode = MinimumScaleMode.FILL; + + /** + * Constructor to use when creating a ZoomPanLayout from code. + * + * @param context The Context the ZoomPanLayout is running in, through which it can access the current theme, resources, etc. + */ + public ZoomPanLayout(Context context) { + this(context, null); + } + + public ZoomPanLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ZoomPanLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + setWillNotDraw(false); + setClipChildren(false); + mScroller = new Scroller(context); + mGestureDetector = new GestureDetector(context, this); + mScaleGestureDetector = new ScaleGestureDetector(context, this); + mTouchUpGestureDetector = new TouchUpGestureDetector(this); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // the container's children should be the size provided by setSize + // don't use measureChildren because that grabs the child's LayoutParams + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mScaledWidth, MeasureSpec.EXACTLY); + int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(mScaledHeight, MeasureSpec.EXACTLY); + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + // but the layout itself should report normal (on screen) dimensions + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + width = resolveSize(width, widthMeasureSpec); + height = resolveSize(height, heightMeasureSpec); + setMeasuredDimension(width, height); + } + + /* + ZoomPanChildren will always be laid out with the scaled dimenions - what is visible during + scroll operations. Thus, a RelativeLayout added as a child that had views within it using + rules like ALIGN_PARENT_RIGHT would function as expected; similarly, an ImageView would be + stretched between the visible edges. + If children further operate on scale values, that should be accounted for + in the child's logic (see ScalingLayout). + */ + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int width = getWidth(); + final int height = getHeight(); + + mOffsetX = mScaledWidth >= width ? 0 : width / 2 - mScaledWidth / 2; + mOffsetY = mScaledHeight >= height ? 0 : height / 2 - mScaledHeight / 2; + + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.layout(mOffsetX, mOffsetY, mScaledWidth + mOffsetX, mScaledHeight + mOffsetY); + } + } + calculateMinimumScaleToFit(); + constrainScrollToLimits(); + } + + /** + * Determines whether the ZoomPanLayout should limit it's minimum scale to no less than what + * would be required to fill it's container. + * + * @param shouldScaleToFit True to limit minimum scale, false to allow arbitrary minimum scale. + */ + public void setShouldScaleToFit(boolean shouldScaleToFit) { + setMinimumScaleMode(shouldScaleToFit ? MinimumScaleMode.FILL : MinimumScaleMode.NONE); + } + + /** + * Sets the minimum scale mode + * + * @param minimumScaleMode The minimum scale mode + */ + public void setMinimumScaleMode(MinimumScaleMode minimumScaleMode) { + mMinimumScaleMode = minimumScaleMode; + calculateMinimumScaleToFit(); + } + + /** + * Determines whether the ZoomPanLayout should go back to minimum scale after a double-tap at + * maximum scale. + * + * @param shouldLoopScale True to allow going back to minimum scale, false otherwise. + */ + public void setShouldLoopScale(boolean shouldLoopScale) { + mShouldLoopScale = shouldLoopScale; + } + + /** + * Set minimum and maximum mScale values for this ZoomPanLayout. + * Note that if minimumScaleMode is set to {@link MinimumScaleMode#FIT} or {@link MinimumScaleMode#FILL}, the minimum value set here will be ignored + * Default values are 0 and 1. + * + * @param min Minimum scale the ZoomPanLayout should accept. + * @param max Maximum scale the ZoomPanLayout should accept. + */ + public void setScaleLimits(float min, float max) { + mMinScale = min; + mMaxScale = max; + setScale(mScale); + } + + /** + * Sets the size (width and height) of the ZoomPanLayout + * as it should be rendered at a scale of 1f (100%). + * + * @param width Width of the underlying image, not the view or viewport. + * @param height Height of the underlying image, not the view or viewport. + */ + public void setSize(int width, int height) { + mBaseWidth = width; + mBaseHeight = height; + updateScaledDimensions(); + calculateMinimumScaleToFit(); + constrainScrollToLimits(); + requestLayout(); + } + + /** + * Returns the base (not scaled) width of the underlying composite image. + * + * @return The base (not scaled) width of the underlying composite image. + */ + public int getBaseWidth() { + return mBaseWidth; + } - private int getConstrainedScrollX( int x ) { - return Math.max( 0, Math.min( x, getScrollLimitX() ) ); - } + /** + * Returns the base (not scaled) height of the underlying composite image. + * + * @return The base (not scaled) height of the underlying composite image. + */ + public int getBaseHeight() { + return mBaseHeight; + } - private int getConstrainedScrollY( int y ) { - return Math.max( 0, Math.min( y, getScrollLimitY() ) ); - } + /** + * Returns the scaled width of the underlying composite image. + * + * @return The scaled width of the underlying composite image. + */ + public int getScaledWidth() { + return mScaledWidth; + } - private int getScrollLimitX() { - return mScaledWidth - getWidth(); - } + /** + * Returns the scaled height of the underlying composite image. + * + * @return The scaled height of the underlying composite image. + */ + public int getScaledHeight() { + return mScaledHeight; + } - private int getScrollLimitY() { - return mScaledHeight - getHeight(); - } + /** + * Retrieves the current scale of the ZoomPanLayout. + * + * @return The current scale of the ZoomPanLayout. + */ + public float getScale() { + return mScale; + } - @Override - public void computeScroll() { - if( mScroller.computeScrollOffset() ) { - int startX = getScrollX(); - int startY = getScrollY(); - int endX = getConstrainedScrollX( mScroller.getCurrX() ); - int endY = getConstrainedScrollY( mScroller.getCurrY() ); - if( startX != endX || startY != endY ) { - scrollTo( endX, endY ); - if( mIsFlinging ) { - broadcastFlingUpdate(); + /** + * Sets the scale (0-1) of the ZoomPanLayout. + * + * @param scale The new value of the ZoomPanLayout scale. + */ + public void setScale(float scale) { + scale = getConstrainedDestinationScale(scale); + if (mScale != scale) { + float previous = mScale; + mScale = scale; + updateScaledDimensions(); + constrainScrollToLimits(); + onScaleChanged(scale, previous); + invalidate(); } - } - if( mScroller.isFinished() ) { - if( mIsFlinging ) { - mIsFlinging = false; - broadcastFlingEnd(); + } + + /** + * Returns the horizontal distance children are offset if the content is scaled smaller than width. + * + * @return + */ + public int getOffsetX() { + return mOffsetX; + } + + /** + * Return the vertical distance children are offset if the content is scaled smaller than height. + * + * @return + */ + public int getOffsetY() { + return mOffsetY; + } + + /** + * Returns whether the ZoomPanLayout is currently being flung. + * + * @return true if the ZoomPanLayout is currently flinging, false otherwise. + */ + public boolean isFlinging() { + return mIsFlinging; + } + + /** + * Returns whether the ZoomPanLayout is currently being dragged. + * + * @return true if the ZoomPanLayout is currently dragging, false otherwise. + */ + public boolean isDragging() { + return mIsDragging; + } + + /** + * Returns whether the ZoomPanLayout is currently operating a scroll tween. + * + * @return True if the ZoomPanLayout is currently scrolling, false otherwise. + */ + public boolean isSliding() { + return mIsSliding; + } + + /** + * Returns whether the ZoomPanLayout is currently operating a scale tween. + * + * @return True if the ZoomPanLayout is currently scaling, false otherwise. + */ + public boolean isScaling() { + return mIsScaling; + } + + /** + * Returns the Scroller instance used to manage dragging and flinging. + * + * @return The Scroller instance use to manage dragging and flinging. + */ + public Scroller getScroller() { + return mScroller; + } + + /** + * Returns the duration zoom and pan animations will use. + * + * @return The duration zoom and pan animations will use. + */ + public int getAnimationDuration() { + return mAnimationDuration; + } + + /** + * Set the duration zoom and pan animation will use. + * + * @param animationDuration The duration animations will use. + */ + public void setAnimationDuration(int animationDuration) { + mAnimationDuration = animationDuration; + if (mZoomPanAnimator != null) { + mZoomPanAnimator.setDuration(mAnimationDuration); } - } else { - ViewCompat.postInvalidateOnAnimation( this ); - } } - } - private void broadcastDragBegin() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanBegin( getScrollX(), getScrollY(), ZoomPanListener.Origination.DRAG ); + /** + * Adds a ZoomPanListener to the ZoomPanLayout, which will receive notification of actions + * relating to zoom and pan events. + * + * @param zoomPanListener ZoomPanListener implementation to add. + * @return True when the listener set did not already contain the Listener, false otherwise. + */ + public boolean addZoomPanListener(ZoomPanListener zoomPanListener) { + return mZoomPanListeners.add(zoomPanListener); } - } - private void broadcastDragUpdate() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanUpdate( getScrollX(), getScrollY(), ZoomPanListener.Origination.DRAG ); + /** + * Removes a ZoomPanListener from the ZoomPanLayout + * + * @param listener ZoomPanListener to remove. + * @return True if the Listener was removed, false otherwise. + */ + public boolean removeZoomPanListener(ZoomPanListener listener) { + return mZoomPanListeners.remove(listener); } - } - private void broadcastDragEnd() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanEnd( getScrollX(), getScrollY(), ZoomPanListener.Origination.DRAG ); + /** + * Scrolls and centers the ZoomPanLayout to the x and y values provided. + * + * @param x Horizontal destination point. + * @param y Vertical destination point. + */ + public void scrollToAndCenter(int x, int y) { + scrollTo(x - getHalfWidth(), y - getHalfHeight()); } - } - private void broadcastFlingBegin() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanBegin( mScroller.getStartX(), mScroller.getStartY(), ZoomPanListener.Origination.FLING ); + /** + * Set the scale of the ZoomPanLayout while maintaining the current center point. + * + * @param scale The new value of the ZoomPanLayout scale. + */ + public void setScaleFromCenter(float scale) { + setScaleFromPosition(getHalfWidth(), getHalfHeight(), scale); } - } - private void broadcastFlingUpdate() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanUpdate( mScroller.getCurrX(), mScroller.getCurrY(), ZoomPanListener.Origination.FLING ); + /** + * Scrolls the ZoomPanLayout to the x and y values provided using scrolling animation. + * + * @param x Horizontal destination point. + * @param y Vertical destination point. + */ + public void slideTo(int x, int y) { + getAnimator().animatePan(x, y); + } + + /** + * Scrolls and centers the ZoomPanLayout to the x and y values provided using scrolling animation. + * + * @param x Horizontal destination point. + * @param y Vertical destination point. + */ + public void slideToAndCenter(int x, int y) { + slideTo(x - getHalfWidth(), y - getHalfHeight()); } - } - private void broadcastFlingEnd() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanEnd( mScroller.getFinalX(), mScroller.getFinalY(), ZoomPanListener.Origination.FLING ); + /** + * Animates the ZoomPanLayout to the scale provided, and centers the viewport to the position + * supplied. + * + * @param x Horizontal destination point. + * @param y Vertical destination point. + * @param scale The final scale value the ZoomPanLayout should animate to. + */ + public void slideToAndCenterWithScale(int x, int y, float scale) { + getAnimator().animateZoomPan(x - getHalfWidth(), y - getHalfHeight(), scale); } - } - private void broadcastProgrammaticPanBegin() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanBegin( getScrollX(), getScrollY(), null ); + /** + * Scales the ZoomPanLayout with animated progress, without maintaining scroll position. + * + * @param destination The final scale value the ZoomPanLayout should animate to. + */ + public void smoothScaleTo(float destination) { + getAnimator().animateZoom(destination); } - } - private void broadcastProgrammaticPanUpdate() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanUpdate( getScrollX(), getScrollY(), null ); + /** + * Animates the ZoomPanLayout to the scale provided, while maintaining position determined by + * the focal point provided. + * + * @param focusX The horizontal focal point to maintain, relative to the screen (as supplied by MotionEvent.getX). + * @param focusY The vertical focal point to maintain, relative to the screen (as supplied by MotionEvent.getY). + * @param scale The final scale value the ZoomPanLayout should animate to. + */ + public void smoothScaleFromFocalPoint(int focusX, int focusY, float scale) { + scale = getConstrainedDestinationScale(scale); + if (scale == mScale) { + return; + } + int x = getOffsetScrollXFromScale(focusX, scale, mScale); + int y = getOffsetScrollYFromScale(focusY, scale, mScale); + getAnimator().animateZoomPan(x, y, scale); } - } - private void broadcastProgrammaticPanEnd() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onPanEnd( getScrollX(), getScrollY(), null ); + /** + * Animate the scale of the ZoomPanLayout while maintaining the current center point. + * + * @param scale The final scale value the ZoomPanLayout should animate to. + */ + public void smoothScaleFromCenter(float scale) { + smoothScaleFromFocalPoint(getHalfWidth(), getHalfHeight(), scale); } - } - private void broadcastPinchBegin() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onZoomBegin( mScale, ZoomPanListener.Origination.PINCH ); + /** + * Provide this method to be overriden by subclasses, e.g., onScrollChanged. + */ + public void onScaleChanged(float currentScale, float previousScale) { + // noop } - } - private void broadcastPinchUpdate() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onZoomUpdate( mScale, ZoomPanListener.Origination.PINCH ); + private float getConstrainedDestinationScale(float scale) { + scale = Math.max(scale, mEffectiveMinScale); + scale = Math.min(scale, mMaxScale); + return scale; } - } - private void broadcastPinchEnd() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onZoomEnd( mScale, ZoomPanListener.Origination.PINCH ); + private void constrainScrollToLimits() { + int x = getScrollX(); + int y = getScrollY(); + int constrainedX = getConstrainedScrollX(x); + int constrainedY = getConstrainedScrollY(y); + if (x != constrainedX || y != constrainedY) { + scrollTo(constrainedX, constrainedY); + } } - } - private void broadcastProgrammaticZoomBegin() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onZoomBegin( mScale, null ); + private void updateScaledDimensions() { + mScaledWidth = FloatMathHelper.scale(mBaseWidth, mScale); + mScaledHeight = FloatMathHelper.scale(mBaseHeight, mScale); } - } - private void broadcastProgrammaticZoomUpdate() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onZoomUpdate( mScale, null ); + protected ZoomPanAnimator getAnimator() { + if (mZoomPanAnimator == null) { + mZoomPanAnimator = new ZoomPanAnimator(this); + mZoomPanAnimator.setDuration(mAnimationDuration); + } + return mZoomPanAnimator; } - } - private void broadcastProgrammaticZoomEnd() { - for( ZoomPanListener listener : mZoomPanListeners ) { - listener.onZoomEnd( mScale, null ); + private int getOffsetScrollXFromScale(int offsetX, float destinationScale, float currentScale) { + int scrollX = getScrollX() + offsetX; + float deltaScale = destinationScale / currentScale; + return (int) (scrollX * deltaScale) - offsetX; } - } - @Override - public boolean onDown( MotionEvent event ) { - if( mIsFlinging && !mScroller.isFinished() ) { - mScroller.forceFinished( true ); - mIsFlinging = false; - broadcastFlingEnd(); + private int getOffsetScrollYFromScale(int offsetY, float destinationScale, float currentScale) { + int scrollY = getScrollY() + offsetY; + float deltaScale = destinationScale / currentScale; + return (int) (scrollY * deltaScale) - offsetY; } - return true; - } - @Override - public boolean onFling( MotionEvent event1, MotionEvent event2, float velocityX, float velocityY ) { - mScroller.fling( getScrollX(), getScrollY(), (int) -velocityX, (int) -velocityY, 0, getScrollLimitX(), 0, getScrollLimitY() ); - mIsFlinging = true; - ViewCompat.postInvalidateOnAnimation( this ); - broadcastFlingBegin(); - return true; - } - - @Override - public void onLongPress( MotionEvent event ) { - - } - - @Override - public boolean onScroll( MotionEvent e1, MotionEvent e2, float distanceX, float distanceY ) { - int scrollEndX = getScrollX() + (int) distanceX; - int scrollEndY = getScrollY() + (int) distanceY; - scrollTo( scrollEndX, scrollEndY ); - if( !mIsDragging ) { - mIsDragging = true; - broadcastDragBegin(); - } else { - broadcastDragUpdate(); - } - return true; - } - - @Override - public void onShowPress( MotionEvent event ) { - - } - - @Override - public boolean onSingleTapUp( MotionEvent event ) { - return true; - } - - @Override - public boolean onSingleTapConfirmed( MotionEvent event ) { - return true; - } - - @Override - public boolean onDoubleTap( MotionEvent event ) { - float destination = (float)( Math.pow( 2, Math.floor( Math.log( mScale * 2 ) / Math.log( 2 ) ) ) ); - float effectiveDestination = mShouldLoopScale && mScale >= mMaxScale ? mMinScale : destination; - destination = getConstrainedDestinationScale( effectiveDestination ); - smoothScaleFromFocalPoint( (int) event.getX(), (int) event.getY(), destination ); - return true; - } - - @Override - public boolean onDoubleTapEvent( MotionEvent event ) { - return true; - } - - @Override - public boolean onTouchUp( MotionEvent event ) { - if( mIsDragging ) { - mIsDragging = false; - if( !mIsFlinging ) { - broadcastDragEnd(); - } - } - return true; - } - - @Override - public boolean onScaleBegin( ScaleGestureDetector scaleGestureDetector ) { - mIsScaling = true; - broadcastPinchBegin(); - return true; - } - - @Override - public void onScaleEnd( ScaleGestureDetector scaleGestureDetector ) { - mIsScaling = false; - broadcastPinchEnd(); - } - - @Override - public boolean onScale( ScaleGestureDetector scaleGestureDetector ) { - float currentScale = mScale * mScaleGestureDetector.getScaleFactor(); - setScaleFromPosition( - (int) scaleGestureDetector.getFocusX(), - (int) scaleGestureDetector.getFocusY(), - currentScale ); - broadcastPinchUpdate(); - return true; - } - - private static class ZoomPanAnimator extends ValueAnimator implements - ValueAnimator.AnimatorUpdateListener, - ValueAnimator.AnimatorListener { - - private WeakReference mZoomPanLayoutWeakReference; - private ZoomPanState mStartState = new ZoomPanState(); - private ZoomPanState mEndState = new ZoomPanState(); - private boolean mHasPendingZoomUpdates; - private boolean mHasPendingPanUpdates; - - public ZoomPanAnimator( ZoomPanLayout zoomPanLayout ) { - super(); - addUpdateListener( this ); - addListener( this ); - setFloatValues( 0f, 1f ); - setInterpolator( new FastEaseInInterpolator() ); - mZoomPanLayoutWeakReference = new WeakReference( zoomPanLayout ); - } - - private boolean setupPanAnimation( int x, int y ) { - ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); - if( zoomPanLayout != null ) { - mStartState.x = zoomPanLayout.getScrollX(); - mStartState.y = zoomPanLayout.getScrollY(); - mEndState.x = x; - mEndState.y = y; - return mStartState.x != mEndState.x || mStartState.y != mEndState.y; - } - return false; - } - - private boolean setupZoomAnimation( float scale ) { - ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); - if( zoomPanLayout != null ) { - mStartState.scale = zoomPanLayout.getScale(); - mEndState.scale = scale; - return mStartState.scale != mEndState.scale; - } - return false; - } - - public void animateZoomPan( int x, int y, float scale ) { - ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); - if( zoomPanLayout != null ) { - mHasPendingZoomUpdates = setupZoomAnimation( scale ); - mHasPendingPanUpdates = setupPanAnimation( x, y ); - if( mHasPendingPanUpdates || mHasPendingZoomUpdates ) { - start(); - } - } - } - - public void animateZoom( float scale ) { - ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); - if( zoomPanLayout != null ) { - mHasPendingZoomUpdates = setupZoomAnimation( scale ); - if( mHasPendingZoomUpdates ) { - start(); - } - } - } - - public void animatePan( int x, int y ) { - ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); - if( zoomPanLayout != null ) { - mHasPendingPanUpdates = setupPanAnimation( x, y ); - if( mHasPendingPanUpdates ) { - start(); - } - } + public void setScaleFromPosition(int offsetX, int offsetY, float scale) { + scale = getConstrainedDestinationScale(scale); + if (scale == mScale) { + return; + } + int x = getOffsetScrollXFromScale(offsetX, scale, mScale); + int y = getOffsetScrollYFromScale(offsetY, scale, mScale); + + setScale(scale); + + x = getConstrainedScrollX(x); + y = getConstrainedScrollY(y); + + scrollTo(x, y); } @Override - public void onAnimationUpdate( ValueAnimator animation ) { - ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); - if( zoomPanLayout != null ) { - float progress = (float) animation.getAnimatedValue(); - if( mHasPendingZoomUpdates ) { - float scale = mStartState.scale + (mEndState.scale - mStartState.scale) * progress; - zoomPanLayout.setScale( scale ); - zoomPanLayout.broadcastProgrammaticZoomUpdate(); + public boolean canScrollHorizontally(int direction) { + int position = getScrollX(); + return direction > 0 ? position < getScrollLimitX() : direction < 0 && position > 0; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean gestureIntercept = mGestureDetector.onTouchEvent(event); + boolean scaleIntercept = mScaleGestureDetector.onTouchEvent(event); + boolean touchIntercept = mTouchUpGestureDetector.onTouchEvent(event); + return gestureIntercept || scaleIntercept || touchIntercept || super.onTouchEvent(event); + } + + @Override + public void scrollTo(int x, int y) { + x = getConstrainedScrollX(x); + y = getConstrainedScrollY(y); + super.scrollTo(x, y); + } + + private void calculateMinimumScaleToFit() { + float minimumScaleX = getWidth() / (float) mBaseWidth; + float minimumScaleY = getHeight() / (float) mBaseHeight; + float recalculatedMinScale = calculatedMinScale(minimumScaleX, minimumScaleY); + if (recalculatedMinScale != mEffectiveMinScale) { + mEffectiveMinScale = recalculatedMinScale; + if (mScale < mEffectiveMinScale) { + setScale(mEffectiveMinScale); + } } - if( mHasPendingPanUpdates ) { - int x = (int) (mStartState.x + (mEndState.x - mStartState.x) * progress); - int y = (int) (mStartState.y + (mEndState.y - mStartState.y) * progress); - zoomPanLayout.scrollTo( x, y ); - zoomPanLayout.broadcastProgrammaticPanUpdate(); + } + + private float calculatedMinScale(float minimumScaleX, float minimumScaleY) { + switch (mMinimumScaleMode) { + case FILL: + return Math.max(minimumScaleX, minimumScaleY); + case FIT: + return Math.min(minimumScaleX, minimumScaleY); } - } + + return mMinScale; + } + + protected int getHalfWidth() { + return FloatMathHelper.scale(getWidth(), 0.5f); + } + + protected int getHalfHeight() { + return FloatMathHelper.scale(getHeight(), 0.5f); + } + + private int getConstrainedScrollX(int x) { + return Math.max(0, Math.min(x, getScrollLimitX())); + } + + private int getConstrainedScrollY(int y) { + return Math.max(0, Math.min(y, getScrollLimitY())); + } + + private int getScrollLimitX() { + return mScaledWidth - getWidth(); + } + + private int getScrollLimitY() { + return mScaledHeight - getHeight(); } @Override - public void onAnimationStart( Animator animator ) { - ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); - if( zoomPanLayout != null ) { - if( mHasPendingZoomUpdates ) { - zoomPanLayout.mIsScaling = true; - zoomPanLayout.broadcastProgrammaticZoomBegin(); + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + int startX = getScrollX(); + int startY = getScrollY(); + int endX = getConstrainedScrollX(mScroller.getCurrX()); + int endY = getConstrainedScrollY(mScroller.getCurrY()); + if (startX != endX || startY != endY) { + scrollTo(endX, endY); + if (mIsFlinging) { + broadcastFlingUpdate(); + } + } + if (mScroller.isFinished()) { + if (mIsFlinging) { + mIsFlinging = false; + broadcastFlingEnd(); + } + } else { + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + + private void broadcastDragBegin() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanBegin(getScrollX(), getScrollY(), ZoomPanListener.Origination.DRAG); + } + } + + private void broadcastDragUpdate() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanUpdate(getScrollX(), getScrollY(), ZoomPanListener.Origination.DRAG); + } + } + + private void broadcastDragEnd() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanEnd(getScrollX(), getScrollY(), ZoomPanListener.Origination.DRAG); + } + } + + private void broadcastFlingBegin() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanBegin(mScroller.getStartX(), mScroller.getStartY(), ZoomPanListener.Origination.FLING); + } + } + + private void broadcastFlingUpdate() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanUpdate(mScroller.getCurrX(), mScroller.getCurrY(), ZoomPanListener.Origination.FLING); } - if( mHasPendingPanUpdates ) { - zoomPanLayout.mIsSliding = true; - zoomPanLayout.broadcastProgrammaticPanBegin(); + } + + private void broadcastFlingEnd() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanEnd(mScroller.getFinalX(), mScroller.getFinalY(), ZoomPanListener.Origination.FLING); + } + } + + private void broadcastProgrammaticPanBegin() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanBegin(getScrollX(), getScrollY(), null); + } + } + + private void broadcastProgrammaticPanUpdate() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanUpdate(getScrollX(), getScrollY(), null); + } + } + + private void broadcastProgrammaticPanEnd() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onPanEnd(getScrollX(), getScrollY(), null); + } + } + + private void broadcastPinchBegin() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onZoomBegin(mScale, ZoomPanListener.Origination.PINCH); + } + } + + private void broadcastPinchUpdate() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onZoomUpdate(mScale, ZoomPanListener.Origination.PINCH); + } + } + + private void broadcastPinchEnd() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onZoomEnd(mScale, ZoomPanListener.Origination.PINCH); + } + } + + private void broadcastProgrammaticZoomBegin() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onZoomBegin(mScale, null); + } + } + + private void broadcastProgrammaticZoomUpdate() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onZoomUpdate(mScale, null); + } + } + + private void broadcastProgrammaticZoomEnd() { + for (ZoomPanListener listener : mZoomPanListeners) { + listener.onZoomEnd(mScale, null); } - } } @Override - public void onAnimationEnd( Animator animator ) { - ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); - if( zoomPanLayout != null ) { - if( mHasPendingZoomUpdates ) { - mHasPendingZoomUpdates = false; - zoomPanLayout.mIsScaling = false; - zoomPanLayout.broadcastProgrammaticZoomEnd(); + public boolean onDown(MotionEvent event) { + if (mIsFlinging && !mScroller.isFinished()) { + mScroller.forceFinished(true); + mIsFlinging = false; + broadcastFlingEnd(); } - if( mHasPendingPanUpdates ) { - mHasPendingPanUpdates = false; - zoomPanLayout.mIsSliding = false; - zoomPanLayout.broadcastProgrammaticPanEnd(); + return true; + } + + @Override + public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) { + mScroller.fling(getScrollX(), getScrollY(), (int) -velocityX, (int) -velocityY, 0, getScrollLimitX(), 0, getScrollLimitY()); + mIsFlinging = true; + ViewCompat.postInvalidateOnAnimation(this); + broadcastFlingBegin(); + return true; + } + + @Override + public void onLongPress(MotionEvent event) { + + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + int scrollEndX = getScrollX() + (int) distanceX; + int scrollEndY = getScrollY() + (int) distanceY; + scrollTo(scrollEndX, scrollEndY); + if (!mIsDragging) { + mIsDragging = true; + broadcastDragBegin(); + } else { + broadcastDragUpdate(); } - } + return true; } @Override - public void onAnimationCancel( Animator animator ) { - onAnimationEnd( animator ); + public void onShowPress(MotionEvent event) { + } @Override - public void onAnimationRepeat( Animator animator ) { + public boolean onSingleTapUp(MotionEvent event) { + return true; + } + @Override + public boolean onSingleTapConfirmed(MotionEvent event) { + return false; } - private static class ZoomPanState { - public int x; - public int y; - public float scale; + @Override + public boolean onDoubleTap(MotionEvent event) { + float destination = (float) (Math.pow(2, Math.floor(Math.log(mScale * 2) / Math.log(2)))); + float effectiveDestination = mShouldLoopScale && mScale >= mMaxScale ? mMinScale : destination; + destination = getConstrainedDestinationScale(effectiveDestination); + smoothScaleFromFocalPoint((int) event.getX(), (int) event.getY(), destination); + return true; } - private static class FastEaseInInterpolator implements Interpolator { - @Override - public float getInterpolation( float input ) { - return (float) (1 - Math.pow( 1 - input, 8 )); - } + @Override + public boolean onDoubleTapEvent(MotionEvent event) { + return true; } - } - public interface ZoomPanListener { - enum Origination { - DRAG, - FLING, - PINCH + @Override + public boolean onTouchUp(MotionEvent event) { + if (mIsDragging) { + mIsDragging = false; + if (!mIsFlinging) { + broadcastDragEnd(); + } + } + return true; } - void onPanBegin( int x, int y, Origination origin ); - void onPanUpdate( int x, int y, Origination origin ); - void onPanEnd( int x, int y, Origination origin ); - void onZoomBegin( float scale, Origination origin ); - void onZoomUpdate( float scale, Origination origin ); - void onZoomEnd( float scale, Origination origin ); - } - public enum MinimumScaleMode { - /** - * Limit the minimum scale to no less than what - * would be required to fill the container - */ - FILL, + @Override + public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { + mIsScaling = true; + broadcastPinchBegin(); + return true; + } - /** - * Limit the minimum scale to no less than what - * would be required to fit inside the container - */ - FIT, + @Override + public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) { + mIsScaling = false; + broadcastPinchEnd(); + } - /** - * Allow arbitrary minimum scale. - */ - NONE - } + @Override + public boolean onScale(ScaleGestureDetector scaleGestureDetector) { + float currentScale = mScale * mScaleGestureDetector.getScaleFactor(); + setScaleFromPosition( + (int) scaleGestureDetector.getFocusX(), + (int) scaleGestureDetector.getFocusY(), + currentScale); + broadcastPinchUpdate(); + return true; + } + + public enum MinimumScaleMode { + /** + * Limit the minimum scale to no less than what + * would be required to fill the container + */ + FILL, + + /** + * Limit the minimum scale to no less than what + * would be required to fit inside the container + */ + FIT, + + /** + * Allow arbitrary minimum scale. + */ + NONE + } + + public interface ZoomPanListener { + void onPanBegin(int x, int y, Origination origin); + + void onPanUpdate(int x, int y, Origination origin); + + void onPanEnd(int x, int y, Origination origin); + + void onZoomBegin(float scale, Origination origin); + + void onZoomUpdate(float scale, Origination origin); + + void onZoomEnd(float scale, Origination origin); + + enum Origination { + DRAG, + FLING, + PINCH + } + } + + private static class ZoomPanAnimator extends ValueAnimator implements + ValueAnimator.AnimatorUpdateListener, + ValueAnimator.AnimatorListener { + + private WeakReference mZoomPanLayoutWeakReference; + private ZoomPanState mStartState = new ZoomPanState(); + private ZoomPanState mEndState = new ZoomPanState(); + private boolean mHasPendingZoomUpdates; + private boolean mHasPendingPanUpdates; + + public ZoomPanAnimator(ZoomPanLayout zoomPanLayout) { + super(); + addUpdateListener(this); + addListener(this); + setFloatValues(0f, 1f); + setInterpolator(new FastEaseInInterpolator()); + mZoomPanLayoutWeakReference = new WeakReference(zoomPanLayout); + } + + private boolean setupPanAnimation(int x, int y) { + ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); + if (zoomPanLayout != null) { + mStartState.x = zoomPanLayout.getScrollX(); + mStartState.y = zoomPanLayout.getScrollY(); + mEndState.x = x; + mEndState.y = y; + return mStartState.x != mEndState.x || mStartState.y != mEndState.y; + } + return false; + } + + private boolean setupZoomAnimation(float scale) { + ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); + if (zoomPanLayout != null) { + mStartState.scale = zoomPanLayout.getScale(); + mEndState.scale = scale; + return mStartState.scale != mEndState.scale; + } + return false; + } + + public void animateZoomPan(int x, int y, float scale) { + ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); + if (zoomPanLayout != null) { + mHasPendingZoomUpdates = setupZoomAnimation(scale); + mHasPendingPanUpdates = setupPanAnimation(x, y); + if (mHasPendingPanUpdates || mHasPendingZoomUpdates) { + start(); + } + } + } + + public void animateZoom(float scale) { + ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); + if (zoomPanLayout != null) { + mHasPendingZoomUpdates = setupZoomAnimation(scale); + if (mHasPendingZoomUpdates) { + start(); + } + } + } + + public void animatePan(int x, int y) { + ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); + if (zoomPanLayout != null) { + mHasPendingPanUpdates = setupPanAnimation(x, y); + if (mHasPendingPanUpdates) { + start(); + } + } + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); + if (zoomPanLayout != null) { + float progress = (float) animation.getAnimatedValue(); + if (mHasPendingZoomUpdates) { + float scale = mStartState.scale + (mEndState.scale - mStartState.scale) * progress; + zoomPanLayout.setScale(scale); + zoomPanLayout.broadcastProgrammaticZoomUpdate(); + } + if (mHasPendingPanUpdates) { + int x = (int) (mStartState.x + (mEndState.x - mStartState.x) * progress); + int y = (int) (mStartState.y + (mEndState.y - mStartState.y) * progress); + zoomPanLayout.scrollTo(x, y); + zoomPanLayout.broadcastProgrammaticPanUpdate(); + } + } + } + + @Override + public void onAnimationStart(Animator animator) { + ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); + if (zoomPanLayout != null) { + if (mHasPendingZoomUpdates) { + zoomPanLayout.mIsScaling = true; + zoomPanLayout.broadcastProgrammaticZoomBegin(); + } + if (mHasPendingPanUpdates) { + zoomPanLayout.mIsSliding = true; + zoomPanLayout.broadcastProgrammaticPanBegin(); + } + } + } + + @Override + public void onAnimationEnd(Animator animator) { + ZoomPanLayout zoomPanLayout = mZoomPanLayoutWeakReference.get(); + if (zoomPanLayout != null) { + if (mHasPendingZoomUpdates) { + mHasPendingZoomUpdates = false; + zoomPanLayout.mIsScaling = false; + zoomPanLayout.broadcastProgrammaticZoomEnd(); + } + if (mHasPendingPanUpdates) { + mHasPendingPanUpdates = false; + zoomPanLayout.mIsSliding = false; + zoomPanLayout.broadcastProgrammaticPanEnd(); + } + } + } + + @Override + public void onAnimationCancel(Animator animator) { + onAnimationEnd(animator); + } + + @Override + public void onAnimationRepeat(Animator animator) { + + } + + private static class ZoomPanState { + public int x; + public int y; + public float scale; + } + + private static class FastEaseInInterpolator implements Interpolator { + @Override + public float getInterpolation(float input) { + return (float) (1 - Math.pow(1 - input, 8)); + } + } + } }