diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80478e9d3..31e734aed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,6 +46,7 @@ play-services-maps = { module = "com.google.android.gms:play-services-maps", ver core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } kxml2 = { module = "net.sf.kxml:kxml2", version.ref = "kxml2" } +androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lint" } lint-checks = { module = "com.android.tools.lint:lint-checks", version.ref = "lint" } diff --git a/library/build.gradle.kts b/library/build.gradle.kts index c7d9b1979..3e1dae575 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { testImplementation(libs.mockk) testImplementation(libs.kotlin.test) testImplementation(libs.truth) + testImplementation(libs.androidx.test.core) implementation(libs.kotlin.stdlib.jdk8) testImplementation(libs.mockk) diff --git a/library/src/main/java/com/google/maps/android/ui/AnimationUtil.java b/library/src/main/java/com/google/maps/android/ui/AnimationUtil.java deleted file mode 100644 index 6aad46f0c..000000000 --- a/library/src/main/java/com/google/maps/android/ui/AnimationUtil.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2023 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.maps.android.ui; - - -import android.os.Handler; -import android.os.SystemClock; -import android.view.animation.AccelerateDecelerateInterpolator; -import android.view.animation.Interpolator; - -import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.Marker; - -/** - * Animation utilities for markers with Maps API. - *
- */ -public class AnimationUtil { - - /** - * Animates a marker from it's current position to the provided finalPosition - * - * @param marker marker to animate - * @param finalPosition the final position of the marker after the animation - */ - public static void animateMarkerTo(final Marker marker, final LatLng finalPosition) { - animateMarkerTo(marker, finalPosition, 2000); // delegate to new version - } - - - /** - * Animates a marker from its current position to the provided finalPosition. - * - * @param marker marker to animate - * @param finalPosition the final position of the marker after the animation - * @param durationInMs the duration of the animation in milliseconds - */ - public static void animateMarkerTo( - final Marker marker, - final LatLng finalPosition, - final long durationInMs - ) { - final LatLngInterpolator latLngInterpolator = new LatLngInterpolator.Linear(); - final LatLng startPosition = marker.getPosition(); - final Handler handler = new Handler(); - final long start = SystemClock.uptimeMillis(); - final Interpolator interpolator = new AccelerateDecelerateInterpolator(); - - handler.post(new Runnable() { - long elapsed; - - float t; - - float v; - - @Override - public void run() { - // Calculate progress using interpolator - elapsed = SystemClock.uptimeMillis() - start; - t = elapsed / (float) durationInMs; - v = interpolator.getInterpolation(t); - - marker.setPosition(latLngInterpolator.interpolate(v, startPosition, finalPosition)); - - // Repeat till progress is complete. - if (t < 1) { - // Post again 16ms later. - handler.postDelayed(this, 16); - } - } - }); - } - - /** - * For other LatLngInterpolator interpolators, see link here - */ - interface LatLngInterpolator { - - LatLng interpolate(float fraction, LatLng a, LatLng b); - - class Linear implements LatLngInterpolator { - - @Override - public LatLng interpolate(float fraction, LatLng a, LatLng b) { - double lat = (b.latitude - a.latitude) * fraction + a.latitude; - double lngDelta = b.longitude - a.longitude; - - // Take the shortest path across the 180th meridian. - if (Math.abs(lngDelta) > 180) { - lngDelta -= Math.signum(lngDelta) * 360; - } - double lng = lngDelta * fraction + a.longitude; - return new LatLng(lat, lng); - } - } - } -} \ No newline at end of file diff --git a/library/src/main/java/com/google/maps/android/ui/AnimationUtil.kt b/library/src/main/java/com/google/maps/android/ui/AnimationUtil.kt new file mode 100644 index 000000000..ca5a17665 --- /dev/null +++ b/library/src/main/java/com/google/maps/android/ui/AnimationUtil.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2025 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.ui + +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.view.animation.AccelerateDecelerateInterpolator +import android.view.animation.Interpolator +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Marker +import kotlin.math.abs +import kotlin.math.sign + +/** + * Animation utilities for markers with Maps API. + */ +object AnimationUtil { + /** + * Animates a marker from it's current position to the provided finalPosition + * + * @param marker marker to animate + * @param finalPosition the final position of the marker after the animation + */ + @JvmStatic + fun animateMarkerTo(marker: Marker, finalPosition: LatLng) { + animateMarkerTo(marker, finalPosition, 2000) // delegate to new version + } + + /** + * Animates a marker from its current position to the provided finalPosition. + * + * @param marker marker to animate + * @param finalPosition the final position of the marker after the animation + * @param durationInMs the duration of the animation in milliseconds + */ + @JvmStatic + fun animateMarkerTo( + marker: Marker, + finalPosition: LatLng, + durationInMs: Long + ) { + val latLngInterpolator: LatLngInterpolator = LatLngInterpolator.Linear() + val startPosition = marker.position + val handler = Handler(Looper.getMainLooper()) + val start = SystemClock.uptimeMillis() + val interpolator: Interpolator = AccelerateDecelerateInterpolator() + handler.post(object : Runnable { + var elapsed: Long = 0 + var t = 0f + var v = 0f + override fun run() { + // Calculate progress using interpolator + elapsed = SystemClock.uptimeMillis() - start + t = elapsed / durationInMs.toFloat() + v = interpolator.getInterpolation(t) + marker.position = latLngInterpolator.interpolate(v, startPosition, finalPosition) + + // Repeat till progress is complete. + if (t < 1) { + // Post again 16ms later. + handler.postDelayed(this, 16) + } + } + }) + } + + /** + * For other LatLngInterpolator interpolators, see [this link](https://gist.github.com/broady/6314689) + */ + fun interface LatLngInterpolator { + fun interpolate(fraction: Float, a: LatLng, b: LatLng): LatLng + class Linear : LatLngInterpolator { + override fun interpolate(fraction: Float, a: LatLng, b: LatLng): LatLng { + val lat = (b.latitude - a.latitude) * fraction + a.latitude + var lngDelta = b.longitude - a.longitude + + // Take the shortest path across the 180th meridian. + if (abs(lngDelta) > 180) { + lngDelta -= sign(lngDelta) * 360 + } + val lng = lngDelta * fraction + a.longitude + return LatLng(lat, lng) + } + } + } +} diff --git a/library/src/main/java/com/google/maps/android/ui/BubbleDrawable.java b/library/src/main/java/com/google/maps/android/ui/BubbleDrawable.java deleted file mode 100644 index fb33c9273..000000000 --- a/library/src/main/java/com/google/maps/android/ui/BubbleDrawable.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.ui; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; - -import com.google.maps.android.R; - -import androidx.core.content.ContextCompat; - -/** - * Draws a bubble with a shadow, filled with any color. - */ -class BubbleDrawable extends Drawable { - - private final Drawable mShadow; - private final Drawable mMask; - private int mColor = Color.WHITE; - - public BubbleDrawable(Context context) { - mMask = ContextCompat.getDrawable(context, R.drawable.amu_bubble_mask); - mShadow = ContextCompat.getDrawable(context, R.drawable.amu_bubble_shadow); - } - - public void setColor(int color) { - mColor = color; - } - - @Override - public void draw(Canvas canvas) { - mMask.draw(canvas); - canvas.drawColor(mColor, PorterDuff.Mode.SRC_IN); - mShadow.draw(canvas); - } - - @Override - public void setAlpha(int alpha) { - throw new UnsupportedOperationException(); - } - - @Override - public void setColorFilter(ColorFilter cf) { - throw new UnsupportedOperationException(); - } - - @Override - public int getOpacity() { - return PixelFormat.TRANSLUCENT; - } - - @Override - public void setBounds(int left, int top, int right, int bottom) { - mMask.setBounds(left, top, right, bottom); - mShadow.setBounds(left, top, right, bottom); - } - - @Override - public void setBounds(Rect bounds) { - mMask.setBounds(bounds); - mShadow.setBounds(bounds); - } - - @Override - public boolean getPadding(Rect padding) { - return mMask.getPadding(padding); - } -} diff --git a/library/src/main/java/com/google/maps/android/ui/BubbleDrawable.kt b/library/src/main/java/com/google/maps/android/ui/BubbleDrawable.kt new file mode 100644 index 000000000..96ed6095c --- /dev/null +++ b/library/src/main/java/com/google/maps/android/ui/BubbleDrawable.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2025 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.ui + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.Rect +import android.graphics.drawable.Drawable +import androidx.core.content.ContextCompat +import com.google.maps.android.R + +/** + * Draws a bubble with a shadow, filled with any color. + */ +internal class BubbleDrawable(context: Context) : Drawable() { + private val shadow: Drawable + private val mask: Drawable + var color = Color.WHITE + + init { + mask = ContextCompat.getDrawable(context, R.drawable.amu_bubble_mask)!! + shadow = ContextCompat.getDrawable(context, R.drawable.amu_bubble_shadow)!! + } + + override fun draw(canvas: Canvas) { + mask.draw(canvas) + canvas.drawColor(color, PorterDuff.Mode.SRC_IN) + shadow.draw(canvas) + } + + override fun setAlpha(alpha: Int) { + throw UnsupportedOperationException() + } + + override fun setColorFilter(cf: ColorFilter?) { + throw UnsupportedOperationException() + } + + override fun getOpacity(): Int { + return PixelFormat.TRANSLUCENT + } + + override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) { + mask.setBounds(left, top, right, bottom) + shadow.setBounds(left, top, right, bottom) + } + + override fun setBounds(bounds: Rect) { + mask.setBounds(bounds) + shadow.setBounds(bounds) + } + + override fun getPadding(padding: Rect): Boolean { + return mask.getPadding(padding) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/google/maps/android/ui/BubbleIconFactory.java b/library/src/main/java/com/google/maps/android/ui/BubbleIconFactory.java deleted file mode 100644 index 9c2c5ce79..000000000 --- a/library/src/main/java/com/google/maps/android/ui/BubbleIconFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.ui; - -/** - * Use {@link IconGenerator} instead. - */ -@Deprecated -public class BubbleIconFactory { -} diff --git a/library/src/main/java/com/google/maps/android/ui/IconGenerator.java b/library/src/main/java/com/google/maps/android/ui/IconGenerator.java deleted file mode 100644 index 37eb3e5e5..000000000 --- a/library/src/main/java/com/google/maps/android/ui/IconGenerator.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright 2013 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.maps.android.ui; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.google.maps.android.R; - -/** - * IconGenerator generates icons that contain text (or custom content) within an info - * window-like shape. - *
- * The icon {@link Bitmap}s generated by the factory should be used in conjunction with a {@link - * com.google.android.gms.maps.model.BitmapDescriptorFactory}. - *
- * This class is not thread safe. - */ -public class IconGenerator { - private final Context mContext; - - private ViewGroup mContainer; - private RotationLayout mRotationLayout; - private TextView mTextView; - private View mContentView; - - private int mRotation; - - private float mAnchorU = 0.5f; - private float mAnchorV = 1f; - private BubbleDrawable mBackground; - - /** - * Creates a new IconGenerator with the default style. - */ - public IconGenerator(Context context) { - mContext = context; - mBackground = new BubbleDrawable(mContext); - mContainer = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.amu_text_bubble, null); - mRotationLayout = (RotationLayout) mContainer.getChildAt(0); - mContentView = mTextView = (TextView) mRotationLayout.findViewById(R.id.amu_text); - setStyle(STYLE_DEFAULT); - } - - /** - * Sets the text content, then creates an icon with the current style. - * - * @param text the text content to display inside the icon. - */ - public Bitmap makeIcon(CharSequence text) { - if (mTextView != null) { - mTextView.setText(text); - } - - return makeIcon(); - } - - /** - * Creates an icon with the current content and style. - *
- * This method is useful if a custom view has previously been set, or if text content is not - * applicable. - */ - public Bitmap makeIcon() { - int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - mContainer.measure(measureSpec, measureSpec); - - int measuredWidth = mContainer.getMeasuredWidth(); - int measuredHeight = mContainer.getMeasuredHeight(); - - mContainer.layout(0, 0, measuredWidth, measuredHeight); - - if (mRotation == 1 || mRotation == 3) { - measuredHeight = mContainer.getMeasuredWidth(); - measuredWidth = mContainer.getMeasuredHeight(); - } - - Bitmap r = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888); - r.eraseColor(Color.TRANSPARENT); - - Canvas canvas = new Canvas(r); - switch (mRotation) { - case 0: - // do nothing - break; - case 1: - canvas.translate(measuredWidth, 0); - canvas.rotate(90); - break; - case 2: - canvas.rotate(180, measuredWidth / 2, measuredHeight / 2); - break; - case 3: - canvas.translate(0, measuredHeight); - canvas.rotate(270); - break; - } - mContainer.draw(canvas); - return r; - } - - /** - * Sets the child view for the icon. - *
- * If the view contains a {@link TextView} with the id "text", operations such as {@link
- * #setTextAppearance} and {@link #makeIcon(CharSequence)} will operate upon that {@link TextView}.
- */
- public void setContentView(View contentView) {
- mRotationLayout.removeAllViews();
- mRotationLayout.addView(contentView);
- mContentView = contentView;
- final View view = mRotationLayout.findViewById(R.id.amu_text);
- mTextView = view instanceof TextView ? (TextView) view : null;
- }
-
- /**
- * Rotates the contents of the icon.
- *
- * @param degrees the amount the contents should be rotated, as a multiple of 90 degrees.
- */
- public void setContentRotation(int degrees) {
- mRotationLayout.setViewRotation(degrees);
- }
-
- /**
- * Rotates the icon.
- *
- * @param degrees the amount the icon should be rotated, as a multiple of 90 degrees.
- */
- public void setRotation(int degrees) {
- mRotation = ((degrees + 360) % 360) / 90;
- }
-
-
- /**
- * @return u coordinate of the anchor, with rotation applied.
- */
- public float getAnchorU() {
- return rotateAnchor(mAnchorU, mAnchorV);
- }
-
- /**
- * @return v coordinate of the anchor, with rotation applied.
- */
- public float getAnchorV() {
- return rotateAnchor(mAnchorV, mAnchorU);
- }
-
- /**
- * Rotates the anchor around (u, v) = (0, 0).
- */
- private float rotateAnchor(float u, float v) {
- switch (mRotation) {
- case 0:
- return u;
- case 1:
- return 1 - v;
- case 2:
- return 1 - u;
- case 3:
- return v;
- }
- throw new IllegalStateException();
- }
-
- /**
- * Sets the text color, size, style, hint color, and highlight color from the specified
- *
- * May not work with padding.
- */
-public class RotationLayout extends FrameLayout {
- private int mRotation;
-
- public RotationLayout(Context context) {
- super(context);
- }
-
- public RotationLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public RotationLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- if (mRotation == 1 || mRotation == 3) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- setMeasuredDimension(getMeasuredHeight(), getMeasuredWidth());
- } else {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- /**
- * @param degrees the rotation, in degrees.
- */
- public void setViewRotation(int degrees) {
- mRotation = ((degrees + 360) % 360) / 90;
- }
-
-
- @Override
- public void dispatchDraw(Canvas canvas) {
- if (mRotation == 0) {
- super.dispatchDraw(canvas);
- return;
- }
-
- if (mRotation == 1) {
- canvas.translate(getWidth(), 0);
- canvas.rotate(90, getWidth() / 2, 0);
- canvas.translate(getHeight() / 2, getWidth() / 2);
- } else if (mRotation == 2) {
- canvas.rotate(180, getWidth() / 2, getHeight() / 2);
- } else {
- canvas.translate(0, getHeight());
- canvas.rotate(270, getWidth() / 2, 0);
- canvas.translate(getHeight() / 2, -getWidth() / 2);
- }
-
- super.dispatchDraw(canvas);
- }
-}
diff --git a/library/src/main/java/com/google/maps/android/ui/RotationLayout.kt b/library/src/main/java/com/google/maps/android/ui/RotationLayout.kt
new file mode 100644
index 000000000..3a244cc1b
--- /dev/null
+++ b/library/src/main/java/com/google/maps/android/ui/RotationLayout.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2025 Google LLC.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.maps.android.ui
+
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+/**
+ * RotationLayout rotates the contents of the layout by multiples of 90 degrees.
+ *
+ *
+ * May not work with padding.
+ */
+class RotationLayout @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : FrameLayout(context, attrs, defStyle) {
+ private var rotation = 0
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ if (rotation == 1 || rotation == 3) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ setMeasuredDimension(measuredHeight, measuredWidth)
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+ }
+
+ /**
+ * @param degrees the rotation, in degrees.
+ */
+ fun setViewRotation(degrees: Int) {
+ rotation = (degrees + 360) % 360 / 90
+ }
+
+ override fun dispatchDraw(canvas: Canvas) {
+ when (rotation) {
+ 0 -> super.dispatchDraw(canvas)
+ 1 -> {
+ canvas.translate(width.toFloat(), 0f)
+ canvas.rotate(90f, width / 2f, 0f)
+ canvas.translate(height / 2f, width / 2f)
+ super.dispatchDraw(canvas)
+ }
+ 2 -> {
+ canvas.rotate(180f, width / 2f, height / 2f)
+ super.dispatchDraw(canvas)
+ }
+ 3 -> {
+ canvas.translate(0f, height.toFloat())
+ canvas.rotate(270f, width / 2f, 0f)
+ canvas.translate(height / 2f, -width / 2f)
+ super.dispatchDraw(canvas)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/google/maps/android/ui/SquareTextView.java b/library/src/main/java/com/google/maps/android/ui/SquareTextView.java
deleted file mode 100644
index 20df4a9d2..000000000
--- a/library/src/main/java/com/google/maps/android/ui/SquareTextView.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2013 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.google.maps.android.ui;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.graphics.Canvas;
-import android.util.AttributeSet;
-import android.widget.TextView;
-
-/**
- * This class is extending from TextView to avoid introducing App Compat dependencies. Android Studio might show an error here or
- * not, depending on the Inspection Settings. It's not really an error, just a warning.
- *
- */
-@SuppressLint("AppCompatCustomView")
-public class SquareTextView extends TextView {
- private int mOffsetTop = 0;
- private int mOffsetLeft = 0;
-
- public SquareTextView(Context context) {
- super(context);
- }
-
- public SquareTextView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public SquareTextView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int width = getMeasuredWidth();
- int height = getMeasuredHeight();
- int dimension = Math.max(width, height);
- if (width > height) {
- mOffsetTop = width - height;
- mOffsetLeft = 0;
- } else {
- mOffsetTop = 0;
- mOffsetLeft = height - width;
- }
- setMeasuredDimension(dimension, dimension);
- }
-
- @Override
- public void draw(Canvas canvas) {
- canvas.translate(mOffsetLeft / 2, mOffsetTop / 2);
- super.draw(canvas);
- }
-}
diff --git a/library/src/main/java/com/google/maps/android/ui/SquareTextView.kt b/library/src/main/java/com/google/maps/android/ui/SquareTextView.kt
new file mode 100644
index 000000000..15850d3c4
--- /dev/null
+++ b/library/src/main/java/com/google/maps/android/ui/SquareTextView.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2025 Google LLC.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.maps.android.ui
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import android.widget.TextView
+import kotlin.math.max
+
+/**
+ * This class is extending from TextView to avoid introducing App Compat dependencies. Android Studio might show an error here or
+ * not, depending on the Inspection Settings. It's not really an error, just a warning.
+ *
+ */
+@SuppressLint("AppCompatCustomView")
+class SquareTextView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : TextView(context, attrs, defStyle) {
+ private var mOffsetTop = 0
+ private var mOffsetLeft = 0
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ val width = measuredWidth
+ val height = measuredHeight
+ val dimension = max(width, height)
+ if (width > height) {
+ mOffsetTop = width - height
+ mOffsetLeft = 0
+ } else {
+ mOffsetTop = 0
+ mOffsetLeft = height - width
+ }
+ setMeasuredDimension(dimension, dimension)
+ }
+
+ override fun draw(canvas: Canvas) {
+ canvas.translate(mOffsetLeft / 2f, mOffsetTop / 2f)
+ super.draw(canvas)
+ }
+}
\ No newline at end of file
diff --git a/library/src/test/java/com/google/maps/android/ui/IconGeneratorTest.kt b/library/src/test/java/com/google/maps/android/ui/IconGeneratorTest.kt
new file mode 100644
index 000000000..ea447845d
--- /dev/null
+++ b/library/src/test/java/com/google/maps/android/ui/IconGeneratorTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.maps.android.ui
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.widget.TextView
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class IconGeneratorTest {
+
+ private lateinit var iconGenerator: IconGenerator
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = ApplicationProvider.getApplicationContext()
+ iconGenerator = IconGenerator(context)
+ }
+
+ @Test
+ fun testMakeIcon() {
+ val icon = iconGenerator.makeIcon("Test")
+ assertThat(icon).isNotNull()
+ assertThat(icon.width).isGreaterThan(0)
+ assertThat(icon.height).isGreaterThan(0)
+ }
+
+ @Test
+ fun testSetContentView() {
+ val textView = TextView(context)
+ textView.text = "Custom View"
+ iconGenerator.setContentView(textView)
+ val icon: Bitmap = iconGenerator.makeIcon()
+ assertThat(icon).isNotNull()
+ assertThat(icon.width).isGreaterThan(0)
+ assertThat(icon.height).isGreaterThan(0)
+ }
+
+ @Test
+ fun testSetRotation() {
+ iconGenerator.setRotation(90)
+ assertThat(iconGenerator.getAnchorU()).isWithin(1e-6f).of(0.0f)
+ assertThat(iconGenerator.getAnchorV()).isWithin(1e-6f).of(0.5f)
+ val icon = iconGenerator.makeIcon("Rotated")
+ assertThat(icon).isNotNull()
+ }
+
+ @Test
+ fun testSetContentRotation() {
+ iconGenerator.setContentRotation(90)
+ val icon = iconGenerator.makeIcon("Content Rotated")
+ assertThat(icon).isNotNull()
+ }
+
+ @Test
+ fun testSetStyle() {
+ iconGenerator.setStyle(IconGenerator.STYLE_RED)
+ // Hard to test the color, but we can check that it doesn't crash
+ val icon = iconGenerator.makeIcon("Styled")
+ assertThat(icon).isNotNull()
+ }
+}
\ No newline at end of file
TextAppearance resource.
- *
- * @param resid the identifier of the resource.
- */
- public void setTextAppearance(Context context, int resid) {
- if (mTextView != null) {
- mTextView.setTextAppearance(context, resid);
- }
- }
-
- /**
- * Sets the text color, size, style, hint color, and highlight color from the specified
- * TextAppearance resource.
- *
- * @param resid the identifier of the resource.
- */
- public void setTextAppearance(int resid) {
- setTextAppearance(mContext, resid);
- }
-
- /**
- * Sets the style of the icon. The style consists of a background and text appearance.
- */
- public void setStyle(int style) {
- setColor(getStyleColor(style));
- setTextAppearance(mContext, getTextStyle(style));
- }
-
- /**
- * Sets the background to the default, with a given color tint.
- *
- * @param color the color for the background tint.
- */
- public void setColor(int color) {
- mBackground.setColor(color);
- setBackground(mBackground);
- }
-
- /**
- * Set the background to a given Drawable, or remove the background.
- *
- * @param background the Drawable to use as the background, or null to remove the background.
- */
- @SuppressWarnings("deprecation")
- // View#setBackgroundDrawable is compatible with pre-API level 16 (Jelly Bean).
- public void setBackground(Drawable background) {
- mContainer.setBackgroundDrawable(background);
-
- // Force setting of padding.
- // setBackgroundDrawable does not call setPadding if the background has 0 padding.
- if (background != null) {
- Rect rect = new Rect();
- background.getPadding(rect);
- mContainer.setPadding(rect.left, rect.top, rect.right, rect.bottom);
- } else {
- mContainer.setPadding(0, 0, 0, 0);
- }
- }
-
- /**
- * Sets the padding of the content view. The default padding of the content view (i.e. text
- * view) is 5dp top/bottom and 10dp left/right.
- *
- * @param left the left padding in pixels.
- * @param top the top padding in pixels.
- * @param right the right padding in pixels.
- * @param bottom the bottom padding in pixels.
- */
- public void setContentPadding(int left, int top, int right, int bottom) {
- mContentView.setPadding(left, top, right, bottom);
- }
-
- public static final int STYLE_DEFAULT = 1;
- public static final int STYLE_WHITE = 2;
- public static final int STYLE_RED = 3;
- public static final int STYLE_BLUE = 4;
- public static final int STYLE_GREEN = 5;
- public static final int STYLE_PURPLE = 6;
- public static final int STYLE_ORANGE = 7;
-
- private static int getStyleColor(int style) {
- switch (style) {
- default:
- case STYLE_DEFAULT:
- case STYLE_WHITE:
- return 0xffffffff;
- case STYLE_RED:
- return 0xffcc0000;
- case STYLE_BLUE:
- return 0xff0099cc;
- case STYLE_GREEN:
- return 0xff669900;
- case STYLE_PURPLE:
- return 0xff9933cc;
- case STYLE_ORANGE:
- return 0xffff8800;
- }
- }
-
- private static int getTextStyle(int style) {
- switch (style) {
- default:
- case STYLE_DEFAULT:
- case STYLE_WHITE:
- return R.style.amu_Bubble_TextAppearance_Dark;
- case STYLE_RED:
- case STYLE_BLUE:
- case STYLE_GREEN:
- case STYLE_PURPLE:
- case STYLE_ORANGE:
- return R.style.amu_Bubble_TextAppearance_Light;
- }
- }
-}
diff --git a/library/src/main/java/com/google/maps/android/ui/IconGenerator.kt b/library/src/main/java/com/google/maps/android/ui/IconGenerator.kt
new file mode 100644
index 000000000..0149b4051
--- /dev/null
+++ b/library/src/main/java/com/google/maps/android/ui/IconGenerator.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2025 Google LLC.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.maps.android.ui
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import com.google.maps.android.R
+import androidx.core.graphics.createBitmap
+
+/**
+ * IconGenerator generates icons that contain text (or custom content) within an info
+ * window-like shape.
+ *
+ *
+ * The icon [Bitmap]s generated by the factory should be used in conjunction with a [com.google.android.gms.maps.model.BitmapDescriptorFactory].
+ *
+ *
+ * This class is not thread safe.
+ */
+class IconGenerator(private val context: Context) {
+ private var container: ViewGroup =
+ LayoutInflater.from(context).inflate(R.layout.amu_text_bubble, null) as ViewGroup
+ private var rotationLayout: RotationLayout = container.getChildAt(0) as RotationLayout
+ private var textView: TextView? = null
+ private var contentView: View? = null
+ private var rotation = 0
+ private var _anchorU = 0.5f
+ private var _anchorV = 1f
+ private val background: BubbleDrawable = BubbleDrawable(context)
+
+ init {
+ textView = rotationLayout.findViewById(R.id.amu_text)
+ contentView = textView
+ setStyle(STYLE_DEFAULT)
+ }
+
+ /**
+ * Sets the text content, then creates an icon with the current style.
+ *
+ * @param text the text content to display inside the icon.
+ */
+ fun makeIcon(text: CharSequence?): Bitmap {
+ textView?.text = text
+ return makeIcon()
+ }
+
+ /**
+ * Creates an icon with the current content and style.
+ *
+ *
+ * This method is useful if a custom view has previously been set, or if text content is not
+ * applicable.
+ */
+ fun makeIcon(): Bitmap {
+ val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+ container.measure(measureSpec, measureSpec)
+ var measuredWidth = container.measuredWidth
+ var measuredHeight = container.measuredHeight
+ container.layout(0, 0, measuredWidth, measuredHeight)
+ if (rotation == 1 || rotation == 3) {
+ measuredHeight = container.measuredWidth
+ measuredWidth = container.measuredHeight
+ }
+ val r = createBitmap(measuredWidth, measuredHeight)
+ r.eraseColor(Color.TRANSPARENT)
+ val canvas = Canvas(r)
+ when (rotation) {
+ 1 -> {
+ canvas.translate(measuredWidth.toFloat(), 0f)
+ canvas.rotate(90f)
+ }
+ 2 -> canvas.rotate(180f, (measuredWidth / 2).toFloat(), (measuredHeight / 2).toFloat())
+ 3 -> {
+ canvas.translate(0f, measuredHeight.toFloat())
+ canvas.rotate(270f)
+ }
+ }
+ container.draw(canvas)
+ return r
+ }
+
+ /**
+ * Sets the child view for the icon.
+ *
+ *
+ * If the view contains a [TextView] with the id "text", operations such as [.setTextAppearance] and [.makeIcon] will operate upon that [TextView].
+ */
+ fun setContentView(contentView: View) {
+ rotationLayout.removeAllViews()
+ rotationLayout.addView(contentView)
+ this.contentView = contentView
+ val view = rotationLayout.findViewById