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 @@

Video 1 Screenshot 1 -Screenshot 2
+Screenshot 2 +Screenshot 2
TapTargetView

@@ -12,7 +13,7 @@ An implementation of tap targets from [Google's Material Design guidelines on feature discovery](https://material.io/archive/guidelines/growth-communications/feature-discovery.html). - **Min SDK:** 14 + **Min SDK:** 21 [JavaDoc](https://javadoc.jitpack.io/com/github/KeepSafe/TapTargetView/latest/javadoc/) @@ -20,15 +21,7 @@ An implementation of tap targets from [Google's Material Design guidelines on fe TapTargetView is distributed using [MavenCentral](https://search.maven.org/artifact/com.getkeepsafe.taptargetview/taptargetview). -```groovy - repositories { - mavenCentral() - } - - dependencies { - implementation 'com.getkeepsafe.taptargetview:taptargetview:x.x.x' - } -``` +## No Publish If you wish, you may also use TapTargetView with [jitpack](https://jitpack.io/#KeepSafe/TapTargetView). For snapshots, please follow the instructions [here](https://jitpack.io/#KeepSafe/TapTargetView/-SNAPSHOT). @@ -37,33 +30,28 @@ For snapshots, please follow the instructions [here](https://jitpack.io/#KeepSaf ### Simple usage +```kotlin + Activity.showGuideView( + view.createTarget("Please Input Some Thing") + .outerCircleColor(R.color.colorAccent) + .targetIconColor(android.R.color.holo_blue_dark) + .transparentTarget(true) + .textColor(android.R.color.black) + .setTargetShapeType(TapTargetShapeType.RectAngle(16)) + ) +``` + ```java -TapTargetView.showFor(this, // `this` is an Activity - TapTarget.forView(findViewById(R.id.target), "This is a target", "We have the best targets, believe me") - // All options below are optional - .outerCircleColor(R.color.red) // Specify a color for the outer circle - .outerCircleAlpha(0.96f) // Specify the alpha amount for the outer circle - .targetCircleColor(R.color.white) // Specify a color for the target circle - .titleTextSize(20) // Specify the size (in sp) of the title text - .titleTextColor(R.color.white) // Specify the color of the title text - .descriptionTextSize(10) // Specify the size (in sp) of the description text - .descriptionTextColor(R.color.red) // Specify the color of the description text - .textColor(R.color.blue) // Specify a color for both the title and description text - .textTypeface(Typeface.SANS_SERIF) // Specify a typeface for the text - .dimColor(R.color.black) // If set, will dim behind the view with 30% opacity of the given color - .drawShadow(true) // Whether to draw a drop shadow or not - .cancelable(false) // Whether tapping outside the outer circle dismisses the view - .tintTarget(true) // Whether to tint the target view's color - .transparentTarget(false) // Specify whether the target is transparent (displays the content underneath) - .icon(Drawable) // Specify a custom drawable to draw as the target - .targetRadius(60), // Specify the target radius (in dp) - new TapTargetView.Listener() { // The listener can listen for regular clicks, long clicks or cancels - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); // This call is optional - doSomething(); - } - }); + TargetViewExtensionsKTX.showGuideView( + Activity, + view.createTarget("Please Input Some Thing") + .outerCircleColor(R.color.colorAccent) + .targetIconColor(android.R.color.holo_blue_dark) + .transparentTarget(true) + .textColor(android.R.color.black) + .setTargetShapeType(TapTargetShapeType.RectAngle(16)), + null + ); ``` You may also choose to target your own custom `Rect` with `TapTarget.forBounds(Rect, ...)` @@ -76,9 +64,9 @@ Additionally, each color can be specified via a `@ColorRes` or a `@ColorInt`. Fu You can easily create a sequence of tap targets with `TapTargetSequence`: -```java +```kotlin new TapTargetSequence(this) - .targets( + .addTarget( TapTarget.forView(findViewById(R.id.never), "Gonna"), TapTarget.forView(findViewById(R.id.give), "You", "Up") .dimColor(android.R.color.never) diff --git a/app/build.gradle b/app/build.gradle index 98f2293..084bdf9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion defCompileSdkVersion @@ -16,11 +17,13 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { implementation project(':taptargetview') - implementation "androidx.appcompat:appcompat:$defAppCompatVersion" implementation "com.google.android.material:material:$defMaterialVersion" - implementation 'com.facebook.stetho:stetho:1.5.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index da29f72..9d11093 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,9 +7,9 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/AppTheme" - android:name=".SampleApplication"> - + android:theme="@style/AppTheme"> + diff --git a/app/src/main/java/com/getkeepsafe/taptargetviewsample/MainActivity.java b/app/src/main/java/com/getkeepsafe/taptargetviewsample/MainActivity.java deleted file mode 100644 index 77e4df6..0000000 --- a/app/src/main/java/com/getkeepsafe/taptargetviewsample/MainActivity.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.getkeepsafe.taptargetviewsample; - -import android.content.DialogInterface; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import androidx.core.content.ContextCompat; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.Toolbar; -import android.text.SpannableString; -import android.text.style.StyleSpan; -import android.text.style.UnderlineSpan; -import android.util.Log; -import android.view.Display; -import android.widget.TextView; -import android.widget.Toast; - -import com.getkeepsafe.taptargetview.TapTarget; -import com.getkeepsafe.taptargetview.TapTargetSequence; -import com.getkeepsafe.taptargetview.TapTargetView; - -public class MainActivity extends AppCompatActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - toolbar.inflateMenu(R.menu.menu_main); - toolbar.setNavigationIcon(ContextCompat.getDrawable(this, R.drawable.ic_arrow_back_white_24dp)); - - // We load a drawable and create a location to show a tap target here - // We need the display to get the width and height at this point in time - final Display display = getWindowManager().getDefaultDisplay(); - // Load our little droid guy - final Drawable droid = ContextCompat.getDrawable(this, R.drawable.ic_android_black_24dp); - // Tell our droid buddy where we want him to appear - final Rect droidTarget = new Rect(0, 0, droid.getIntrinsicWidth() * 2, droid.getIntrinsicHeight() * 2); - // Using deprecated methods makes you look way cool - droidTarget.offset(display.getWidth() / 2, display.getHeight() / 2); - - final SpannableString sassyDesc = new SpannableString("It allows you to go back, sometimes"); - sassyDesc.setSpan(new StyleSpan(Typeface.ITALIC), sassyDesc.length() - "sometimes".length(), sassyDesc.length(), 0); - - // We have a sequence of targets, so lets build it! - final TapTargetSequence sequence = new TapTargetSequence(this) - .targets( - // This tap target will target the back button, we just need to pass its containing toolbar - TapTarget.forToolbarNavigationIcon(toolbar, "This is the back button", sassyDesc).id(1), - // Likewise, this tap target will target the search button - TapTarget.forToolbarMenuItem(toolbar, R.id.search, "This is a search icon", "As you can see, it has gotten pretty dark around here...") - .dimColor(android.R.color.black) - .outerCircleColor(R.color.colorAccent) - .targetCircleColor(android.R.color.black) - .transparentTarget(true) - .textColor(android.R.color.black) - .id(2), - // You can also target the overflow button in your toolbar - TapTarget.forToolbarOverflow(toolbar, "This will show more options", "But they're not useful :(").id(3), - // This tap target will target our droid buddy at the given target rect - TapTarget.forBounds(droidTarget, "Oh look!", "You can point to any part of the screen. You also can't cancel this one!") - .cancelable(false) - .icon(droid) - .id(4) - ) - .listener(new TapTargetSequence.Listener() { - // This listener will tell us when interesting(tm) events happen in regards - // to the sequence - @Override - public void onSequenceFinish() { - ((TextView) findViewById(R.id.educated)).setText("Congratulations! You're educated now!"); - } - - @Override - public void onSequenceStep(TapTarget lastTarget, boolean targetClicked) { - Log.d("TapTargetView", "Clicked on " + lastTarget.id()); - } - - @Override - public void onSequenceCanceled(TapTarget lastTarget) { - final AlertDialog dialog = new AlertDialog.Builder(MainActivity.this) - .setTitle("Uh oh") - .setMessage("You canceled the sequence") - .setPositiveButton("Oops", null).show(); - TapTargetView.showFor(dialog, - TapTarget.forView(dialog.getButton(DialogInterface.BUTTON_POSITIVE), "Uh oh!", "You canceled the sequence at step " + lastTarget.id()) - .cancelable(false) - .tintTarget(false), new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - dialog.dismiss(); - } - }); - } - }); - - // You don't always need a sequence, and for that there's a single time tap target - final SpannableString spannedDesc = new SpannableString("This is the sample app for TapTargetView"); - spannedDesc.setSpan(new UnderlineSpan(), spannedDesc.length() - "TapTargetView".length(), spannedDesc.length(), 0); - TapTargetView.showFor(this, TapTarget.forView(findViewById(R.id.fab), "Hello, world!", spannedDesc) - .cancelable(false) - .drawShadow(true) - .titleTextDimen(R.dimen.title_text_size) - .tintTarget(false), new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - // .. which evidently starts the sequence we defined earlier - sequence.start(); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - super.onOuterCircleClick(view); - Toast.makeText(view.getContext(), "You clicked the outer circle!", Toast.LENGTH_SHORT).show(); - } - - @Override - public void onTargetDismissed(TapTargetView view, boolean userInitiated) { - Log.d("TapTargetViewSample", "You dismissed me :("); - } - }); - } -} diff --git a/app/src/main/java/com/getkeepsafe/taptargetviewsample/MainActivity.kt b/app/src/main/java/com/getkeepsafe/taptargetviewsample/MainActivity.kt new file mode 100644 index 0000000..526ec90 --- /dev/null +++ b/app/src/main/java/com/getkeepsafe/taptargetviewsample/MainActivity.kt @@ -0,0 +1,172 @@ +package com.getkeepsafe.taptargetviewsample + +import android.content.DialogInterface +import android.graphics.Rect +import android.graphics.Typeface +import android.os.Bundle +import android.text.SpannableString +import android.text.style.StyleSpan +import android.text.style.UnderlineSpan +import android.util.Log +import android.view.View +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.core.content.ContextCompat +import com.getkeepsafe.taptargetview.target.TapTarget +import com.getkeepsafe.taptargetview.TapTargetSequence +import com.getkeepsafe.taptargetview.TapTargetView +import com.getkeepsafe.taptargetview.createNavigationIcon +import com.getkeepsafe.taptargetview.createOverflow +import com.getkeepsafe.taptargetview.createTarget +import com.getkeepsafe.taptargetview.forToolbarMenuItem +import com.getkeepsafe.taptargetview.showGuideView +import com.getkeepsafe.taptargetview.target.TapTargetShapeType + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + val toolbar = findViewById(R.id.toolbar) as Toolbar + toolbar.inflateMenu(R.menu.menu_main) + toolbar.navigationIcon = + ContextCompat.getDrawable(this, R.drawable.ic_arrow_back_white_24dp) + + // We load a drawable and create a location to show a tap target here + // We need the display to get the width and height at this point in time + val display = windowManager.defaultDisplay + // Load our little droid guy + val droid = ContextCompat.getDrawable(this, R.drawable.ic_android_black_24dp) + // Tell our droid buddy where we want him to appear + val droidTarget = Rect(0, 0, droid!!.intrinsicWidth * 2, droid.intrinsicHeight * 2) + // Using deprecated methods makes you look way cool + droidTarget.offset(display.width / 2, display.height / 2) + val sassyDesc = SpannableString("It allows you to go back, sometimes") + sassyDesc.setSpan( + StyleSpan(Typeface.ITALIC), + sassyDesc.length - "sometimes".length, + sassyDesc.length, + 0 + ) + + // We have a sequence of targets, so lets build it! + val sequence = TapTargetSequence(this) + .addTarget( // This tap target will target the back button, we just need to pass its containing toolbar + toolbar.createNavigationIcon("This is the back button", sassyDesc) + .id(1), // Likewise, this tap target will target the search button + toolbar.forToolbarMenuItem( + R.id.search, + "This is a search icon", + "As you can see, it has gotten pretty dark around here..." + ) + .dimColor(android.R.color.black) + .outerCircleColor(R.color.colorAccent) + .targetIconColor(android.R.color.black) + .transparentTarget(true) + .textColor(android.R.color.black) + .id(2), // You can also target the overflow button in your toolbar + toolbar.createOverflow( + "This will show more options", + "But they're not useful :(" + ).id(3), // This tap target will target our droid buddy at the given target rect + droid.createTarget( + droidTarget, + "Oh look!", + "You can point to any part of the screen. You also can't cancel this one!" + ) + .cancelable(false) + .id(4) + ) + .listener(object : TapTargetSequence.Listener { + // This listener will tell us when interesting(tm) events happen in regards + // to the sequence + override fun onSequenceFinish() { + (findViewById(R.id.educated) as TextView).text = + "Congratulations! You're educated now!" + } + + override fun onSequenceStep(lastTarget: TapTarget?, targetClicked: Boolean) { + Log.d("TapTargetView", "Clicked on " + lastTarget!!.id()) + } + + override fun onSequenceCanceled(lastTarget: TapTarget?) { + val dialog = AlertDialog.Builder(this@MainActivity) + .setTitle("Uh oh") + .setMessage("You canceled the sequence") + .setPositiveButton("Oops", null).show() + dialog.showGuideView( + dialog.getButton(DialogInterface.BUTTON_POSITIVE).createTarget( + "Uh oh!", + "You canceled the sequence at step " + lastTarget?.id() + ) + .cancelable(false) + .tintTarget(false), object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + dialog.dismiss() + } + }) + } + }) + + // You don't always need a sequence, and for that there's a single time tap target + val spannedDesc = SpannableString("This is the sample app for TapTargetView") + spannedDesc.setSpan( + UnderlineSpan(), + spannedDesc.length - "TapTargetView".length, + spannedDesc.length, + 0 + ) + this.showGuideView( + findViewById(R.id.fab).createTarget("Hello, world!", spannedDesc) + .cancelable(false) + .drawShadow(true) + .titleTextDimen(R.dimen.title_text_size) + .tintTarget(false), object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + super.onTargetClick(view) + // .. which evidently starts the sequence we defined earlier + sequence.start() + } + + override fun onOuterCircleClick(view: TapTargetView) { + super.onOuterCircleClick(view) + Toast.makeText( + view.context, + "You clicked the outer circle!", + Toast.LENGTH_SHORT + ).show() + } + + override fun onTargetDismissed(view: TapTargetView, userInitiated: Boolean) { + Log.d("TapTargetViewSample", "You dismissed me :(") + } + }) + + val ed = findViewById(R.id.ed) + ed.setOnClickListener { + showGuideView( + ed.createTarget("Please Input Some Thing", "Hello Some One") + .outerCircleColor(R.color.colorAccent) + .targetIconColor(android.R.color.holo_blue_dark) + .transparentTarget(true) + .textColor(android.R.color.black) + .setTargetShapeType(TapTargetShapeType.RectAngle(16)), + object : TapTargetView.Listener() { + override fun onTargetClick(view: TapTargetView) { + Toast.makeText( + view.context, + "You clicked the target! yes!!!!!", + Toast.LENGTH_SHORT + ).show() + super.onTargetClick(view) + } + } + ) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/getkeepsafe/taptargetviewsample/SampleApplication.java b/app/src/main/java/com/getkeepsafe/taptargetviewsample/SampleApplication.java deleted file mode 100644 index 8df680c..0000000 --- a/app/src/main/java/com/getkeepsafe/taptargetviewsample/SampleApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.getkeepsafe.taptargetviewsample; - -import android.app.Application; - -import com.facebook.stetho.Stetho; - -public class SampleApplication extends Application { - @Override - public void onCreate() { - super.onCreate(); - Stetho.initializeWithDefaults(this); - } -} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f65883e..9011d35 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -62,4 +62,13 @@ android:layout_height="wrap_content" android:padding="16dp"/> + + diff --git a/build.gradle b/build.gradle index 00806cc..f474b23 100644 --- a/build.gradle +++ b/build.gradle @@ -4,18 +4,18 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' - classpath 'com.vanniktech:gradle-maven-publish-plugin:0.17.0' + classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.vanniktech:gradle-maven-publish-plugin:0.25.3' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22' } } ext { - defAndroidXCoreVersion = '1.6.0' - defAndroidAnnotationVersion = '1.2.0' - defAppCompatVersion = '1.3.0' - defMaterialVersion = '1.4.0' - defCompileSdkVersion = 30 - defMinSdkVersion = 14 + defAndroidXCoreVersion = '1.10.1' + defAppCompatVersion = '1.6.1' + defMaterialVersion = '1.9.0' + defCompileSdkVersion = 33 + defMinSdkVersion = 21 } allprojects { @@ -25,6 +25,6 @@ allprojects { } } -task clean(type: Delete) { +tasks.register('clean', Delete) { delete rootProject.buildDir } diff --git a/gradle.properties b/gradle.properties index 3902f0f..2be6570 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,4 +22,5 @@ POM_LICENCE_DIST=repo POM_DEVELOPER_ID=keepsafe POM_DEVELOPER_NAME=KeepSafe Software, Inc. -POM_DEVELOPER_URL=https://github.com/KeepSafe/ \ No newline at end of file +POM_DEVELOPER_URL=https://github.com/KeepSafe/ +android.disableAutomaticComponentCreation=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9fe8d05..01ad3fb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sat Aug 05 11:39:16 CST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/gradlew b/gradlew index cccdd3d..3da45c1 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright ? 2015-2021 the original authors. +# +# 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 +# +# https://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. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions ?$var?, ?${var}?, ?${var:-default}?, ?${var+SET}?, +# ?${var#prefix}?, ?${var%suffix}?, and ?$( cmd )?; +# * compound commands having a testable exit status, especially ?case?; +# * various built-in commands including ?command?, ?set?, and ?ulimit?. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -89,84 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/taptargetview/build.gradle b/taptargetview/build.gradle index 85a0f5c..cfbacc8 100644 --- a/taptargetview/build.gradle +++ b/taptargetview/build.gradle @@ -1,35 +1,50 @@ -apply plugin: 'com.android.library' -apply plugin: 'com.vanniktech.maven.publish' +plugins { + id 'com.vanniktech.maven.publish' + id 'com.android.library' + id 'kotlin-android' +} android { - compileSdkVersion defCompileSdkVersion defaultConfig { + compileSdk defCompileSdkVersion minSdkVersion defMinSdkVersion targetSdkVersion defCompileSdkVersion } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { - api "androidx.annotation:annotation:$defAndroidAnnotationVersion" - api "androidx.appcompat:appcompat:$defAppCompatVersion" - implementation "androidx.core:core:$defAndroidXCoreVersion" + api("androidx.appcompat:appcompat:$defAppCompatVersion") { + exclude group: 'androidx.core', module: 'core-ktx' + } + implementation ("androidx.core:core-ktx:$defAndroidXCoreVersion") { + exclude group: 'androidx.core', module: 'core-ktx' + } + api("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22") { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib' + } } // build a jar with source files -task sourcesJar(type: Jar) { +tasks.register('sourcesJar', Jar) { from android.sourceSets.main.java.srcDirs classifier = 'sources' } -task javadoc(type: Javadoc) { - failOnError false +tasks.register('javadoc', Javadoc) { + failOnError false source = android.sourceSets.main.java.sourceFiles classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) } // build a jar with javadoc -task javadocJar(type: Jar, dependsOn: javadoc) { +tasks.register('javadocJar', Jar) { + dependsOn javadoc classifier = 'javadoc' from javadoc.destinationDir } @@ -38,3 +53,15 @@ artifacts { archives sourcesJar archives javadocJar } + +publishing { + publications { + myMaven(MavenPublication) { + groupId 'com.getkeepsafe' + artifactId 'taptargetview' + version '1.13.3' + artifact("$buildDir/outputs/aar/taptargetview-release.aar") + } + } + +} diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/FloatValueAnimatorBuilder.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/FloatValueAnimatorBuilder.java index 6389158..6382721 100644 --- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/FloatValueAnimatorBuilder.java +++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/FloatValueAnimatorBuilder.java @@ -69,12 +69,7 @@ public FloatValueAnimatorBuilder repeat(int times) { } public FloatValueAnimatorBuilder onUpdate(final UpdateListener listener) { - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - listener.onUpdate((float) animation.getAnimatedValue()); - } - }); + animator.addUpdateListener(animation -> listener.onUpdate((float) animation.getAnimatedValue())); return this; } diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ReflectExtensions.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ReflectExtensions.kt new file mode 100644 index 0000000..6ab870f --- /dev/null +++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ReflectExtensions.kt @@ -0,0 +1,12 @@ +@file:JvmName("ReflectExtensions") +package com.getkeepsafe.taptargetview + +import kotlin.jvm.Throws + +@Throws(NoSuchFieldException::class, IllegalAccessException::class) +fun Any?.getPrivateField(fieldName: String?): Any? { + if (fieldName == null || this == null) return null + val field = this.javaClass.getDeclaredField(fieldName) + field.isAccessible = true + return field.get(this) +} \ No newline at end of file diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ReflectUtil.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ReflectUtil.java deleted file mode 100644 index c9d84af..0000000 --- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ReflectUtil.java +++ /dev/null @@ -1,31 +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 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 targets; - private boolean active; - - @Nullable - private TapTargetView currentView; - - Listener listener; - boolean considerOuterCircleCanceled; - boolean continueOnCancel; - - public interface Listener { - /** Called when there are no more tap targets to display */ - void onSequenceFinish(); - - /** - * Called when moving onto the next tap target. - * @param lastTarget The last displayed target - * @param targetClicked Whether the last displayed target was clicked (this will always be true - * unless you have set {@link #continueOnCancel(boolean)} and the user - * clicks outside of the target - */ - void onSequenceStep(TapTarget lastTarget, boolean targetClicked); - - /** - * Called when the user taps outside of the current target, the target is cancelable, and - * {@link #continueOnCancel(boolean)} is not set. - * @param lastTarget The last displayed target - */ - void onSequenceCanceled(TapTarget lastTarget); - } - - public TapTargetSequence(Activity activity) { - if (activity == null) throw new IllegalArgumentException("Activity is null"); - this.activity = activity; - this.dialog = null; - this.targets = new LinkedList<>(); - } - - public TapTargetSequence(Dialog dialog) { - if (dialog == null) throw new IllegalArgumentException("Given null Dialog"); - this.dialog = dialog; - this.activity = null; - this.targets = new LinkedList<>(); - } - - /** Adds the given targets, in order, to the pending queue of {@link TapTarget}s */ - public TapTargetSequence targets(List targets) { - this.targets.addAll(targets); - return this; - } - - /** Adds the given targets, in order, to the pending queue of {@link TapTarget}s */ - public TapTargetSequence targets(TapTarget... targets) { - Collections.addAll(this.targets, targets); - return this; - } - - /** Adds the given target to the pending queue of {@link TapTarget}s */ - public TapTargetSequence target(TapTarget target) { - this.targets.add(target); - return this; - } - - /** Whether or not to continue the sequence when a {@link TapTarget} is canceled **/ - public TapTargetSequence continueOnCancel(boolean status) { - this.continueOnCancel = status; - return this; - } - - /** Whether or not to consider taps on the outer circle as a cancellation **/ - public TapTargetSequence considerOuterCircleCanceled(boolean status) { - this.considerOuterCircleCanceled = status; - return this; - } - - /** Specify the listener for this sequence **/ - public TapTargetSequence listener(Listener listener) { - this.listener = listener; - return this; - } - - /** Immediately starts the sequence and displays the first target from the queue **/ - @UiThread - public void start() { - if (targets.isEmpty() || active) { - return; - } - - active = true; - showNext(); - } - - /** Immediately starts the sequence from the given targetId's position in the queue */ - public void startWith(int targetId) { - if (active) { - return; - } - - while (targets.peek() != null && targets.peek().id() != targetId) { - targets.poll(); - } - - TapTarget peekedTarget = targets.peek(); - if (peekedTarget == null || peekedTarget.id() != targetId) { - throw new IllegalStateException("Given target " + targetId + " not in sequence"); - } - - start(); - } - - /** Immediately starts the sequence at the specified zero-based index in the queue */ - public void startAt(int index) { - if (active) { - return; - } - - if (index < 0 || index >= targets.size()) { - throw new IllegalArgumentException("Given invalid index " + index); - } - - final int expectedSize = targets.size() - index; - while (targets.peek() != null && targets.size() != expectedSize) { - targets.poll(); - } - - if (targets.size() != expectedSize) { - throw new IllegalStateException("Given index " + index + " not in sequence"); - } - - start(); - } - - /** - * Cancels the sequence, if the current target is cancelable. - * When the sequence is canceled, the current target is dismissed and the remaining targets are - * removed from the sequence. - * @return whether the sequence was canceled or not - */ - @UiThread - public boolean cancel() { - if (!active || currentView == null || !currentView.cancelable) { - return false; - } - currentView.dismiss(false); - active = false; - targets.clear(); - if (listener != null) { - listener.onSequenceCanceled(currentView.target); - } - return true; - } - - void showNext() { - try { - TapTarget tapTarget = targets.remove(); - if (activity != null) { - currentView = TapTargetView.showFor(activity, tapTarget, tapTargetListener); - } else { - currentView = TapTargetView.showFor(dialog, tapTarget, tapTargetListener); - } - } catch (NoSuchElementException e) { - currentView = null; - // No more targets - if (listener != null) { - listener.onSequenceFinish(); - } - } - } - - private final TapTargetView.Listener tapTargetListener = new TapTargetView.Listener() { - @Override - public void onTargetClick(TapTargetView view) { - super.onTargetClick(view); - if (listener != null) { - listener.onSequenceStep(view.target, true); - } - showNext(); - } - - @Override - public void onOuterCircleClick(TapTargetView view) { - if (considerOuterCircleCanceled) { - onTargetCancel(view); - } - } - - @Override - public void onTargetCancel(TapTargetView view) { - super.onTargetCancel(view); - if (continueOnCancel) { - if (listener != null) { - listener.onSequenceStep(view.target, false); - } - showNext(); - } else { - if (listener != null) { - listener.onSequenceCanceled(view.target); - } - } - } - }; -} diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetSequence.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetSequence.kt new file mode 100644 index 0000000..3927b74 --- /dev/null +++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetSequence.kt @@ -0,0 +1,147 @@ +@file:Suppress("unused") +package com.getkeepsafe.taptargetview + +import android.app.Activity +import android.app.Dialog +import androidx.annotation.UiThread +import com.getkeepsafe.taptargetview.target.TapTarget +import java.util.LinkedList +import java.util.Queue + +class TapTargetSequence: TapTargetView.Listener { + + private val targets: Queue = LinkedList() + + private var activity: Activity? = null + + private var dialog: Dialog? = null + + private var isActive: Boolean = false + + private var listener: Listener? = null + + private var considerOuterCircleCanceled = false + + private var continueOnCancel = false + + private var currentView: TapTargetView? = null + + constructor(activity: Activity?) { + this.activity = activity + this.dialog = null + } + + constructor(dialog: Dialog?) { + this.dialog = dialog + this.activity = null + } + + fun targets(targets: Iterable): TapTargetSequence { + this.targets.addAll(targets) + return this + } + + fun addTarget(vararg targets: TapTarget): TapTargetSequence { + this.targets.addAll(targets) + return this + } + + fun continueOnCancel(status: Boolean): TapTargetSequence { + this.continueOnCancel = status + return this + } + + fun considerOuterCircleCanceled(status: Boolean): TapTargetSequence { + considerOuterCircleCanceled = status + return this + } + + /** Specify the listener for this sequence */ + fun listener(listener: Listener?): TapTargetSequence { + this.listener = listener + return this + } + + + @UiThread + fun start() { + if (targets.isEmpty() || isActive) return + isActive = true + showNext() + } + + @UiThread + fun cancel(): Boolean { + val currentView = this.currentView ?: return false + if (!isActive || !currentView.cancelable) return false + currentView.dismiss(false) + isActive = false + targets.clear() + listener?.onSequenceCanceled(currentView.target) + return true + } + + fun startAt(index: Int) { + if (isActive) return + if (index < 0 || index >= targets.size) return + val expectedSize = targets.size - index + repeat(index) { + targets.poll() + } + check(targets.size == expectedSize) { "Given index $index not in sequence" } + start() + } + + private fun showNext() { + val target = targets.poll() + if (target == null) { + currentView = null + listener?.onSequenceFinish() + return + } + currentView = activity?.let { it.showGuideView(target, this) } + currentView = dialog?.let { it.showGuideView(target, this) } + } + + override fun onTargetClick(view: TapTargetView?) { + super.onTargetClick(view) + listener?.onSequenceStep(view?.target, true) + showNext() + } + + override fun onTargetCancel(view: TapTargetView?) { + super.onTargetCancel(view) + if (continueOnCancel) { + listener?.onSequenceStep(view?.target, false) + showNext() + } else { + listener?.onSequenceCanceled(view?.target) + } + } + + override fun onOuterCircleClick(view: TapTargetView?) { + if (considerOuterCircleCanceled) onTargetCancel(view) + } + + interface Listener { + /** Called when there are no more tap targets to display */ + fun onSequenceFinish() + + /** + * Called when moving onto the next tap target. + * @param lastTarget The last displayed target + * @param targetClicked Whether the last displayed target was clicked (this will always be true + * unless you have set [.continueOnCancel] and the user + * clicks outside of the target + */ + fun onSequenceStep(lastTarget: TapTarget?, targetClicked: Boolean) + + /** + * Called when the user taps outside of the current target, the target is cancelable, and + * [.continueOnCancel] is not set. + * @param lastTarget The last displayed target + */ + fun onSequenceCanceled(lastTarget: TapTarget?) + } + +} \ No newline at end of file diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetView.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetView.java index c21735d..a8c543d 100644 --- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetView.java +++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetView.java @@ -19,7 +19,6 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; -import android.app.Dialog; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; @@ -28,7 +27,6 @@ import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Path; -import android.graphics.PixelFormat; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffXfermode; @@ -44,7 +42,6 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.util.DisplayMetrics; -import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -55,12 +52,15 @@ import android.view.WindowManager; import android.view.animation.AccelerateDecelerateInterpolator; +import com.getkeepsafe.taptargetview.target.TapTarget; +import com.getkeepsafe.taptargetview.target.TapTargetShapeType; + /** * TapTargetView implements a feature discovery paradigm following Google's Material Design * guidelines. *

* 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(android.R.id.content) + val targetView = TapTargetView(this, decorView, content, target, listener) + decorView.addView(targetView, layoutParams) + return targetView +} + +@JvmName("showGuideWithDialog") +fun Dialog?.showGuideView( + target: TapTarget, + listener: TapTargetView.Listener? = null +): TapTargetView { + if (this == null) throw IllegalArgumentException("Dialog is null") + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val params = WindowManager.LayoutParams().apply { + type = WindowManager.LayoutParams.TYPE_APPLICATION + format = PixelFormat.RGBA_8888 + flags = 0 + gravity = Gravity.START or Gravity.TOP + x = 0 + y = 0 + width = WindowManager.LayoutParams.MATCH_PARENT + height = WindowManager.LayoutParams.MATCH_PARENT + } + + val targetView = TapTargetView(context, windowManager, null, target, listener) + windowManager.addView(targetView, params) + return targetView +} \ No newline at end of file diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ToolbarTapTarget.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ToolbarTapTarget.java index 664ed39..78e6149 100644 --- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ToolbarTapTarget.java +++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ToolbarTapTarget.java @@ -15,7 +15,6 @@ */ package com.getkeepsafe.taptargetview; -import android.annotation.TargetApi; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.TextUtils; @@ -31,23 +30,25 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; -class ToolbarTapTarget extends ViewTapTarget { - ToolbarTapTarget(Toolbar toolbar, @IdRes int menuItemId, +import com.getkeepsafe.taptargetview.target.TapTarget; + +public class ToolbarTapTarget extends TapTarget { + public ToolbarTapTarget(Toolbar toolbar, @IdRes int menuItemId, CharSequence title, @Nullable CharSequence description) { super(toolbar.findViewById(menuItemId), title, description); } - ToolbarTapTarget(android.widget.Toolbar toolbar, @IdRes int menuItemId, + public ToolbarTapTarget(android.widget.Toolbar toolbar, @IdRes int menuItemId, CharSequence title, @Nullable CharSequence description) { super(toolbar.findViewById(menuItemId), title, description); } - ToolbarTapTarget(Toolbar toolbar, boolean findNavView, + public ToolbarTapTarget(Toolbar toolbar, boolean findNavView, CharSequence title, @Nullable CharSequence description) { super(findNavView ? findNavView(toolbar) : findOverflowView(toolbar), title, description); } - ToolbarTapTarget(android.widget.Toolbar toolbar, boolean findNavView, + public ToolbarTapTarget(android.widget.Toolbar toolbar, boolean findNavView, CharSequence title, @Nullable CharSequence description) { super(findNavView ? findNavView(toolbar) : findOverflowView(toolbar), title, description); } @@ -59,8 +60,7 @@ private static ToolbarProxy proxyOf(Object instance) { if (instance instanceof Toolbar) { return new SupportToolbarProxy((Toolbar) instance); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && instance instanceof android.widget.Toolbar) { + } else if (instance instanceof android.widget.Toolbar) { return new StandardToolbarProxy((android.widget.Toolbar) instance); } @@ -139,9 +139,9 @@ private static View findOverflowView(Object instance) { // The "ActionMenuPresenter" then holds a reference to an "OverflowMenuButton" which is the // desired target try { - final Object actionMenuView = ReflectUtil.getPrivateField(toolbar.internalToolbar(), "mMenuView"); - final Object actionMenuPresenter = ReflectUtil.getPrivateField(actionMenuView, "mPresenter"); - return (View) ReflectUtil.getPrivateField(actionMenuPresenter, "mOverflowButton"); + final Object actionMenuView = ReflectExtensions.getPrivateField(toolbar.internalToolbar(), "mMenuView"); + final Object actionMenuPresenter = ReflectExtensions.getPrivateField(actionMenuView, "mPresenter"); + return (View) ReflectExtensions.getPrivateField(actionMenuPresenter, "mOverflowButton"); } catch (NoSuchFieldException e) { throw new IllegalStateException("Could not find overflow view for Toolbar!", e); } catch (IllegalAccessException e) { @@ -216,7 +216,6 @@ public Object internalToolbar() { } } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static class StandardToolbarProxy implements ToolbarProxy { private final android.widget.Toolbar toolbar; diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/UiUtil.java b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/UiUtil.java deleted file mode 100644 index b34ec09..0000000 --- a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/UiUtil.java +++ /dev/null @@ -1,70 +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.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 { + val possibleTop = targetBounds.top - edgeLength - totalTextHeight - textPadding + val top = if (possibleTop > topBoundary) { + possibleTop + } else { + targetBounds.centerY() + edgeLength + targetPadding + } + return top to top + totalTextHeight + } + + override fun clickInTarget(targetBounds: Rect, lastTouchX: Int, lastTouchY: Int): Boolean { + return targetBounds.contains(lastTouchX, lastTouchY) + } +} \ No newline at end of file diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/TapTarget.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/TapTarget.kt new file mode 100644 index 0000000..a44f788 --- /dev/null +++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/TapTarget.kt @@ -0,0 +1,382 @@ +@file:Suppress("unused") +package com.getkeepsafe.taptargetview.target + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.Typeface +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DimenRes +import androidx.core.content.ContextCompat +import androidx.core.view.doOnLayout +import androidx.core.view.drawToBitmap +import com.getkeepsafe.taptargetview.sp + +open class TapTarget { + + private var view: View? = null + + val title: CharSequence? + val description: CharSequence? + private var bounds: Rect? + var icon: Drawable? = null + + internal var tapTargetType: TapTargetShapeType = TapTargetShapeType.Circle + + @JvmOverloads + constructor( + view: View, + title: CharSequence?, + description: CharSequence?, + bounds: Rect? = null + ) { + this.view = view + this.title = title + this.description = description + this.bounds = bounds + } + + @JvmOverloads + constructor( + icon: Drawable, + title: CharSequence?, + description: CharSequence?, + bounds: Rect? = null, + iconBounds: Rect? = null + ) { + this.icon = icon + if (iconBounds == null) { + icon.bounds = Rect(0, 0, icon.intrinsicWidth, icon.intrinsicHeight) + } else { + icon.bounds = iconBounds + } + this.title = title + this.description = description + this.bounds = bounds + } + + var outerCircleAlpha = 0.96f + private set + + var titleTypeface: Typeface? = null + var descriptionTypeface: Typeface? = null + + @ColorRes + private var outerCircleColorRes = -1 + + @ColorRes + private var targetIconColorRes = -1 + + @ColorRes + private var dimColorRes = -1 + + @ColorRes + private var titleTextColorRes = -1 + + @ColorRes + private var descriptionTextColorRes = -1 + private var outerCircleColor: Int? = null + private var targetIconColor: Int? = null + private var dimColor: Int? = null + private var titleTextColor: Int? = null + private var descriptionTextColor: Int? = null + + @DimenRes + private var titleTextDimen = -1 + + @DimenRes + private var descriptionTextDimen = -1 + private var titleTextSize = 20 + private var descriptionTextSize = 18 + private var id = -1 + var drawShadow = false + var cancelable = true + var tintTarget = true + var transparentTarget = false + var descriptionTextAlpha = 0.54f + + /** Specify whether the target should be transparent */ + fun transparentTarget(transparent: Boolean): TapTarget { + transparentTarget = transparent + return this + } + + /** Specify the color resource for the outer circle */ + fun outerCircleColor(@ColorRes color: Int): TapTarget { + outerCircleColorRes = color + return this + } + + /** Specify the color value for the outer circle */ + fun outerCircleColorInt(@ColorInt color: Int): TapTarget { + outerCircleColor = color + return this + } + + /** Specify the alpha value [0.0, 1.0] of the outer circle */ + fun outerCircleAlpha(alpha: Float): TapTarget { + require(!(alpha < 0.0f || alpha > 1.0f)) { "Given an invalid alpha value: $alpha" } + outerCircleAlpha = alpha + return this + } + + /** Specify the color resource for the target circle */ + fun targetIconColor(@ColorRes color: Int): TapTarget { + targetIconColorRes = color + return this + } + + /** Specify the color value for the target circle */ + fun targetIconColorInt(@ColorInt color: Int): TapTarget { + targetIconColor = color + return this + } + + /** Specify the color resource for all text */ + fun textColor(@ColorRes color: Int): TapTarget { + titleTextColorRes = color + descriptionTextColorRes = color + return this + } + + /** Specify the color value for all text */ + fun textColorInt(@ColorInt color: Int): TapTarget { + titleTextColor = color + descriptionTextColor = color + return this + } + + /** Specify the color resource for the title text */ + fun titleTextColor(@ColorRes color: Int): TapTarget { + titleTextColorRes = color + return this + } + + /** Specify the color value for the title text */ + fun titleTextColorInt(@ColorInt color: Int): TapTarget { + titleTextColor = color + return this + } + + /** Specify the color resource for the description text */ + fun descriptionTextColor(@ColorRes color: Int): TapTarget { + descriptionTextColorRes = color + return this + } + + /** Specify the color value for the description text */ + fun descriptionTextColorInt(@ColorInt color: Int): TapTarget { + descriptionTextColor = color + return this + } + + /** Specify the typeface for all text */ + fun textTypeface(typeface: Typeface?): TapTarget { + requireNotNull(typeface) { "Cannot use a null typeface" } + titleTypeface = typeface + descriptionTypeface = typeface + return this + } + + /** Specify the typeface for title text */ + fun titleTypeface(titleTypeface: Typeface?): TapTarget { + requireNotNull(titleTypeface) { "Cannot use a null typeface" } + this.titleTypeface = titleTypeface + return this + } + + /** Specify the typeface for description text */ + fun descriptionTypeface(descriptionTypeface: Typeface?): TapTarget { + requireNotNull(descriptionTypeface) { "Cannot use a null typeface" } + this.descriptionTypeface = descriptionTypeface + return this + } + + /** Specify the text size for the title in SP */ + fun titleTextSize(sp: Int): TapTarget { + require(sp >= 0) { "Given negative text size" } + titleTextSize = sp + return this + } + + /** Specify the text size for the description in SP */ + fun descriptionTextSize(sp: Int): TapTarget { + require(sp >= 0) { "Given negative text size" } + 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 + */ + fun titleTextDimen(@DimenRes dimen: Int): TapTarget { + titleTextDimen = dimen + return this + } + + /** Specify the alpha value [0.0, 1.0] of the description text */ + fun descriptionTextAlpha(descriptionTextAlpha: Float): TapTarget { + require(!(descriptionTextAlpha < 0 || descriptionTextAlpha > 1f)) { "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 + */ + fun descriptionTextDimen(@DimenRes dimen: Int): TapTarget { + 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 + */ + fun dimColor(@ColorRes color: Int): TapTarget { + 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 + */ + fun dimColorInt(@ColorInt color: Int): TapTarget { + dimColor = color + return this + } + + /** Specify whether or not to draw a drop shadow around the outer circle */ + fun drawShadow(draw: Boolean): TapTarget { + drawShadow = draw + return this + } + + /** Specify whether or not the target should be cancelable */ + fun cancelable(status: Boolean): TapTarget { + cancelable = status + return this + } + + /** Specify whether to tint the target's icon with the outer circle's color */ + fun tintTarget(tint: Boolean): TapTarget { + tintTarget = tint + return this + } + + /** Specify a unique identifier for this target. */ + fun id(id: Int): TapTarget { + this.id = id + return this + } + + /** Return the id associated with this tap target */ + fun id(): Int { + 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. + */ + open fun onReady(runnable: Runnable?) { + val view = this.view ?: kotlin.run { + runnable?.run() + tapTargetType.onReadyTarget(bounds) + return + } + view.doOnLayout { + val location = IntArray(2) + view.getLocationOnScreen(location) + bounds = Rect( + location[0], + location[1], + location[0] + view.width, + location[1] + view.height + ) + if (icon == null && view.width > 0 && view.height > 0) { + val viewBitmap = view.drawToBitmap() + val canvas = Canvas(viewBitmap) + view.draw(canvas) + val icon = BitmapDrawable(view.context.resources, viewBitmap) + icon.setBounds(0, 0, view.width, view.height) + this.icon = icon + } + tapTargetType.onReadyTarget(bounds) + 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 [.onReady] invokes its runnable + */ + fun bounds(): Rect { + checkNotNull(bounds) { "Requesting bounds that are not set! Make sure your target is ready" } + return bounds as Rect + } + + fun outerCircleColorInt(context: Context): Int? { + return colorResOrInt(context, outerCircleColor, outerCircleColorRes) + } + + fun targetCircleColorInt(context: Context): Int? { + return colorResOrInt(context, targetIconColor, targetIconColorRes) + } + + fun dimColorInt(context: Context): Int? { + return colorResOrInt(context, dimColor, dimColorRes) + } + + fun titleTextColorInt(context: Context): Int? { + return colorResOrInt(context, titleTextColor, titleTextColorRes) + } + + fun descriptionTextColorInt(context: Context): Int? { + return colorResOrInt(context, descriptionTextColor, descriptionTextColorRes) + } + + fun titleTextSizePx(context: Context): Int { + return dimenOrSize(context, titleTextSize, titleTextDimen) + } + + fun descriptionTextSizePx(context: Context): Int { + return dimenOrSize(context, descriptionTextSize, descriptionTextDimen) + } + + private fun colorResOrInt(context: Context, value: Int?, @ColorRes resource: Int): Int? { + return if (resource != -1) { + ContextCompat.getColor(context, resource) + } else value + } + + private fun dimenOrSize(context: Context, size: Int, @DimenRes dimen: Int): Int { + return if (dimen != -1) { + context.resources.getDimensionPixelSize(dimen) + } else size.sp + } + + fun setTargetShapeType(type: TapTargetShapeType): TapTarget { + this.tapTargetType = type + return this + } + +} \ No newline at end of file diff --git a/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/TapTargetShapeType.kt b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/TapTargetShapeType.kt new file mode 100644 index 0000000..817a045 --- /dev/null +++ b/taptargetview/src/main/java/com/getkeepsafe/taptargetview/target/TapTargetShapeType.kt @@ -0,0 +1,115 @@ +@file:Suppress("unused") +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 + +abstract class TapTargetShapeType { + + var textPadding = 40.dp + + var circlePadding = 40.dp + + var textSpacing = 8.dp + + var textMaxWidth = 360.dp + + var targetPadding = 20.dp + + var textPositionBias = 20.dp + + + companion object { + + val Circle = CircleShapeTapTarget() + + val RectAngle = RectAngleShapeType() + + fun RectAngle(roundRadius: Int): RectAngleShapeType { + val rectangleType = RectAngleShapeType() + rectangleType.roundRadius = roundRadius + return rectangleType + } + + } + + + abstract val edgeLength: Int + + open fun initResource(context: Context) {} + + abstract fun expandContractChange(lerpTime: Float, isExpanding: Boolean) + + open fun pulseAnimation(lerpTime: Float) {} + + abstract fun dismissConfirmAnimation(lerpTime: Float) + + abstract fun drawTarget( + canvas: Canvas, + targetBounds: Rect, + paint: Paint + ) + + open fun drawPulse( + canvas: Canvas, + targetPulseAlpha: Float, + targetBounds: Rect, + paint: Paint + ) {} + + open fun drawInformation(canvas: Canvas, targetBounds: Rect, paint: Paint) {} + + abstract fun clickInTarget(targetBounds: Rect, lastTouchX: Int, lastTouchY: Int): Boolean + + open fun onReadyTarget(bounds: Rect?) {} + + open fun getTextBounds( + totalTextHeight: Int, + totalTextWidth: Int, + targetBounds: Rect, + topBoundary: Int, + viewWidth: Int + ): Rect { + val verticalLocation = getTextVertical(targetBounds, totalTextHeight, topBoundary) + val horizontalLocation = getTextHorizontal(targetBounds, totalTextWidth, viewWidth, viewWidth) + return Rect( + horizontalLocation.first, + verticalLocation.first, + horizontalLocation.second, + verticalLocation.second + ) + } + + open fun getTextVertical( + targetBounds: Rect, + totalTextHeight: Int, + topBoundary: Int + ): Pair { + val possibleTop = targetBounds.centerY() - edgeLength - targetPadding - totalTextHeight + val top = if (possibleTop > topBoundary) { + possibleTop + } else { + targetBounds.centerY() + edgeLength + targetPadding + } + return top to top + totalTextHeight + } + + open fun getTextHorizontal( + targetBounds: Rect, + totalTextWidth: Int, + leftBoundary: Int, + viewWidth: Int + ): Pair { + val relativeCenterDistance: Int = viewWidth / 2 - targetBounds.centerX() + val bias: Int = + if (relativeCenterDistance < 0) -textPositionBias else textPositionBias + val left: Int = textPadding.coerceAtLeast(targetBounds.centerX() - bias - totalTextWidth) + val right = (viewWidth - textPadding).coerceAtMost(left + totalTextWidth) + return left to right + } + +} +