diff --git a/.github/screenshot3.jpg b/.github/screenshot3.jpg new file mode 100644 index 0000000..2cacb97 Binary files /dev/null and b/.github/screenshot3.jpg differ diff --git a/README.md b/README.md index 1f5f16e..edd554e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@
- * 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.getkeepsafe.taptargetview; - -import java.lang.reflect.Field; - -class ReflectUtil { - ReflectUtil() { - } - - /** Returns the value of the given private field from the source object **/ - static Object getPrivateField(Object source, String fieldName) - throws NoSuchFieldException, IllegalAccessException { - final Field objectField = source.getClass().getDeclaredField(fieldName); - objectField.setAccessible(true); - return objectField.get(source); - } -} diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTarget.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTarget.java deleted file mode 100644 index aeb0ab9..0000000 --- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTarget.java +++ /dev/null @@ -1,502 +0,0 @@ -/** - * Copyright 2016 Keepsafe Software, 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.getkeepsafe.taptargetview; - -import android.content.Context; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import androidx.annotation.ColorInt; -import androidx.annotation.ColorRes; -import androidx.annotation.DimenRes; -import androidx.annotation.IdRes; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.appcompat.widget.Toolbar; -import android.view.View; - -/** - * Describes the properties and options for a {@link TapTargetView}. - *
- * Each tap target describes a target via a pair of bounds and icon. The bounds dictate the - * location and touch area of the target, where the icon is what will be drawn within the center of - * the bounds. - *
- * This class can be extended to support various target types. - * - * @see ViewTapTarget ViewTapTarget for targeting standard Android views - */ -public class TapTarget { - final CharSequence title; - @Nullable - final CharSequence description; - - float outerCircleAlpha = 0.96f; - int targetRadius = 44; - - Rect bounds; - Drawable icon; - Typeface titleTypeface; - Typeface descriptionTypeface; - - @ColorRes - private int outerCircleColorRes = -1; - @ColorRes - private int targetCircleColorRes = -1; - @ColorRes - private int dimColorRes = -1; - @ColorRes - private int titleTextColorRes = -1; - @ColorRes - private int descriptionTextColorRes = -1; - - private Integer outerCircleColor = null; - private Integer targetCircleColor = null; - private Integer dimColor = null; - private Integer titleTextColor = null; - private Integer descriptionTextColor = null; - - @DimenRes - private int titleTextDimen = -1; - @DimenRes - private int descriptionTextDimen = -1; - - private int titleTextSize = 20; - private int descriptionTextSize = 18; - int id = -1; - - boolean drawShadow = false; - boolean cancelable = true; - boolean tintTarget = true; - boolean transparentTarget = false; - float descriptionTextAlpha = 0.54f; - - /** - * Return a tap target for the overflow button from the given toolbar - *
- * Note: This is currently experimental, use at your own risk - */ - public static TapTarget forToolbarOverflow(Toolbar toolbar, CharSequence title) { - return forToolbarOverflow(toolbar, title, null); - } - - /** Return a tap target for the overflow button from the given toolbar - *
- * Note: This is currently experimental, use at your own risk - */ - public static TapTarget forToolbarOverflow(Toolbar toolbar, CharSequence title, - @Nullable CharSequence description) { - return new ToolbarTapTarget(toolbar, false, title, description); - } - - /** Return a tap target for the overflow button from the given toolbar - *
- * Note: This is currently experimental, use at your own risk - */ - public static TapTarget forToolbarOverflow(android.widget.Toolbar toolbar, CharSequence title) { - return forToolbarOverflow(toolbar, title, null); - } - - /** Return a tap target for the overflow button from the given toolbar - *
- * Note: This is currently experimental, use at your own risk - */ - public static TapTarget forToolbarOverflow(android.widget.Toolbar toolbar, CharSequence title, - @Nullable CharSequence description) { - return new ToolbarTapTarget(toolbar, false, title, description); - } - - /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/ - public static TapTarget forToolbarNavigationIcon(Toolbar toolbar, CharSequence title) { - return forToolbarNavigationIcon(toolbar, title, null); - } - - /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/ - public static TapTarget forToolbarNavigationIcon(Toolbar toolbar, CharSequence title, - @Nullable CharSequence description) { - return new ToolbarTapTarget(toolbar, true, title, description); - } - - /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/ - public static TapTarget forToolbarNavigationIcon(android.widget.Toolbar toolbar, CharSequence title) { - return forToolbarNavigationIcon(toolbar, title, null); - } - - /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/ - public static TapTarget forToolbarNavigationIcon(android.widget.Toolbar toolbar, CharSequence title, - @Nullable CharSequence description) { - return new ToolbarTapTarget(toolbar, true, title, description); - } - - /** Return a tap target for the menu item from the given toolbar **/ - public static TapTarget forToolbarMenuItem(Toolbar toolbar, @IdRes int menuItemId, - CharSequence title) { - return forToolbarMenuItem(toolbar, menuItemId, title, null); - } - - /** Return a tap target for the menu item from the given toolbar **/ - public static TapTarget forToolbarMenuItem(Toolbar toolbar, @IdRes int menuItemId, - CharSequence title, @Nullable CharSequence description) { - return new ToolbarTapTarget(toolbar, menuItemId, title, description); - } - - /** Return a tap target for the menu item from the given toolbar **/ - public static TapTarget forToolbarMenuItem(android.widget.Toolbar toolbar, @IdRes int menuItemId, - CharSequence title) { - return forToolbarMenuItem(toolbar, menuItemId, title, null); - } - - /** Return a tap target for the menu item from the given toolbar **/ - public static TapTarget forToolbarMenuItem(android.widget.Toolbar toolbar, @IdRes int menuItemId, - CharSequence title, @Nullable CharSequence description) { - return new ToolbarTapTarget(toolbar, menuItemId, title, description); - } - - /** Return a tap target for the specified view **/ - public static TapTarget forView(View view, CharSequence title) { - return forView(view, title, null); - } - - /** Return a tap target for the specified view **/ - public static TapTarget forView(View view, CharSequence title, @Nullable CharSequence description) { - return new ViewTapTarget(view, title, description); - } - - /** Return a tap target for the specified bounds **/ - public static TapTarget forBounds(Rect bounds, CharSequence title) { - return forBounds(bounds, title, null); - } - - /** Return a tap target for the specified bounds **/ - public static TapTarget forBounds(Rect bounds, CharSequence title, @Nullable CharSequence description) { - return new TapTarget(bounds, title, description); - } - - protected TapTarget(Rect bounds, CharSequence title, @Nullable CharSequence description) { - this(title, description); - if (bounds == null) { - throw new IllegalArgumentException("Cannot pass null bounds or title"); - } - - this.bounds = bounds; - } - - protected TapTarget(CharSequence title, @Nullable CharSequence description) { - if (title == null) { - throw new IllegalArgumentException("Cannot pass null title"); - } - - this.title = title; - this.description = description; - } - - /** Specify whether the target should be transparent **/ - public TapTarget transparentTarget(boolean transparent) { - this.transparentTarget = transparent; - return this; - } - - /** Specify the color resource for the outer circle **/ - public TapTarget outerCircleColor(@ColorRes int color) { - this.outerCircleColorRes = color; - return this; - } - - /** Specify the color value for the outer circle **/ - // TODO(Hilal): In v2, this API should be cleaned up / torched - public TapTarget outerCircleColorInt(@ColorInt int color) { - this.outerCircleColor = color; - return this; - } - - /** Specify the alpha value [0.0, 1.0] of the outer circle **/ - public TapTarget outerCircleAlpha(float alpha) { - if (alpha < 0.0f || alpha > 1.0f) { - throw new IllegalArgumentException("Given an invalid alpha value: " + alpha); - } - this.outerCircleAlpha = alpha; - return this; - } - - /** Specify the color resource for the target circle **/ - public TapTarget targetCircleColor(@ColorRes int color) { - this.targetCircleColorRes = color; - return this; - } - - /** Specify the color value for the target circle **/ - // TODO(Hilal): In v2, this API should be cleaned up / torched - public TapTarget targetCircleColorInt(@ColorInt int color) { - this.targetCircleColor = color; - return this; - } - - /** Specify the color resource for all text **/ - public TapTarget textColor(@ColorRes int color) { - this.titleTextColorRes = color; - this.descriptionTextColorRes = color; - return this; - } - - /** Specify the color value for all text **/ - // TODO(Hilal): In v2, this API should be cleaned up / torched - public TapTarget textColorInt(@ColorInt int color) { - this.titleTextColor = color; - this.descriptionTextColor = color; - return this; - } - - /** Specify the color resource for the title text **/ - public TapTarget titleTextColor(@ColorRes int color) { - this.titleTextColorRes = color; - return this; - } - - /** Specify the color value for the title text **/ - // TODO(Hilal): In v2, this API should be cleaned up / torched - public TapTarget titleTextColorInt(@ColorInt int color) { - this.titleTextColor = color; - return this; - } - - /** Specify the color resource for the description text **/ - public TapTarget descriptionTextColor(@ColorRes int color) { - this.descriptionTextColorRes = color; - return this; - } - - /** Specify the color value for the description text **/ - // TODO(Hilal): In v2, this API should be cleaned up / torched - public TapTarget descriptionTextColorInt(@ColorInt int color) { - this.descriptionTextColor = color; - return this; - } - - /** Specify the typeface for all text **/ - public TapTarget textTypeface(Typeface typeface) { - if (typeface == null) throw new IllegalArgumentException("Cannot use a null typeface"); - titleTypeface = typeface; - descriptionTypeface = typeface; - return this; - } - - /** Specify the typeface for title text **/ - public TapTarget titleTypeface(Typeface titleTypeface) { - if (titleTypeface == null) throw new IllegalArgumentException("Cannot use a null typeface"); - this.titleTypeface = titleTypeface; - return this; - } - - /** Specify the typeface for description text **/ - public TapTarget descriptionTypeface(Typeface descriptionTypeface) { - if (descriptionTypeface == null) throw new IllegalArgumentException("Cannot use a null typeface"); - this.descriptionTypeface = descriptionTypeface; - return this; - } - - /** Specify the text size for the title in SP **/ - public TapTarget titleTextSize(int sp) { - if (sp < 0) throw new IllegalArgumentException("Given negative text size"); - this.titleTextSize = sp; - return this; - } - - /** Specify the text size for the description in SP **/ - public TapTarget descriptionTextSize(int sp) { - if (sp < 0) throw new IllegalArgumentException("Given negative text size"); - this.descriptionTextSize = sp; - return this; - } - - /** - * Specify the text size for the title via a dimen resource - *
- * Note: If set, this value will take precedence over the specified sp size - */ - public TapTarget titleTextDimen(@DimenRes int dimen) { - this.titleTextDimen = dimen; - return this; - } - - /** Specify the alpha value [0.0, 1.0] of the description text **/ - public TapTarget descriptionTextAlpha(float descriptionTextAlpha) { - if (descriptionTextAlpha < 0 || descriptionTextAlpha > 1f) { - throw new IllegalArgumentException("Given an invalid alpha value: " + descriptionTextAlpha); - } - this.descriptionTextAlpha = descriptionTextAlpha; - return this; - } - - /** - * Specify the text size for the description via a dimen resource - *
- * Note: If set, this value will take precedence over the specified sp size - */ - public TapTarget descriptionTextDimen(@DimenRes int dimen) { - this.descriptionTextDimen = dimen; - return this; - } - - /** - * Specify the color resource to use as a dim effect - *
- * Note: The given color will have its opacity modified to 30% automatically - */ - public TapTarget dimColor(@ColorRes int color) { - this.dimColorRes = color; - return this; - } - - /** - * Specify the color value to use as a dim effect - *
- * Note: The given color will have its opacity modified to 30% automatically
- */
- // TODO(Hilal): In v2, this API should be cleaned up / torched
- public TapTarget dimColorInt(@ColorInt int color) {
- this.dimColor = color;
- return this;
- }
-
- /** Specify whether or not to draw a drop shadow around the outer circle **/
- public TapTarget drawShadow(boolean draw) {
- this.drawShadow = draw;
- return this;
- }
-
- /** Specify whether or not the target should be cancelable **/
- public TapTarget cancelable(boolean status) {
- this.cancelable = status;
- return this;
- }
-
- /** Specify whether to tint the target's icon with the outer circle's color **/
- public TapTarget tintTarget(boolean tint) {
- this.tintTarget = tint;
- return this;
- }
-
- /** Specify the icon that will be drawn in the center of the target bounds **/
- public TapTarget icon(Drawable icon) {
- return icon(icon, false);
- }
-
- /**
- * Specify the icon that will be drawn in the center of the target bounds
- * @param hasSetBounds Whether the drawable already has its bounds correctly set. If the
- * drawable does not have its bounds set, then the following bounds will
- * be applied:
- * (0, 0, intrinsic-width, intrinsic-height)
- */
- public TapTarget icon(Drawable icon, boolean hasSetBounds) {
- if (icon == null) throw new IllegalArgumentException("Cannot use null drawable");
- this.icon = icon;
-
- if (!hasSetBounds) {
- this.icon.setBounds(new Rect(0, 0, this.icon.getIntrinsicWidth(), this.icon.getIntrinsicHeight()));
- }
-
- return this;
- }
-
- /** Specify a unique identifier for this target. **/
- public TapTarget id(int id) {
- this.id = id;
- return this;
- }
-
- /** Specify the target radius in dp. **/
- public TapTarget targetRadius(int targetRadius) {
- this.targetRadius = targetRadius;
- return this;
- }
-
- /** Return the id associated with this tap target **/
- public int id() {
- return id;
- }
-
- /**
- * In case your target needs time to be ready (laid out in your view, not created, etc), the
- * runnable passed here will be invoked when the target is ready.
- */
- public void onReady(Runnable runnable) {
- runnable.run();
- }
-
- /**
- * Returns the target bounds. Throws an exception if they are not set
- * (target may not be ready)
- *
- * This will only be called internally when {@link #onReady(Runnable)} invokes its runnable - */ - public Rect bounds() { - if (bounds == null) { - throw new IllegalStateException("Requesting bounds that are not set! Make sure your target is ready"); - } - return bounds; - } - - @Nullable - Integer outerCircleColorInt(Context context) { - return colorResOrInt(context, outerCircleColor, outerCircleColorRes); - } - - @Nullable - Integer targetCircleColorInt(Context context) { - return colorResOrInt(context, targetCircleColor, targetCircleColorRes); - } - - @Nullable - Integer dimColorInt(Context context) { - return colorResOrInt(context, dimColor, dimColorRes); - } - - @Nullable - Integer titleTextColorInt(Context context) { - return colorResOrInt(context, titleTextColor, titleTextColorRes); - } - - @Nullable - Integer descriptionTextColorInt(Context context) { - return colorResOrInt(context, descriptionTextColor, descriptionTextColorRes); - } - - int titleTextSizePx(Context context) { - return dimenOrSize(context, titleTextSize, titleTextDimen); - } - - int descriptionTextSizePx(Context context) { - return dimenOrSize(context, descriptionTextSize, descriptionTextDimen); - } - - @Nullable - private Integer colorResOrInt(Context context, @Nullable Integer value, @ColorRes int resource) { - if (resource != -1) { - return ContextCompat.getColor(context, resource); - } - - return value; - } - - private int dimenOrSize(Context context, int size, @DimenRes int dimen) { - if (dimen != -1) { - return context.getResources().getDimensionPixelSize(dimen); - } - - return UiUtil.sp(context, size); - } -} diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetSequence.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetSequence.java deleted file mode 100644 index 23a33b2..0000000 --- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetSequence.java +++ /dev/null @@ -1,238 +0,0 @@ -/** - * Copyright 2016 Keepsafe Software, 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.getkeepsafe.taptargetview; - -import android.app.Activity; -import android.app.Dialog; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Queue; - -/** - * Displays a sequence of {@link TapTargetView}s. - *
- * Internally, a FIFO queue is held to dictate which {@link TapTarget} will be shown.
- */
-public class TapTargetSequence {
- private final @Nullable Activity activity;
- private final @Nullable Dialog dialog;
- private final Queue
* This class should not be instantiated directly. Instead, please use the
- * {@link #showFor(Activity, TapTarget, Listener)} static factory method instead.
+ * {@see TargetViewExtensionsKTX#showGuideView} static factory method instead.
*
* More information can be found here:
* https://material.google.com/growth-communications/feature-discovery.html#feature-discovery-design
@@ -72,8 +72,6 @@ public class TapTargetView extends View {
private boolean isInteractable = true;
final int TARGET_PADDING;
- final int TARGET_RADIUS;
- final int TARGET_PULSE_RADIUS;
final int TEXT_PADDING;
final int TEXT_SPACING;
final int TEXT_MAX_WIDTH;
@@ -130,10 +128,8 @@ public class TapTargetView extends View {
int[] outerCircleCenter;
int outerCircleAlpha;
- float targetCirclePulseRadius;
int targetCirclePulseAlpha;
- float targetCircleRadius;
int targetCircleAlpha;
int textAlpha;
@@ -152,48 +148,6 @@ public class TapTargetView extends View {
@Nullable
ViewOutlineProvider outlineProvider;
- public static TapTargetView showFor(Activity activity, TapTarget target) {
- return showFor(activity, target, null);
- }
-
- public static TapTargetView showFor(Activity activity, TapTarget target, Listener listener) {
- if (activity == null) throw new IllegalArgumentException("Activity is null");
-
- final ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
- final ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- final ViewGroup content = (ViewGroup) decor.findViewById(android.R.id.content);
- final TapTargetView tapTargetView = new TapTargetView(activity, decor, content, target, listener);
- decor.addView(tapTargetView, layoutParams);
-
- return tapTargetView;
- }
-
- public static TapTargetView showFor(Dialog dialog, TapTarget target) {
- return showFor(dialog, target, null);
- }
-
- public static TapTargetView showFor(Dialog dialog, TapTarget target, Listener listener) {
- if (dialog == null) throw new IllegalArgumentException("Dialog is null");
-
- final Context context = dialog.getContext();
- final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
- params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
- params.format = PixelFormat.RGBA_8888;
- params.flags = 0;
- params.gravity = Gravity.START | Gravity.TOP;
- params.x = 0;
- params.y = 0;
- params.width = WindowManager.LayoutParams.MATCH_PARENT;
- params.height = WindowManager.LayoutParams.MATCH_PARENT;
-
- final TapTargetView tapTargetView = new TapTargetView(context, windowManager, null, target, listener);
- windowManager.addView(tapTargetView, params);
-
- return tapTargetView;
- }
-
public static class Listener {
/** Signals that the user has clicked inside of the target **/
public void onTargetClick(TapTargetView view) {
@@ -234,22 +188,15 @@ public void onUpdate(float lerpTime) {
calculateDrawingBounds();
}
- final float targetAlpha = target.outerCircleAlpha * 255;
+ final float targetAlpha = target.getOuterCircleAlpha() * 255;
outerCircleRadius = newOuterCircleRadius;
outerCircleAlpha = (int) Math.min(targetAlpha, (lerpTime * 1.5f * targetAlpha));
outerCirclePath.reset();
outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW);
targetCircleAlpha = (int) Math.min(255.0f, (lerpTime * 1.5f * 255.0f));
-
- if (expanding) {
- targetCircleRadius = TARGET_RADIUS * Math.min(1.0f, lerpTime * 1.5f);
- } else {
- targetCircleRadius = TARGET_RADIUS * lerpTime;
- targetCirclePulseRadius *= lerpTime;
- }
-
- textAlpha = (int) (delayedLerp(lerpTime, 0.7f) * 255);
+ getTapType().expandContractChange(lerpTime, expanding);
+ textAlpha = (int) (ValueExtensions.getDelayLerp(lerpTime, 0.7f) * 255);
if (expanding) {
calculateDrawingBounds();
@@ -263,12 +210,7 @@ public void onUpdate(float lerpTime) {
.duration(250)
.delayBy(250)
.interpolator(new AccelerateDecelerateInterpolator())
- .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
- @Override
- public void onUpdate(float lerpTime) {
- expandContractUpdateListener.onUpdate(lerpTime);
- }
- })
+ .onUpdate(lerpTime -> expandContractUpdateListener.onUpdate(lerpTime))
.onEnd(new FloatValueAnimatorBuilder.EndListener() {
@Override
public void onEnd() {
@@ -285,15 +227,12 @@ public void onEnd() {
.onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
@Override
public void onUpdate(float lerpTime) {
- final float pulseLerp = delayedLerp(lerpTime, 0.5f);
- targetCirclePulseRadius = (1.0f + pulseLerp) * TARGET_RADIUS;
+ final float pulseLerp = ValueExtensions.getDelayLerp(lerpTime, 0.5f);
+ getTapType().pulseAnimation(lerpTime);
targetCirclePulseAlpha = (int) ((1.0f - pulseLerp) * 255);
- targetCircleRadius = TARGET_RADIUS + halfwayLerp(lerpTime) * TARGET_PULSE_RADIUS;
-
if (outerCircleRadius != calculatedOuterCircleRadius) {
outerCircleRadius = calculatedOuterCircleRadius;
}
-
calculateDrawingBounds();
invalidateViewAndOutline(drawingBounds);
}
@@ -303,18 +242,8 @@ public void onUpdate(float lerpTime) {
final ValueAnimator dismissAnimation = new FloatValueAnimatorBuilder(true)
.duration(250)
.interpolator(new AccelerateDecelerateInterpolator())
- .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
- @Override
- public void onUpdate(float lerpTime) {
- expandContractUpdateListener.onUpdate(lerpTime);
- }
- })
- .onEnd(new FloatValueAnimatorBuilder.EndListener() {
- @Override
- public void onEnd() {
- finishDismiss(true);
- }
- })
+ .onUpdate(lerpTime -> expandContractUpdateListener.onUpdate(lerpTime))
+ .onEnd(() -> finishDismiss(true))
.build();
private final ValueAnimator dismissConfirmAnimation = new FloatValueAnimatorBuilder()
@@ -325,24 +254,18 @@ public void onEnd() {
public void onUpdate(float lerpTime) {
final float spedUpLerp = Math.min(1.0f, lerpTime * 2.0f);
outerCircleRadius = calculatedOuterCircleRadius * (1.0f + (spedUpLerp * 0.2f));
- outerCircleAlpha = (int) ((1.0f - spedUpLerp) * target.outerCircleAlpha * 255.0f);
+ outerCircleAlpha = (int) ((1.0f - spedUpLerp) * target.getOuterCircleAlpha() * 255.0f);
outerCirclePath.reset();
outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW);
- targetCircleRadius = (1.0f - lerpTime) * TARGET_RADIUS;
+ getTapType().dismissConfirmAnimation(lerpTime);
targetCircleAlpha = (int) ((1.0f - lerpTime) * 255.0f);
- targetCirclePulseRadius = (1.0f + lerpTime) * TARGET_RADIUS;
targetCirclePulseAlpha = (int) ((1.0f - lerpTime) * targetCirclePulseAlpha);
textAlpha = (int) ((1.0f - spedUpLerp) * 255.0f);
calculateDrawingBounds();
invalidateViewAndOutline(drawingBounds);
}
})
- .onEnd(new FloatValueAnimatorBuilder.EndListener() {
- @Override
- public void onEnd() {
- finishDismiss(true);
- }
- })
+ .onEnd(() -> finishDismiss(true))
.build();
private ValueAnimator[] animators = new ValueAnimator[]
@@ -374,23 +297,22 @@ public TapTargetView(final Context context,
if (target == null) throw new IllegalArgumentException("Target cannot be null");
this.target = target;
+ getTapType().initResource(context);
this.parent = parent;
this.boundingParent = boundingParent;
this.listener = userListener != null ? userListener : new Listener();
- this.title = target.title;
- this.description = target.description;
-
- TARGET_PADDING = UiUtil.dp(context, 20);
- CIRCLE_PADDING = UiUtil.dp(context, 40);
- TARGET_RADIUS = UiUtil.dp(context, target.targetRadius);
- TEXT_PADDING = UiUtil.dp(context, 40);
- TEXT_SPACING = UiUtil.dp(context, 8);
- TEXT_MAX_WIDTH = UiUtil.dp(context, 360);
- TEXT_POSITIONING_BIAS = UiUtil.dp(context, 20);
- GUTTER_DIM = UiUtil.dp(context, 88);
- SHADOW_DIM = UiUtil.dp(context, 8);
- SHADOW_JITTER_DIM = UiUtil.dp(context, 1);
- TARGET_PULSE_RADIUS = (int) (0.1f * TARGET_RADIUS);
+ this.title = target.getTitle();
+ this.description = target.getDescription();
+
+ TARGET_PADDING = getTapType().getTargetPadding();
+ CIRCLE_PADDING = getTapType().getCirclePadding();
+ TEXT_PADDING = getTapType().getTextPadding();
+ TEXT_SPACING = getTapType().getTextSpacing();
+ TEXT_MAX_WIDTH = getTapType().getTextMaxWidth();
+ TEXT_POSITIONING_BIAS = getTapType().getTextPositionBias();
+ GUTTER_DIM = UiUtils.getDp( 88);
+ SHADOW_DIM = UiUtils.getDp( 8);
+ SHADOW_JITTER_DIM = UiUtils.getDp( 1);
outerCirclePath = new Path();
targetBounds = new Rect();
@@ -409,7 +331,7 @@ public TapTargetView(final Context context,
outerCirclePaint = new Paint();
outerCirclePaint.setAntiAlias(true);
- outerCirclePaint.setAlpha((int) (target.outerCircleAlpha * 255.0f));
+ outerCirclePaint.setAlpha((int) (target.getOuterCircleAlpha() * 255.0f));
outerCircleShadowPaint = new Paint();
outerCircleShadowPaint.setAntiAlias(true);
@@ -443,101 +365,88 @@ public TapTargetView(final Context context,
layoutNoLimits = false;
}
- globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- if (isDismissing) {
- return;
- }
- updateTextLayouts();
- target.onReady(new Runnable() {
- @Override
- public void run() {
- final int[] offset = new int[2];
-
- targetBounds.set(target.bounds());
-
- getLocationOnScreen(offset);
- targetBounds.offset(-offset[0], -offset[1]);
-
- if (boundingParent != null) {
- final WindowManager windowManager
- = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
- final DisplayMetrics displayMetrics = new DisplayMetrics();
- windowManager.getDefaultDisplay().getMetrics(displayMetrics);
-
- final Rect rect = new Rect();
- boundingParent.getWindowVisibleDisplayFrame(rect);
- int[] parentLocation = new int[2];
- boundingParent.getLocationInWindow(parentLocation);
-
- if (translucentStatusBar) {
- rect.top = parentLocation[1];
- }
- if (translucentNavigationBar) {
- rect.bottom = parentLocation[1] + boundingParent.getHeight();
- }
-
- // We bound the boundaries to be within the screen's coordinates to
- // handle the case where the flag FLAG_LAYOUT_NO_LIMITS is set
- if (layoutNoLimits) {
- topBoundary = Math.max(0, rect.top);
- bottomBoundary = Math.min(rect.bottom, displayMetrics.heightPixels);
- } else {
- topBoundary = rect.top;
- bottomBoundary = rect.bottom;
- }
- }
-
- drawTintedTarget();
- requestFocus();
- calculateDimensions();
-
- startExpandAnimation();
- }
- });
+ globalLayoutListener = () -> {
+ if (isDismissing) {
+ return;
}
+ updateTextLayouts();
+ target.onReady(() -> {
+ final int[] offset = new int[2];
+
+ targetBounds.set(target.bounds());
+
+ getLocationOnScreen(offset);
+ targetBounds.offset(-offset[0], -offset[1]);
+
+ if (boundingParent != null) {
+ final WindowManager windowManager
+ = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ final DisplayMetrics displayMetrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(displayMetrics);
+
+ final Rect rect = new Rect();
+ boundingParent.getWindowVisibleDisplayFrame(rect);
+ int[] parentLocation = new int[2];
+ boundingParent.getLocationInWindow(parentLocation);
+
+ if (translucentStatusBar) {
+ rect.top = parentLocation[1];
+ }
+ if (translucentNavigationBar) {
+ rect.bottom = parentLocation[1] + boundingParent.getHeight();
+ }
+
+ // We bound the boundaries to be within the screen's coordinates to
+ // handle the case where the flag FLAG_LAYOUT_NO_LIMITS is set
+ if (layoutNoLimits) {
+ topBoundary = Math.max(0, rect.top);
+ bottomBoundary = Math.min(rect.bottom, displayMetrics.heightPixels);
+ } else {
+ topBoundary = rect.top;
+ bottomBoundary = rect.bottom;
+ }
+ }
+
+ drawTintedTarget();
+ requestFocus();
+ calculateDimensions();
+
+ startExpandAnimation();
+ });
};
getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
setFocusableInTouchMode(true);
setClickable(true);
- setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- if (listener == null || outerCircleCenter == null || !isInteractable) return;
-
- final boolean clickedInTarget =
- distance(targetBounds.centerX(), targetBounds.centerY(), (int) lastTouchX, (int) lastTouchY) <= targetCircleRadius;
- final double distanceToOuterCircleCenter = distance(outerCircleCenter[0], outerCircleCenter[1],
- (int) lastTouchX, (int) lastTouchY);
- final boolean clickedInsideOfOuterCircle = distanceToOuterCircleCenter <= outerCircleRadius;
-
- if (clickedInTarget) {
- isInteractable = false;
- listener.onTargetClick(TapTargetView.this);
- } else if (clickedInsideOfOuterCircle) {
- listener.onOuterCircleClick(TapTargetView.this);
- } else if (cancelable) {
- isInteractable = false;
- listener.onTargetCancel(TapTargetView.this);
- }
+ setOnClickListener(v -> {
+ if (listener == null || outerCircleCenter == null || !isInteractable) return;
+
+ final boolean clickedInTarget = getTapType().clickInTarget(targetBounds, (int) lastTouchX, (int) lastTouchY);
+ final double distanceToOuterCircleCenter = distance(outerCircleCenter[0], outerCircleCenter[1],
+ (int) lastTouchX, (int) lastTouchY);
+ final boolean clickedInsideOfOuterCircle = distanceToOuterCircleCenter <= outerCircleRadius;
+
+ if (clickedInTarget) {
+ isInteractable = false;
+ listener.onTargetClick(TapTargetView.this);
+ } else if (clickedInsideOfOuterCircle) {
+ listener.onOuterCircleClick(TapTargetView.this);
+ } else if (cancelable) {
+ isInteractable = false;
+ listener.onTargetCancel(TapTargetView.this);
}
});
- setOnLongClickListener(new OnLongClickListener() {
- @Override
- public boolean onLongClick(View v) {
- if (listener == null) return false;
-
- if (targetBounds.contains((int) lastTouchX, (int) lastTouchY)) {
- listener.onTargetLongClick(TapTargetView.this);
- return true;
- }
+ setOnLongClickListener(v -> {
+ if (listener == null) return false;
- return false;
+ if (targetBounds.contains((int) lastTouchX, (int) lastTouchY)) {
+ listener.onTargetLongClick(TapTargetView.this);
+ return true;
}
+
+ return false;
});
}
@@ -550,13 +459,13 @@ private void startExpandAnimation() {
}
protected void applyTargetOptions(Context context) {
- shouldTintTarget = !target.transparentTarget && target.tintTarget;
- shouldDrawShadow = target.drawShadow;
- cancelable = target.cancelable;
+ shouldTintTarget = !target.getTransparentTarget() && target.getTintTarget();
+ shouldDrawShadow = target.getDrawShadow();
+ cancelable = target.getCancelable();
// We can't clip out portions of a view outline, so if the user specified a transparent
// target, we need to fallback to drawing a jittered shadow approximation
- if (shouldDrawShadow && Build.VERSION.SDK_INT >= 21 && !target.transparentTarget) {
+ if (shouldDrawShadow && Build.VERSION.SDK_INT >= 21 && !target.getTransparentTarget()) {
outlineProvider = new ViewOutlineProvider() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
@@ -583,13 +492,13 @@ public void getOutline(View view, Outline outline) {
}
final Resources.Theme theme = context.getTheme();
- isDark = UiUtil.themeIntAttr(context, "isLightTheme") == 0;
+ isDark = UiUtils.getThemeIneAttr(context, "isLightTheme") == 0;
final Integer outerCircleColor = target.outerCircleColorInt(context);
if (outerCircleColor != null) {
outerCirclePaint.setColor(outerCircleColor);
} else if (theme != null) {
- outerCirclePaint.setColor(UiUtil.themeIntAttr(context, "colorPrimary"));
+ outerCirclePaint.setColor(UiUtils.getThemeIneAttr(context, "colorPrimary"));
} else {
outerCirclePaint.setColor(Color.WHITE);
}
@@ -601,7 +510,7 @@ public void getOutline(View view, Outline outline) {
targetCirclePaint.setColor(isDark ? Color.BLACK : Color.WHITE);
}
- if (target.transparentTarget) {
+ if (target.getTransparentTarget()) {
targetCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
}
@@ -609,7 +518,7 @@ public void getOutline(View view, Outline outline) {
final Integer targetDimColor = target.dimColorInt(context);
if (targetDimColor != null) {
- dimColor = UiUtil.setAlpha(targetDimColor, 0.3f);
+ dimColor = UiUtils.setAlpha(targetDimColor, 0.3f);
} else {
dimColor = -1;
}
@@ -628,12 +537,12 @@ public void getOutline(View view, Outline outline) {
descriptionPaint.setColor(titlePaint.getColor());
}
- if (target.titleTypeface != null) {
- titlePaint.setTypeface(target.titleTypeface);
+ if (target.getTitleTypeface() != null) {
+ titlePaint.setTypeface(target.getTitleTypeface());
}
- if (target.descriptionTypeface != null) {
- descriptionPaint.setTypeface(target.descriptionTypeface);
+ if (target.getDescriptionTypeface() != null) {
+ descriptionPaint.setTypeface(target.getTitleTypeface());
}
}
@@ -654,7 +563,7 @@ void onDismiss(boolean userInitiated) {
animator.removeAllUpdateListeners();
}
- ViewUtil.removeOnGlobalLayoutListener(getViewTreeObserver(), globalLayoutListener);
+ getViewTreeObserver().removeOnGlobalLayoutListener(globalLayoutListener);
visible = false;
if (listener != null) {
@@ -689,12 +598,9 @@ protected void onDraw(Canvas c) {
targetCirclePaint.setAlpha(targetCircleAlpha);
if (targetCirclePulseAlpha > 0) {
targetCirclePulsePaint.setAlpha(targetCirclePulseAlpha);
- c.drawCircle(targetBounds.centerX(), targetBounds.centerY(),
- targetCirclePulseRadius, targetCirclePulsePaint);
+ getTapType().drawPulse(c, targetCirclePulseAlpha, targetBounds, targetCirclePulsePaint);
}
- c.drawCircle(targetBounds.centerX(), targetBounds.centerY(),
- targetCircleRadius, targetCirclePaint);
-
+ getTapType().drawTarget(c, targetBounds, targetCirclePaint);
saveCount = c.save();
{
c.translate(textBounds.left, textBounds.top);
@@ -705,7 +611,7 @@ protected void onDraw(Canvas c) {
if (descriptionLayout != null && titleLayout != null) {
c.translate(0, titleLayout.getHeight() + TEXT_SPACING);
- descriptionPaint.setAlpha((int) (target.descriptionTextAlpha * textAlpha));
+ descriptionPaint.setAlpha((int) (target.getDescriptionTextAlpha() * textAlpha));
descriptionLayout.draw(c);
}
}
@@ -717,11 +623,11 @@ protected void onDraw(Canvas c) {
c.translate(targetBounds.centerX() - tintedTarget.getWidth() / 2,
targetBounds.centerY() - tintedTarget.getHeight() / 2);
c.drawBitmap(tintedTarget, 0, 0, targetCirclePaint);
- } else if (target.icon != null) {
- c.translate(targetBounds.centerX() - target.icon.getBounds().width() / 2,
- targetBounds.centerY() - target.icon.getBounds().height() / 2);
- target.icon.setAlpha(targetCirclePaint.getAlpha());
- target.icon.draw(c);
+ } else if (target.getIcon() != null) {
+ c.translate(targetBounds.centerX() - target.getIcon().getBounds().width() / 2,
+ targetBounds.centerY() - target.getIcon().getBounds().height() / 2);
+ target.getIcon().setAlpha(targetCirclePaint.getAlpha());
+ target.getIcon().draw(c);
}
}
c.restoreToCount(saveCount);
@@ -788,7 +694,7 @@ public void dismiss(boolean tappedTarget) {
private void finishDismiss(boolean userInitiated) {
onDismiss(userInitiated);
- ViewUtil.removeView(parent, TapTargetView.this);
+ parent.removeView(TapTargetView.this);
}
/** Specify whether to draw a wireframe around the view, useful for debugging **/
@@ -823,13 +729,13 @@ void drawDebugInformation(Canvas c) {
debugPaint = new Paint();
debugPaint.setARGB(255, 255, 0, 0);
debugPaint.setStyle(Paint.Style.STROKE);
- debugPaint.setStrokeWidth(UiUtil.dp(getContext(), 1));
+ debugPaint.setStrokeWidth(UiUtils.getDp(1));
}
if (debugTextPaint == null) {
debugTextPaint = new TextPaint();
debugTextPaint.setColor(0xFFFF0000);
- debugTextPaint.setTextSize(UiUtil.sp(getContext(), 16));
+ debugTextPaint.setTextSize(UiUtils.getSp(16));
}
// Draw wireframe
@@ -838,7 +744,7 @@ void drawDebugInformation(Canvas c) {
c.drawRect(targetBounds, debugPaint);
c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], 10, debugPaint);
c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], calculatedOuterCircleRadius - CIRCLE_PADDING, debugPaint);
- c.drawCircle(targetBounds.centerX(), targetBounds.centerY(), TARGET_RADIUS + TARGET_PADDING, debugPaint);
+ getTapType().drawInformation(c, targetBounds, targetCirclePaint);
// Draw positions and dimensions
debugPaint.setStyle(Paint.Style.FILL);
@@ -872,7 +778,7 @@ void drawDebugInformation(Canvas c) {
}
void drawTintedTarget() {
- final Drawable icon = target.icon;
+ final Drawable icon = target.getIcon();
if (!shouldTintTarget || icon == null) {
tintedTarget = null;
return;
@@ -906,22 +812,6 @@ void updateTextLayouts() {
}
}
- float halfwayLerp(float lerp) {
- if (lerp < 0.5f) {
- return lerp / 0.5f;
- }
-
- return (1.0f - lerp) / 0.5f;
- }
-
- float delayedLerp(float lerp, float threshold) {
- if (lerp < threshold) {
- return 0.0f;
- }
-
- return (lerp - threshold) / (1.0f - threshold);
- }
-
void calculateDimensions() {
textBounds = getTextBounds();
outerCircleCenter = getOuterCircleCenterPoint();
@@ -945,7 +835,7 @@ void calculateDrawingBounds() {
int getOuterCircleRadius(int centerX, int centerY, Rect textBounds, Rect targetBounds) {
final int targetCenterX = targetBounds.centerX();
final int targetCenterY = targetBounds.centerY();
- final int expandedRadius = (int) (1.1f * TARGET_RADIUS);
+ final int expandedRadius = (int) (1.1f * getTapType().getEdgeLength());
final Rect expandedBounds = new Rect(targetCenterX, targetCenterY, targetCenterX, targetCenterY);
expandedBounds.inset(-expandedRadius, -expandedRadius);
@@ -957,20 +847,7 @@ int getOuterCircleRadius(int centerX, int centerY, Rect textBounds, Rect targetB
Rect getTextBounds() {
final int totalTextHeight = getTotalTextHeight();
final int totalTextWidth = getTotalTextWidth();
-
- final int possibleTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight;
- final int top;
- if (possibleTop > topBoundary) {
- top = possibleTop;
- } else {
- top = targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING;
- }
-
- final int relativeCenterDistance = (getWidth() / 2) - targetBounds.centerX();
- final int bias = relativeCenterDistance < 0 ? -TEXT_POSITIONING_BIAS : TEXT_POSITIONING_BIAS;
- final int left = Math.max(TEXT_PADDING, targetBounds.centerX() - bias - totalTextWidth);
- final int right = Math.min(getWidth() - TEXT_PADDING, left + totalTextWidth);
- return new Rect(left, top, right, top + totalTextHeight);
+ return getTapType().getTextBounds(totalTextHeight, totalTextWidth, targetBounds, topBoundary, getWidth());
}
int[] getOuterCircleCenterPoint() {
@@ -978,18 +855,20 @@ int[] getOuterCircleCenterPoint() {
return new int[]{targetBounds.centerX(), targetBounds.centerY()};
}
+ int edgeLength = getTapType().getEdgeLength();
+
final int targetRadius = Math.max(targetBounds.width(), targetBounds.height()) / 2 + TARGET_PADDING;
final int totalTextHeight = getTotalTextHeight();
- final boolean onTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight > 0;
+ final boolean onTop = targetBounds.centerY() - edgeLength - TARGET_PADDING - totalTextHeight > 0;
final int left = Math.min(textBounds.left, targetBounds.left - targetRadius);
final int right = Math.max(textBounds.right, targetBounds.right + targetRadius);
final int titleHeight = titleLayout == null ? 0 : titleLayout.getHeight();
final int centerY = onTop ?
- targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight + titleHeight
+ targetBounds.centerY() - edgeLength - TARGET_PADDING - totalTextHeight + titleHeight
:
- targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING + titleHeight;
+ targetBounds.centerY() + edgeLength + TARGET_PADDING + titleHeight;
return new int[] { (left + right) / 2, centerY };
}
@@ -1044,4 +923,8 @@ void invalidateViewAndOutline(Rect bounds) {
invalidateOutline();
}
}
+
+ TapTargetShapeType getTapType() {
+ return target.getTapTargetType$taptargetview_debug();
+ }
}
diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TargetExtensions.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TargetExtensions.kt
new file mode 100644
index 0000000..e23c650
--- /dev/null
+++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TargetExtensions.kt
@@ -0,0 +1,90 @@
+@file:Suppress("unused")
+package com.getkeepsafe.taptargetview
+
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.view.View
+import androidx.annotation.IdRes
+import androidx.appcompat.widget.Toolbar
+import com.getkeepsafe.taptargetview.target.TapTarget
+
+/** Return a tap target for the overflow button from the given toolbar
+ *
+ *
+ * **Note:** This is currently experimental, use at your own risk
+ */
+@JvmOverloads
+fun Toolbar?.createOverflow(
+ title: CharSequence?,
+ description: CharSequence? = null
+): TapTarget {
+ return ToolbarTapTarget(this, false, title, description)
+}
+/** Return a tap target for the overflow button from the given toolbar
+ *
+ *
+ * **Note:** This is currently experimental, use at your own risk
+ */
+@JvmOverloads
+fun android.widget.Toolbar?.createOverflow(
+ title: CharSequence?,
+ description: CharSequence? = null
+): TapTarget {
+ return ToolbarTapTarget(this, false, title, description)
+}
+/** Return a tap target for the navigation button (back, up, etc) from the given toolbar */
+@JvmOverloads
+fun Toolbar?.createNavigationIcon(
+ title: CharSequence?,
+ description: CharSequence? = null
+): TapTarget {
+ return ToolbarTapTarget(this, true, title, description)
+}
+/** Return a tap target for the navigation button (back, up, etc) from the given toolbar */
+@JvmOverloads
+fun android.widget.Toolbar?.createNavigationIcon(
+ title: CharSequence?,
+ description: CharSequence? = null
+): TapTarget {
+ return ToolbarTapTarget(this, true, title, description)
+}
+/** Return a tap target for the menu item from the given toolbar */
+@JvmOverloads
+fun Toolbar?.forToolbarMenuItem(
+ @IdRes menuItemId: Int,
+ title: CharSequence?, description: CharSequence? = null
+): TapTarget {
+ return ToolbarTapTarget(this, menuItemId, title, description)
+}
+/** Return a tap target for the menu item from the given toolbar */
+/** Return a tap target for the menu item from the given toolbar */
+@JvmOverloads
+fun android.widget.Toolbar?.forToolbarMenuItem(
+ @IdRes menuItemId: Int,
+ title: CharSequence?, description: CharSequence? = null
+): TapTarget {
+ return ToolbarTapTarget(this, menuItemId, title, description)
+}
+/** Return a tap target for the specified view */
+@JvmOverloads
+fun View?.createTarget(
+ title: CharSequence,
+ description: CharSequence? = null
+): TapTarget {
+ requireNotNull(this) {
+ "Cannot create tap target with null"
+ }
+ return TapTarget(this, title, description)
+}
+/** Return a tap target for the specified bounds */
+@JvmOverloads
+fun Drawable?.createTarget(
+ bounds: Rect?,
+ title: CharSequence?,
+ description: CharSequence? = null
+): TapTarget {
+ requireNotNull(this) {
+ "Cannot create tap target with null"
+ }
+ return TapTarget(this, title, description, bounds)
+}
\ No newline at end of file
diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TargetViewExtensions.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TargetViewExtensions.kt
new file mode 100644
index 0000000..a6dd51f
--- /dev/null
+++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TargetViewExtensions.kt
@@ -0,0 +1,49 @@
+@file:JvmName("TargetViewExtensionsKTX")
+package com.getkeepsafe.taptargetview
+
+import android.app.Activity
+import android.app.Dialog
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.Gravity
+import android.view.ViewGroup
+import android.view.WindowManager
+import com.getkeepsafe.taptargetview.target.TapTarget
+
+@JvmName("showGuideView")
+fun Activity?.showGuideView(
+ target: TapTarget,
+ listener: TapTargetView.Listener? = null
+): TapTargetView {
+ if (this == null) throw IllegalArgumentException("Activity is null")
+ val decorView = this.window.decorView as? ViewGroup
+ ?: throw IllegalArgumentException("Activity has no decorView")
+ val layoutParams = ViewGroup.LayoutParams(-1, -1)
+ val content = decorView.findViewById
- * 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.getkeepsafe.taptargetview;
-
-import android.content.Context;
-import android.content.res.Resources;
-
-import androidx.annotation.ColorRes;
-import androidx.annotation.DimenRes;
-import android.util.TypedValue;
-
-class UiUtil {
- UiUtil() {
- }
-
- /** Returns the given pixel value in dp **/
- static int dp(Context context, int val) {
- return (int) TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP, val, context.getResources().getDisplayMetrics());
- }
-
- /** Returns the given pixel value in sp **/
- static int sp(Context context, int val) {
- return (int) TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP, val, context.getResources().getDisplayMetrics());
- }
-
- /** Returns the value of the desired theme integer attribute, or -1 if not found **/
- static int themeIntAttr(Context context, String attr) {
- final Resources.Theme theme = context.getTheme();
- if (theme == null) {
- return -1;
- }
-
- final TypedValue value = new TypedValue();
- final int id = context.getResources().getIdentifier(attr, "attr", context.getPackageName());
-
- if (id == 0) {
- // Not found
- return -1;
- }
-
- theme.resolveAttribute(id, value, true);
- return value.data;
- }
-
- /** Modifies the alpha value of the given ARGB color **/
- static int setAlpha(int argb, float alpha) {
- if (alpha > 1.0f) {
- alpha = 1.0f;
- } else if (alpha <= 0.0f) {
- alpha = 0.0f;
- }
-
- return ((int) ((argb >>> 24) * alpha) << 24) | (argb & 0x00FFFFFF);
- }
-}
diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/UiUtils.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/UiUtils.kt
new file mode 100644
index 0000000..c392bec
--- /dev/null
+++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/UiUtils.kt
@@ -0,0 +1,35 @@
+@file:JvmName("UiUtils")
+@file:Suppress("unused")
+package com.getkeepsafe.taptargetview
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.Resources
+import android.util.TypedValue
+import androidx.annotation.FloatRange
+
+internal val Int.dp get(): Int = (this * Resources.getSystem().displayMetrics.density).toInt()
+
+internal val Int.px get(): Int = (this / Resources.getSystem().displayMetrics.density).toInt()
+
+internal val Int.sp get(): Int = (this * Resources.getSystem().displayMetrics.scaledDensity).toInt()
+
+internal val Int.px2dp get(): Int = (this / Resources.getSystem().displayMetrics.density).toInt()
+
+@SuppressLint("DiscouragedApi")
+internal fun Context.getThemeIneAttr(
+ attr: String
+): Int {
+ val theme = theme ?: return -1
+ val typedValue = TypedValue()
+ val id = resources.getIdentifier(attr, "attr", packageName)
+ if (id == 0) return -1
+ theme.resolveAttribute(id, typedValue, true)
+ return typedValue.data
+}
+
+fun Int.setAlpha(
+ @FloatRange(0.0, 1.0) alpha: Float
+): Int {
+ return ((this ushr 24) * alpha).toInt() shl 24 or (this and 0x00FFFFFF)
+}
\ No newline at end of file
diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ValueUtils.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ValueUtils.kt
new file mode 100644
index 0000000..62ccd0f
--- /dev/null
+++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ValueUtils.kt
@@ -0,0 +1,14 @@
+@file:JvmName("ValueExtensions")
+package com.getkeepsafe.taptargetview
+
+
+internal val Float.halfwayLerp: Float
+ get() {
+ if (this < 0.5f) return this / 0.5f
+ return (1.0f - this) / 0.5f
+ }
+
+internal fun Float.getDelayLerp(threshold: Float): Float {
+ if (this < threshold) return 0f
+ return (this - threshold) / (1.0f - threshold)
+}
\ No newline at end of file
diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewTapTarget.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewTapTarget.java
deleted file mode 100644
index 045ae58..0000000
--- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewTapTarget.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Copyright 2016 Keepsafe Software, 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.getkeepsafe.taptargetview;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import androidx.annotation.Nullable;
-import android.view.View;
-
-class ViewTapTarget extends TapTarget {
- final View view;
-
- ViewTapTarget(View view, CharSequence title, @Nullable CharSequence description) {
- super(title, description);
- if (view == null) {
- throw new IllegalArgumentException("Given null view to target");
- }
- this.view = view;
- }
-
- @Override
- public void onReady(final Runnable runnable) {
- ViewUtil.onLaidOut(view, new Runnable() {
- @Override
- public void run() {
- // Cache bounds
- final int[] location = new int[2];
- view.getLocationOnScreen(location);
- bounds = new Rect(location[0], location[1],
- location[0] + view.getWidth(), location[1] + view.getHeight());
-
- if (icon == null && view.getWidth() > 0 && view.getHeight() > 0) {
- final Bitmap viewBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(viewBitmap);
- view.draw(canvas);
- icon = new BitmapDrawable(view.getContext().getResources(), viewBitmap);
- icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
- }
-
- runnable.run();
- }
- });
- }
-}
diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewUtil.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewUtil.java
deleted file mode 100644
index 44c9a9a..0000000
--- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewUtil.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * Copyright 2016 Keepsafe Software, 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.getkeepsafe.taptargetview;
-
-import android.os.Build;
-import androidx.core.view.ViewCompat;
-import android.view.View;
-import android.view.ViewManager;
-import android.view.ViewTreeObserver;
-
-class ViewUtil {
- ViewUtil() {
- }
-
- /** Returns whether or not the view has been laid out **/
- private static boolean isLaidOut(View view) {
- return ViewCompat.isLaidOut(view) && view.getWidth() > 0 && view.getHeight() > 0;
- }
-
- /** Executes the given {@link java.lang.Runnable} when the view is laid out **/
- static void onLaidOut(final View view, final Runnable runnable) {
- if (isLaidOut(view)) {
- runnable.run();
- return;
- }
-
- final ViewTreeObserver observer = view.getViewTreeObserver();
- observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
- @Override
- public void onGlobalLayout() {
- final ViewTreeObserver trueObserver;
-
- if (observer.isAlive()) {
- trueObserver = observer;
- } else {
- trueObserver = view.getViewTreeObserver();
- }
-
- removeOnGlobalLayoutListener(trueObserver, this);
-
- runnable.run();
- }
- });
- }
-
- @SuppressWarnings("deprecation")
- static void removeOnGlobalLayoutListener(ViewTreeObserver observer,
- ViewTreeObserver.OnGlobalLayoutListener listener) {
- if (Build.VERSION.SDK_INT >= 16) {
- observer.removeOnGlobalLayoutListener(listener);
- } else {
- observer.removeGlobalOnLayoutListener(listener);
- }
- }
-
- static void removeView(ViewManager parent, View child) {
- if (parent == null || child == null) {
- return;
- }
-
- try {
- parent.removeView(child);
- } catch (Exception ignored) {
- // This catch exists for modified versions of Android that have a buggy ViewGroup
- // implementation. See b.android.com/77639, #121 and #49
- }
- }
-}
diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/CircleShapeTapTarget.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/CircleShapeTapTarget.kt
new file mode 100644
index 0000000..5a557e2
--- /dev/null
+++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/CircleShapeTapTarget.kt
@@ -0,0 +1,100 @@
+package com.getkeepsafe.taptargetview.target
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import com.getkeepsafe.taptargetview.dp
+import com.getkeepsafe.taptargetview.getDelayLerp
+import com.getkeepsafe.taptargetview.halfwayLerp
+import kotlin.math.pow
+import kotlin.math.roundToInt
+
+class CircleShapeTapTarget: TapTargetShapeType() {
+
+ private var targetRadius = 44
+
+ private var targetCircleRadius = 0f
+
+ private var targetCirclePulseRadius = 0f
+
+ private var TARGET_RADIUS = 0
+
+ private var TARGET_PULSE_RADIUS = 0
+
+
+ /**
+ * 用于测量文本,圆心等位置的
+ */
+ override val edgeLength: Int get() = TARGET_RADIUS
+
+ /** Specify the target radius in dp. */
+ fun setTargetRadius(targetRadius: Int): CircleShapeTapTarget {
+ this.targetRadius = targetRadius
+ return this
+ }
+
+ override fun initResource(context: Context) {
+ TARGET_RADIUS = targetRadius.dp
+ TARGET_PULSE_RADIUS = (TARGET_RADIUS * 0.1f).roundToInt()
+ }
+
+ override fun expandContractChange(lerpTime: Float, isExpanding: Boolean) {
+ if (isExpanding) {
+ targetCircleRadius = TARGET_RADIUS * 1.0f.coerceAtMost(lerpTime * 1.5f)
+ } else {
+ targetCircleRadius = TARGET_RADIUS * lerpTime
+ targetCirclePulseRadius *= lerpTime
+ }
+ }
+
+ override fun pulseAnimation(lerpTime: Float) {
+ targetCirclePulseRadius = (1.0f + lerpTime.getDelayLerp(0.5f)) * TARGET_RADIUS
+ targetCircleRadius = TARGET_RADIUS + lerpTime.halfwayLerp * TARGET_PULSE_RADIUS
+ }
+
+ override fun dismissConfirmAnimation(lerpTime: Float) {
+ targetCircleRadius = (1.0f - lerpTime) * TARGET_RADIUS
+ targetCirclePulseRadius = (1.0f + lerpTime) * TARGET_RADIUS
+ }
+
+ override fun drawTarget(
+ canvas: Canvas,
+ targetBounds: Rect,
+ paint: Paint
+ ) {
+ canvas.drawCircle(
+ targetBounds.centerX().toFloat(), targetBounds.centerY().toFloat(),
+ targetCircleRadius, paint
+ )
+ }
+
+ override fun drawPulse(
+ canvas: Canvas,
+ targetPulseAlpha: Float,
+ targetBounds: Rect,
+ paint: Paint
+ ) {
+ if (targetPulseAlpha < 0) return
+ canvas.drawCircle(
+ targetBounds.centerX().toFloat(), targetBounds.centerY().toFloat(),
+ targetCirclePulseRadius, paint
+ )
+ }
+
+ override fun drawInformation(canvas: Canvas, targetBounds: Rect, paint: Paint) {
+ canvas.drawCircle(
+ targetBounds.centerX().toFloat(),
+ targetBounds.centerY().toFloat(),
+ TARGET_RADIUS + 20.dp.toFloat(),
+ paint
+ )
+ }
+
+ override fun clickInTarget(targetBounds: Rect, lastTouchX: Int, lastTouchY: Int): Boolean {
+ val xPow = (lastTouchX - targetBounds.centerX()).toDouble().pow(2.0)
+ val yPow = (lastTouchY - targetBounds.centerY()).toDouble().pow(2.0)
+ val sqrt = (xPow + yPow).pow(0.5)
+ return sqrt <= targetCircleRadius
+ }
+}
\ No newline at end of file
diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/RectAngleShapeType.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/RectAngleShapeType.kt
new file mode 100644
index 0000000..d20866b
--- /dev/null
+++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/RectAngleShapeType.kt
@@ -0,0 +1,126 @@
+@file:Suppress("unused")
+package com.getkeepsafe.taptargetview.target
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.RectF
+import com.getkeepsafe.taptargetview.dp
+import com.getkeepsafe.taptargetview.getDelayLerp
+import com.getkeepsafe.taptargetview.halfwayLerp
+
+class RectAngleShapeType : TapTargetShapeType() {
+
+ private var width = 0
+
+ private var height = 0
+
+ private var drawWidth = 0f
+
+ private var drawHeight = 0f
+
+ private var pulseLength = 4.dp
+
+ private var drawPulseWidth= 0f
+
+ private var drawPulseHeight = 0f
+
+ internal var roundRadius = 8.dp
+
+ override val edgeLength: Int
+ get() = 8.dp
+
+ override fun onReadyTarget(bounds: Rect?) {
+ checkNotNull(bounds)
+ this.width = bounds.width()
+ this.height = bounds.height()
+ }
+
+ override fun expandContractChange(lerpTime: Float, isExpanding: Boolean) {
+ if (isExpanding) {
+ drawHeight = height * 1.0f.coerceAtMost(lerpTime * 1.5f)
+ drawWidth = width * 1.0f.coerceAtMost(lerpTime * 1.5f)
+ } else {
+ drawHeight = height * lerpTime
+ drawWidth = width * lerpTime
+ drawPulseWidth *= lerpTime
+ drawPulseHeight *= lerpTime
+ }
+ }
+
+ override fun dismissConfirmAnimation(lerpTime: Float) {
+ drawHeight = height * (1.0f - lerpTime)
+ drawWidth = width * (1.0f - lerpTime)
+ drawPulseWidth = (1.0f + lerpTime) * width
+ drawPulseHeight = (1.0f + lerpTime) * height
+ }
+
+ override fun pulseAnimation(lerpTime: Float) {
+ drawWidth = width + lerpTime.halfwayLerp * pulseLength
+ drawHeight = height + lerpTime.halfwayLerp * pulseLength
+ drawPulseHeight = (1.0f + lerpTime.getDelayLerp(0.5f)) * height
+ drawPulseWidth = (1.0f + lerpTime.getDelayLerp(0.5f)) * width
+ }
+
+ override fun drawTarget(canvas: Canvas, targetBounds: Rect, paint: Paint) {
+ canvas.drawRoundRect(
+ targetBounds.toTargetRectF(
+ drawWidth, drawHeight
+ ),
+ roundRadius.toFloat(),
+ roundRadius.toFloat(),
+ paint
+ )
+ }
+
+ private fun Rect.toTargetRectF(
+ width: Float,
+ height: Float
+ ): RectF {
+ val centerX = centerX()
+ val centerY = centerY()
+ val right = width * 0.5f + roundRadius / 2
+ val bottom = height * 0.5f + roundRadius / 2
+ return RectF(
+ centerX - right,
+ centerY - bottom,
+ centerX + right,
+ centerY + bottom
+ )
+ }
+
+ override fun drawPulse(
+ canvas: Canvas,
+ targetPulseAlpha: Float,
+ targetBounds: Rect,
+ paint: Paint
+ ) {
+ if (targetPulseAlpha < 0) return
+ canvas.drawRoundRect(
+ targetBounds.toTargetRectF(
+ drawPulseWidth, drawPulseHeight
+ ),
+ roundRadius.toFloat(),
+ roundRadius.toFloat(),
+ paint
+ )
+ }
+
+ override fun getTextVertical(
+ targetBounds: Rect,
+ totalTextHeight: Int,
+ topBoundary: Int
+ ): Pair