views, final OnMeasuredHandler handler) {
28 | final int[] count = {views.size()};
29 |
30 | if (count[0] <= 0) {
31 | handler.onMeasured();
32 | }
33 |
34 | for (View view : views) {
35 | afterOrAlreadyMeasured(view, new OnMeasuredHandler() {
36 | @Override
37 | public void onMeasured() {
38 | count[0]--;
39 | if (count[0] <= 0) {
40 | handler.onMeasured();
41 | }
42 | }
43 | });
44 | }
45 | }
46 |
47 | public static void afterOrAlreadyMeasured(final View view, final OnMeasuredHandler handler) {
48 | if (view.getMeasuredWidth() != 0 && view.getMeasuredHeight() != 0) {
49 | handler.onMeasured();
50 | } else {
51 | afterMeasured(view, new OnMeasuredHandler() {
52 | @Override
53 | public void onMeasured() {
54 | handler.onMeasured();
55 | }
56 | });
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://jitpack.io/#dimorinny/show-case-card-view)
4 |
5 | ## Dependency
6 |
7 | Firstly, add Jitpack repository in your root build.gradle file (not your module build.gradle file):
8 |
9 | ```
10 | allprojects {
11 | repositories {
12 | ...
13 | maven { url "https://jitpack.io" }
14 | }
15 | }
16 | ```
17 |
18 | Add dependency to your module's build.gradle file:
19 |
20 | ```
21 | dependencies {
22 | implementation 'com.github.dimorinny:show-case-card-view:0.0.4'
23 | }
24 | ```
25 |
26 | ## Usage
27 |
28 | You can display a ShowCase on your activity or fragment using the below code.
29 |
30 | To display a list of (click-through) steps:
31 | ```java
32 | new ShowCaseStepDisplayer.Builder(MainActivity.this)
33 | .addStep(new ShowCaseStep(new Center(), "Message at center"))
34 | .addStep(new ShowCaseStep(view, "Message at View"))
35 | .build().start();
36 | ```
37 |
38 | Use withScrollView() if some step's target Views could be inside a ScrollView, they will be auto-scrolled to:
39 |
40 | ```java
41 | new ShowCaseStepDisplayer.Builder(MainActivity.this)
42 | .withScrollView(scrollView)
43 | .addStep(new ShowCaseStep(view, "Message at View to scroll to"))
44 | .addStep(new ShowCaseStep(new TopLeft(), "Message at TopLeft"))
45 | .build().start();
46 | ```
47 |
48 | To display a single item:
49 |
50 | ```java
51 | new ShowCaseView.Builder(MainActivity.this)
52 | .withTypedPosition(new TopLeft())
53 | .withTypedRadius(new Radius(186F))
54 | .withContent("This is hello world!")
55 | .build()
56 | .show(this);
57 | ```
58 |
59 | **Available positions:**
60 |
61 | * `Position(PointF position)`
62 | * `TopLeft()`
63 | * `TopRight()`
64 | * `BottomLeft()`
65 | * `BottomRight()`
66 | * `TopLeftToolbar()`
67 | * `TopRightToolbar()`
68 | * `ViewPosition(View view)`
69 | * `Center()`
70 | * `BottomCenter()`
71 |
72 | **Available radiuses:**
73 |
74 | * `Radius(float radius)`
75 | * `ViewRadius(View view)`
76 |
77 | For more complicated usage - see [example](https://github.com/dimorinny/show-case-card-view/blob/master/app/src/main/java/ru/dimorinny/showcasesample/MainActivity.java).
--------------------------------------------------------------------------------
/showcasecard/src/main/java/ru/dimorinny/showcasecard/step/ShowCaseStepScroller.java:
--------------------------------------------------------------------------------
1 | package ru.dimorinny.showcasecard.step;
2 |
3 | import android.graphics.Point;
4 | import android.support.annotation.NonNull;
5 | import android.widget.ScrollView;
6 |
7 | /**
8 | * Created by Frank on 2017/08/17.
9 | *
10 | * Handles scrolling to a {@link ShowCaseStep} if needed.
11 | */
12 | public class ShowCaseStepScroller {
13 |
14 | @NonNull
15 | private ScrollView scrollView;
16 | private int lastTrackedScrollY;
17 |
18 | public ShowCaseStepScroller(@NonNull ScrollView scrollView) {
19 | this.scrollView = scrollView;
20 | }
21 |
22 | interface OnCompleteListener {
23 |
24 | void onComplete();
25 | }
26 |
27 | interface OnScrollStoppedListener {
28 |
29 | void onScrollStopped();
30 | }
31 |
32 | /**
33 | * Scrolls the given scrollView so that showCaseItem is completely in view.
34 | *
35 | * @param showCaseItem
36 | * @param onCompleteListener
37 | */
38 | public void scrollToShowCaseStepItem(ShowCaseStep showCaseItem, final OnCompleteListener onCompleteListener) {
39 |
40 | Point scrollTo = showCaseItem.getPosition().getScrollPosition(scrollView);
41 |
42 | if (scrollTo == null) {
43 | // no scroll needed
44 | onCompleteListener.onComplete();
45 | return;
46 | }
47 |
48 | scrollView.smoothScrollBy(scrollTo.x, scrollTo.y - scrollView.getScrollY());
49 |
50 | detectScrollFinished(new OnScrollStoppedListener() {
51 | @Override
52 | public void onScrollStopped() {
53 | onCompleteListener.onComplete();
54 | }
55 | });
56 | }
57 |
58 | private void detectScrollFinished(final OnScrollStoppedListener onScrollStoppedListener) {
59 |
60 | lastTrackedScrollY = scrollView.getScrollY();
61 |
62 | final int checkIntervalMs = 50;
63 |
64 | scrollView.postDelayed(new Runnable() {
65 |
66 | public void run() {
67 | int newPosition = scrollView.getScrollY();
68 |
69 | if (lastTrackedScrollY - newPosition == 0) {
70 | onScrollStoppedListener.onScrollStopped();
71 | } else {
72 | lastTrackedScrollY = scrollView.getScrollY();
73 | scrollView.postDelayed(this, checkIntervalMs);
74 | }
75 | }
76 | }, checkIntervalMs);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/macos,android,intellij
3 |
4 | ### Android ###
5 | # Built application files
6 | *.apk
7 | *.ap_
8 |
9 | # Files for the ART/Dalvik VM
10 | *.dex
11 |
12 | # Java class files
13 | *.class
14 |
15 | # Generated files
16 | bin/
17 | gen/
18 | out/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # Intellij
40 | *.iml
41 | .idea/
42 |
43 | # Keystore files
44 | *.jks
45 |
46 | # External native build folder generated in Android Studio 2.2 and later
47 | .externalNativeBuild
48 |
49 | # Google Services (e.g. APIs or Firebase)
50 | google-services.json
51 |
52 | # Freeline
53 | freeline.py
54 | freeline/
55 | freeline_project_description.json
56 |
57 | ### Android Patch ###
58 | gen-external-apklibs
59 |
60 | ### Intellij ###
61 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
62 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
63 |
64 | # CMake
65 | cmake-build-debug/
66 |
67 | ## File-based project format:
68 | *.iws
69 |
70 | ## Plugin-specific files:
71 |
72 | # IntelliJ
73 | /out/
74 |
75 | # mpeltonen/sbt-idea plugin
76 | .idea_modules/
77 |
78 | # JIRA plugin
79 | atlassian-ide-plugin.xml
80 |
81 | # Cursive Clojure plugin
82 | .idea/replstate.xml
83 |
84 | # Crashlytics plugin (for Android Studio and IntelliJ)
85 | com_crashlytics_export_strings.xml
86 | crashlytics.properties
87 | crashlytics-build.properties
88 | fabric.properties
89 |
90 | ### Intellij Patch ###
91 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
92 |
93 | # *.iml
94 | # modules.xml
95 | # *.ipr
96 |
97 | ### macOS ###
98 | *.DS_Store
99 | .AppleDouble
100 | .LSOverride
101 |
102 | # Icon must end with two \r
103 | Icon
104 |
105 | # Thumbnails
106 | ._*
107 |
108 | # Files that might appear in the root of a volume
109 | .DocumentRevisions-V100
110 | .fseventsd
111 | .Spotlight-V100
112 | .TemporaryItems
113 | .Trashes
114 | .VolumeIcon.icns
115 | .com.apple.timemachine.donotpresent
116 |
117 | # Directories potentially created on remote AFP share
118 | .AppleDB
119 | .AppleDesktop
120 | Network Trash Folder
121 | Temporary Items
122 | .apdisk
123 |
124 | # End of https://www.gitignore.io/api/macos,android,intellij
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/showcasecard/src/main/java/ru/dimorinny/showcasecard/util/NavigationBarUtils.java:
--------------------------------------------------------------------------------
1 | package ru.dimorinny.showcasecard.util;
2 |
3 | import android.app.Activity;
4 | import android.os.Build;
5 | import android.view.Gravity;
6 | import android.widget.FrameLayout;
7 |
8 | public class NavigationBarUtils {
9 |
10 | public enum NavigationBarPosition {
11 | BOTTOM,
12 | LEFT,
13 | RIGHT,
14 | UNKNOWN
15 | }
16 |
17 | public static int navigationBarHeight(Activity activity) {
18 | int resourceId = activity.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
19 |
20 | if (hasNavigationBar(activity) && resourceId > 0) {
21 | return activity.getResources().getDimensionPixelSize(resourceId);
22 | }
23 |
24 | return 0;
25 | }
26 |
27 | public static NavigationBarPosition navigationBarPosition(final Activity activity) {
28 | NavigationBarPosition result = NavigationBarPosition.UNKNOWN;
29 |
30 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && hasNavigationBar(activity)) {
31 | int gravity = ((FrameLayout.LayoutParams) activity.findViewById(android.R.id.navigationBarBackground)
32 | .getLayoutParams()).gravity;
33 | return gravityToNavigationBarPosition(gravity);
34 | }
35 |
36 | return result;
37 | }
38 |
39 | private static boolean hasNavigationBar(Activity activity) {
40 | int id = activity.getResources().getIdentifier("config_showNavigationBar", "bool", "android");
41 |
42 | return !(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
43 | && activity.findViewById(android.R.id.navigationBarBackground) == null) &&
44 | id > 0 && activity.getResources().getBoolean(id);
45 | }
46 |
47 | private static NavigationBarPosition gravityToNavigationBarPosition(int gravity) {
48 | switch (gravity) {
49 | case Gravity.BOTTOM:
50 | return NavigationBarPosition.BOTTOM;
51 | case Gravity.LEFT:
52 | return NavigationBarPosition.LEFT;
53 | case Gravity.RIGHT:
54 | return NavigationBarPosition.RIGHT;
55 | default:
56 | return NavigationBarPosition.UNKNOWN;
57 | }
58 | }
59 |
60 | public static float navigationBarMarginForRightOrientation(Activity activity) {
61 | if (NavigationBarUtils.navigationBarPosition(activity) == NavigationBarUtils.NavigationBarPosition.LEFT) {
62 | return 0F;
63 | } else {
64 | return NavigationBarUtils.navigationBarHeight(activity);
65 | }
66 | }
67 |
68 | public static float navigationBarMarginForLeftOrientation(Activity activity) {
69 | if (NavigationBarUtils.navigationBarPosition(activity) == NavigationBarUtils.NavigationBarPosition.LEFT) {
70 | return NavigationBarUtils.navigationBarHeight(activity);
71 | } else {
72 | return 0F;
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/showcasecard/src/main/java/ru/dimorinny/showcasecard/step/ShowCaseStep.java:
--------------------------------------------------------------------------------
1 | package ru.dimorinny.showcasecard.step;
2 |
3 | import android.content.Context;
4 | import android.support.annotation.ColorRes;
5 |
6 | import ru.dimorinny.showcasecard.R;
7 | import ru.dimorinny.showcasecard.position.ShowCasePosition;
8 | import ru.dimorinny.showcasecard.radius.Radius;
9 | import ru.dimorinny.showcasecard.radius.ShowCaseRadius;
10 | import ru.dimorinny.showcasecard.util.ViewUtils;
11 |
12 | public class ShowCaseStep {
13 |
14 | private ShowCasePosition position;
15 | private ShowCaseRadius radius;
16 | private String message;
17 |
18 | @ColorRes
19 | private int color;
20 |
21 | public ShowCasePosition getPosition() {
22 | return position;
23 | }
24 |
25 | public ShowCaseRadius getRadius() {
26 | return radius;
27 | }
28 |
29 | public String getMessage() {
30 | return message;
31 | }
32 |
33 | public @ColorRes
34 | int getColor() {
35 | return color;
36 | }
37 |
38 | public static class Builder {
39 |
40 | private final static int DEFAULT_RADIUS_DP = 70;
41 |
42 | private ShowCaseRadius radius;
43 | private ShowCasePosition position;
44 | private String message;
45 |
46 | @ColorRes
47 | private int color = R.color.black20;
48 |
49 | public ShowCaseStep.Builder withTypedRadius(ShowCaseRadius radius) {
50 | this.radius = radius;
51 | return this;
52 | }
53 |
54 | public ShowCaseStep.Builder withTypedPosition(ShowCasePosition position) {
55 | this.position = position;
56 | return this;
57 | }
58 |
59 | public ShowCaseStep.Builder withMessage(String message) {
60 | this.message = message;
61 | return this;
62 | }
63 |
64 | public ShowCaseStep.Builder withColor(@ColorRes int overlayColor) {
65 | color = overlayColor;
66 | return this;
67 | }
68 |
69 | public ShowCaseStep build(Context context) {
70 | checkRequiredFields();
71 |
72 | ShowCaseStep step = new ShowCaseStep();
73 | if (radius == null) {
74 | radius = new Radius(
75 | ViewUtils.convertDpToPx(context, DEFAULT_RADIUS_DP)
76 | );
77 | }
78 | step.position = position;
79 | step.radius = radius;
80 | step.message = message;
81 | step.color = color;
82 |
83 | return step;
84 | }
85 |
86 | private void checkRequiredFields() {
87 | if (position == null) {
88 | throw new IllegalArgumentException(
89 | "position is required field for ShowCaseStep builder"
90 | );
91 | }
92 |
93 | if (message == null) {
94 | throw new IllegalArgumentException(
95 | "message is required field for ShowCaseStep builder"
96 | );
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
25 |
26 |
35 |
36 |
45 |
46 |
55 |
56 |
65 |
66 |
75 |
76 |
85 |
86 |
95 |
96 |
99 |
100 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/ru/dimorinny/showcasesample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package ru.dimorinny.showcasesample;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.AppCompatActivity;
5 | import android.view.View;
6 | import android.widget.Button;
7 | import android.widget.ScrollView;
8 |
9 | import ru.dimorinny.showcasecard.ShowCaseView;
10 | import ru.dimorinny.showcasecard.position.BottomLeft;
11 | import ru.dimorinny.showcasecard.position.BottomRight;
12 | import ru.dimorinny.showcasecard.position.Center;
13 | import ru.dimorinny.showcasecard.position.ShowCasePosition;
14 | import ru.dimorinny.showcasecard.position.TopLeft;
15 | import ru.dimorinny.showcasecard.position.TopLeftToolbar;
16 | import ru.dimorinny.showcasecard.position.TopRight;
17 | import ru.dimorinny.showcasecard.position.TopRightToolbar;
18 | import ru.dimorinny.showcasecard.position.ViewPosition;
19 | import ru.dimorinny.showcasecard.radius.Radius;
20 | import ru.dimorinny.showcasecard.radius.ShowCaseRadius;
21 | import ru.dimorinny.showcasecard.step.ShowCaseStep;
22 | import ru.dimorinny.showcasecard.step.ShowCaseStepDisplayer;
23 | import ru.dimorinny.showcasecard.util.ViewUtils;
24 |
25 | public class MainActivity extends AppCompatActivity {
26 |
27 | private ScrollView scrollView;
28 | private View dummyViewToScrollTo;
29 |
30 | private Button topLeft;
31 | private Button topRight;
32 | private Button bottomLeft;
33 | private Button bottomRight;
34 | private Button topLeftToolbar;
35 | private Button topRightToolbar;
36 | private Button viewPosition;
37 | private Button listOfSteps;
38 |
39 | @Override
40 | protected void onCreate(Bundle savedInstanceState) {
41 | super.onCreate(savedInstanceState);
42 | setContentView(R.layout.activity_main);
43 |
44 | scrollView = findViewById(R.id.activity_main);
45 | dummyViewToScrollTo = findViewById(R.id.dummy_view_to_scroll_to);
46 |
47 | initButtons();
48 |
49 | topLeft.setOnClickListener(v -> showTipWithPosition(new TopLeft()));
50 | topRight.setOnClickListener(v -> showTipWithPosition(new TopRight()));
51 | bottomLeft.setOnClickListener(v -> showTipWithPosition(new BottomLeft()));
52 | bottomRight.setOnClickListener(v -> showTipWithPosition(new BottomRight()));
53 | topLeftToolbar.setOnClickListener(v -> showTipWithPosition(new TopLeftToolbar()));
54 | topRightToolbar.setOnClickListener(v -> showTipWithPosition(new TopRightToolbar()));
55 | viewPosition.setOnClickListener(v -> showTipWithPosition(new ViewPosition(
56 | viewPosition
57 | )));
58 | listOfSteps.setOnClickListener(v -> displayListOfSteps());
59 | }
60 |
61 | private void displayListOfSteps() {
62 | new ShowCaseStepDisplayer.Builder(this)
63 | .withScrollView(scrollView)
64 | .addStep(
65 | new ShowCaseStep.Builder()
66 | .withMessage("This is the center of the screen. Tap anywhere to continue.")
67 | .withTypedPosition(new Center())
68 | .build(this)
69 | )
70 | .addStep(
71 | new ShowCaseStep.Builder()
72 | .withMessage("This is the button you just clicked.")
73 | .withTypedPosition(new ViewPosition(listOfSteps))
74 | .build(this)
75 | )
76 | .addStep(
77 | new ShowCaseStep.Builder()
78 | .withMessage("A dummy item to auto-scroll to.")
79 | .withTypedPosition(new ViewPosition(dummyViewToScrollTo))
80 | .build(this)
81 | )
82 |
83 | .addStep(
84 | new ShowCaseStep.Builder()
85 | .withMessage("We end our showcase at the top button.")
86 | .withTypedPosition(new ViewPosition(topLeft))
87 | .build(this)
88 | )
89 | .addStep(
90 | new ShowCaseStep.Builder()
91 | .withMessage("With custom radius")
92 | .withTypedPosition(new ViewPosition(topLeftToolbar))
93 | .withTypedRadius(new Radius(
94 | ViewUtils.convertDpToPx(
95 | this,
96 | 100
97 | )
98 | ))
99 | .build(this)
100 | )
101 | .addStep(
102 | new ShowCaseStep.Builder()
103 | .withMessage("With custom color")
104 | .withTypedPosition(new ViewPosition(topRightToolbar))
105 | .withColor(R.color.colorPrimaryDark)
106 | .build(this)
107 | )
108 | .build()
109 | .start();
110 | }
111 |
112 | private void initButtons() {
113 | topLeft = findViewById(R.id.top_left);
114 | topRight = findViewById(R.id.top_right);
115 | bottomLeft = findViewById(R.id.bottom_left);
116 | bottomRight = findViewById(R.id.bottom_right);
117 | topLeftToolbar = findViewById(R.id.top_left_toolbar);
118 | topRightToolbar = findViewById(R.id.top_right_toolbar);
119 | viewPosition = findViewById(R.id.view_position);
120 | listOfSteps = findViewById(R.id.list_of_steps);
121 | }
122 |
123 | private void showTipWithPosition(ShowCasePosition position) {
124 | showTip(
125 | position,
126 | new Radius(186F)
127 | );
128 | }
129 |
130 | private void showTip(ShowCasePosition position, ShowCaseRadius radius) {
131 | new ShowCaseView.Builder(MainActivity.this)
132 | .withTypedPosition(position)
133 | .withTypedRadius(radius)
134 | .withContent(
135 | "This is hello world!"
136 | )
137 | .build()
138 | .show(MainActivity.this);
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/showcasecard/src/main/java/ru/dimorinny/showcasecard/step/ShowCaseStepDisplayer.java:
--------------------------------------------------------------------------------
1 | package ru.dimorinny.showcasecard.step;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.support.annotation.NonNull;
6 | import android.support.annotation.Nullable;
7 | import android.support.v4.app.Fragment;
8 | import android.util.TypedValue;
9 | import android.widget.ScrollView;
10 |
11 | import java.util.ArrayList;
12 | import java.util.List;
13 |
14 | import ru.dimorinny.showcasecard.ShowCaseView;
15 | import ru.dimorinny.showcasecard.radius.Radius;
16 | import ru.dimorinny.showcasecard.util.ViewUtils;
17 |
18 | public class ShowCaseStepDisplayer {
19 |
20 | private Context context;
21 |
22 | @Nullable
23 | private Activity activity;
24 | @Nullable
25 | private Fragment fragment;
26 | @Nullable
27 | private ScrollView scrollView;
28 |
29 | private List items = new ArrayList<>();
30 |
31 | @Nullable
32 | private ShowCaseStepScroller showCaseStepScroller;
33 |
34 | private int currentlyDisplayedTipIndex = -1;
35 |
36 | @Nullable
37 | private ShowCaseView showCaseView;
38 |
39 | private ShowCaseStepDisplayer(
40 | @Nullable Activity activity,
41 | @Nullable Fragment fragment,
42 | @Nullable ScrollView scrollView
43 | ) {
44 | this.activity = activity;
45 | this.fragment = fragment;
46 | this.scrollView = scrollView;
47 |
48 | //noinspection ConstantConditions
49 | this.context = activity != null ? activity : fragment.getContext();
50 |
51 | if (scrollView != null) {
52 | showCaseStepScroller = new ShowCaseStepScroller(scrollView);
53 | }
54 | }
55 |
56 | public void start() {
57 | tryShowNextTip();
58 | }
59 |
60 | public void dismiss() {
61 |
62 | if (showCaseView != null) {
63 | showCaseView.hide();
64 | }
65 |
66 | currentlyDisplayedTipIndex = -1;
67 | items.clear();
68 | }
69 |
70 | private void tryShowNextTip() {
71 |
72 | if (!isContextActive()) {
73 | return;
74 | }
75 |
76 | if (currentlyDisplayedTipIndex >= items.size() - 1) {
77 |
78 | // end of tips reached.
79 | dismiss();
80 | } else {
81 |
82 | currentlyDisplayedTipIndex++;
83 | displayTip(items.get(currentlyDisplayedTipIndex));
84 | }
85 | }
86 |
87 | private void displayTip(final ShowCaseStep item) {
88 |
89 | if (item.getPosition().getScrollPosition(scrollView) != null
90 | && showCaseStepScroller != null) {
91 | // try to scroll to the item
92 |
93 | if (showCaseView != null) {
94 | // hide last card, just show dark overlay for now:
95 | showCaseView.hideCard();
96 | }
97 |
98 | // scroll first, after that display the item:
99 | showCaseStepScroller.scrollToShowCaseStepItem(
100 | item,
101 | new ShowCaseStepScroller.OnCompleteListener() {
102 | @Override
103 | public void onComplete() {
104 | doDisplayTip(item);
105 | }
106 | }
107 | );
108 |
109 | } else {
110 | // display item right away
111 | doDisplayTip(item);
112 | }
113 | }
114 |
115 | private void doDisplayTip(ShowCaseStep item) {
116 |
117 | if (!isContextActive()) {
118 | return;
119 | }
120 |
121 | if (showCaseView != null) {
122 | // completely remove old view now:
123 | showCaseView.hide();
124 | }
125 |
126 | final int myTipIndex = currentlyDisplayedTipIndex;
127 | showCaseView = new ShowCaseView.Builder(context)
128 | .withTypedPosition(item.getPosition())
129 | .withTypedRadius(item.getRadius())
130 | .dismissOnTouch(false)
131 | .withTouchListener(new ShowCaseView.TouchListener() {
132 | @Override
133 | public void onTouchEvent() {
134 | if (myTipIndex == currentlyDisplayedTipIndex) {
135 | tryShowNextTip();
136 | }
137 | }
138 | })
139 | .withContent(item.getMessage())
140 | .withColor(item.getColor())
141 | .build();
142 |
143 | if (activity == null) {
144 | showCaseView.show(fragment);
145 | } else {
146 | showCaseView.show(activity);
147 | }
148 | }
149 |
150 | @SuppressWarnings("unused")
151 | public Context getContext() {
152 | return context;
153 | }
154 |
155 | /**
156 | * Adds on item to the list of items to display.
157 | *
158 | * @param item
159 | */
160 | @SuppressWarnings("unused")
161 | public void addStep(ShowCaseStep item) {
162 | items.add(item);
163 | }
164 |
165 | /**
166 | * Sets the list of items to display.
167 | *
168 | * @param items
169 | */
170 | @SuppressWarnings("unused")
171 | public void setSteps(List items) {
172 | this.items = items;
173 | }
174 |
175 | /**
176 | * Returns true if the attached Context is still active / not shutting down.
177 | */
178 | private boolean isContextActive() {
179 | if (fragment != null) {
180 | return fragment.isAdded();
181 | } else if (activity != null) {
182 | return !activity.isFinishing();
183 | }
184 | return true;
185 | }
186 |
187 | public static class Builder {
188 |
189 | @Nullable
190 | private Activity activity;
191 | @Nullable
192 | private Fragment fragment;
193 | /**
194 | * ScrollView used on all {@link ShowCaseStep}'s that used to scroll to the View
195 | * on activation.
196 | */
197 | @Nullable
198 | private ScrollView scrollView;
199 |
200 | private List items = new ArrayList<>();
201 |
202 | @SuppressWarnings("unused")
203 | public Builder(@NonNull Fragment fragment) {
204 | this.fragment = fragment;
205 | }
206 |
207 | @SuppressWarnings("unused")
208 | public Builder(@NonNull Activity activity) {
209 | this.activity = activity;
210 | }
211 |
212 | /**
213 | * ScrollView used on all {@link ShowCaseStep}'s to scroll to the View
214 | * on activation.
215 | */
216 | public Builder withScrollView(@Nullable ScrollView scrollView) {
217 | this.scrollView = scrollView;
218 | return this;
219 | }
220 |
221 | /**
222 | * Adds on item to the list of items to display.
223 | *
224 | * @param item
225 | */
226 | @SuppressWarnings("unused")
227 | public Builder addStep(ShowCaseStep item) {
228 | items.add(item);
229 | return this;
230 | }
231 |
232 | @SuppressWarnings("unused")
233 | public ShowCaseStepDisplayer build() {
234 |
235 | ShowCaseStepDisplayer stepController =
236 | new ShowCaseStepDisplayer(activity, fragment, scrollView);
237 |
238 | stepController.setSteps(items);
239 |
240 | return stepController;
241 | }
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/showcasecard/src/main/java/ru/dimorinny/showcasecard/ShowCaseView.java:
--------------------------------------------------------------------------------
1 | package ru.dimorinny.showcasecard;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ObjectAnimator;
6 | import android.annotation.SuppressLint;
7 | import android.app.Activity;
8 | import android.content.Context;
9 | import android.content.res.Configuration;
10 | import android.graphics.Canvas;
11 | import android.graphics.Paint;
12 | import android.graphics.PointF;
13 | import android.graphics.PorterDuff;
14 | import android.graphics.PorterDuffXfermode;
15 | import android.support.annotation.ColorRes;
16 | import android.support.annotation.DrawableRes;
17 | import android.support.annotation.Nullable;
18 | import android.support.v4.app.Fragment;
19 | import android.support.v4.content.ContextCompat;
20 | import android.view.Gravity;
21 | import android.view.LayoutInflater;
22 | import android.view.MotionEvent;
23 | import android.view.View;
24 | import android.view.ViewGroup;
25 | import android.widget.FrameLayout;
26 | import android.widget.TextView;
27 |
28 | import java.util.ArrayList;
29 | import java.util.List;
30 |
31 | import ru.dimorinny.showcasecard.position.Position;
32 | import ru.dimorinny.showcasecard.position.ShowCasePosition;
33 | import ru.dimorinny.showcasecard.position.ViewPosition;
34 | import ru.dimorinny.showcasecard.radius.Radius;
35 | import ru.dimorinny.showcasecard.radius.ShowCaseRadius;
36 | import ru.dimorinny.showcasecard.radius.ViewRadius;
37 | import ru.dimorinny.showcasecard.util.ActivityUtils;
38 | import ru.dimorinny.showcasecard.util.MeasuredUtils;
39 | import ru.dimorinny.showcasecard.util.NavigationBarUtils;
40 | import ru.dimorinny.showcasecard.util.ViewUtils;
41 |
42 | public class ShowCaseView extends FrameLayout {
43 |
44 | private static final float MAX_CARD_WIDTH = 0.40F;
45 | private static final String CARD_ANIMATION_PROPERTY = "translationY";
46 | private static final long CARD_ANIMATION_DURATION = 200L;
47 | private static final long VIEW_FADE_IN_DURATION = 200L;
48 | private static final long ANIMATION_START_DELAY = 200L;
49 |
50 | private final int CARD_PADDING_VERTICAL = ViewUtils.convertDpToPx(this, 16);
51 | private final int CARD_PADDING_HORIZONTAL = ViewUtils.convertDpToPx(this, 8);
52 | private final long CARD_TO_ARROW_OFFSET = ViewUtils.convertDpToPx(this, 25);
53 | private final long CARD_MIN_MARGIN = ViewUtils.convertDpToPx(this, 14);
54 | private final long CARD_ANIMATION_OFFSET = ViewUtils.convertDpToPx(this, 16);
55 |
56 | private final Paint overlayPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
57 | private final Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
58 |
59 | private ShowCaseView.DismissListener dismissListener;
60 |
61 | private boolean hideAnimationPerforming = false;
62 |
63 | private PointF position;
64 | private ShowCasePosition typedPosition;
65 |
66 | private float radius = -1F;
67 | private ShowCaseRadius typedRadius;
68 |
69 | private int cardRightOffset = 0;
70 | private int cardLeftOffset = 0;
71 |
72 | private TextView cardContent;
73 |
74 | /**
75 | * True to dismiss the card on touch/click. True by default.
76 | */
77 | private boolean dismissOnTouch = true;
78 |
79 | @Nullable
80 | private TouchListener touchListener;
81 | /**
82 | * True to hide the card view (dark overlay will still be shown).
83 | */
84 | private boolean hideCard = false;
85 |
86 | public ShowCaseView(Context context) {
87 | super(context);
88 |
89 | initView();
90 | initPaints();
91 | }
92 |
93 | private void initView() {
94 | setAlpha(0F);
95 | setWillNotDraw(false);
96 | setLayerType(View.LAYER_TYPE_SOFTWARE, null);
97 | }
98 |
99 | private void initPaints() {
100 | overlayPaint.setStyle(Paint.Style.FILL);
101 | circlePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
102 | }
103 |
104 | private void setContent(TextView contentView, String text) {
105 | cardContent = contentView;
106 | cardContent.setText(text);
107 | }
108 |
109 | private boolean isTouchInCircle(MotionEvent touchEvent) {
110 | float dx = Math.abs(touchEvent.getX() - position.x);
111 | float dy = Math.abs(touchEvent.getY() - position.y);
112 | return dx <= radius && dy <= radius;
113 | }
114 |
115 | private void removeFromWindow() {
116 | if (getParent() != null && getParent() instanceof ViewGroup) {
117 | ((ViewGroup) getParent()).removeView(this);
118 | }
119 | }
120 |
121 | private void initCardOffsets(Activity activity) {
122 | if (ActivityUtils.getOrientation(activity) == Configuration.ORIENTATION_LANDSCAPE) {
123 | switch (NavigationBarUtils.navigationBarPosition(activity)) {
124 | case LEFT:
125 | cardLeftOffset = NavigationBarUtils.navigationBarHeight(activity);
126 | break;
127 | case RIGHT:
128 | cardRightOffset = NavigationBarUtils.navigationBarHeight(activity);
129 | break;
130 | default:
131 | break;
132 | }
133 | }
134 | }
135 |
136 | private int getCardWidth() {
137 | return (int) (getWidth() * (MAX_CARD_WIDTH + getPositionXPercentageDistanceToCenter() / 3));
138 | }
139 |
140 | private double getPositionXPercentageDistanceToCenter() {
141 | return Math.abs(position.x - getWidth() / 2.0) / getWidth();
142 | }
143 |
144 | private int getCardGravity() {
145 | int vertical = (position.y <= getHeight() / 2) ? Gravity.TOP : Gravity.BOTTOM;
146 | int horizontal = (position.x <= getWidth() / 2) ? Gravity.START : Gravity.END;
147 | return vertical | horizontal;
148 | }
149 |
150 | private boolean cardFromTop() {
151 | return position.y > getHeight() / 2;
152 | }
153 |
154 | private boolean cardFromBottom() {
155 | return position.y <= getHeight() / 2;
156 | }
157 |
158 | private boolean cardFromRight() {
159 | return position.x <= getWidth() / 2;
160 | }
161 |
162 | private boolean cardFromLeft() {
163 | return position.x > getWidth() / 2;
164 | }
165 |
166 | private
167 | @DrawableRes
168 | int getCardBackgroundDrawable() {
169 | boolean fromBottom = cardFromBottom();
170 | boolean fromLeft = cardFromLeft();
171 | boolean fromRight = cardFromRight();
172 | boolean fromTop = cardFromTop();
173 |
174 | if (fromBottom && fromLeft) {
175 | return R.drawable.background_showcase_right_top;
176 | } else if (fromBottom && fromRight) {
177 | return R.drawable.background_showcase_left_top;
178 | } else if (fromTop && fromLeft) {
179 | return R.drawable.background_showcase_right_bottom;
180 | } else {
181 | return R.drawable.background_showcase_left_bottom;
182 | }
183 | }
184 |
185 | private int getCardMarginTop() {
186 | if (cardFromBottom()) {
187 | if (position.y + radius < CARD_MIN_MARGIN) {
188 | return (int) CARD_MIN_MARGIN;
189 | } else {
190 | return (int) (position.y + radius + CARD_MIN_MARGIN);
191 | }
192 | } else {
193 | return 0;
194 | }
195 | }
196 |
197 | private int getCardMarginBottom() {
198 | if (cardFromTop()) {
199 | if (position.y - radius >= getHeight() - CARD_MIN_MARGIN) {
200 | return (int) CARD_MIN_MARGIN;
201 | } else {
202 | return (int) (getHeight() - position.y + radius + CARD_MIN_MARGIN);
203 | }
204 | } else {
205 | return 0;
206 | }
207 | }
208 |
209 | private int getCardMarginLeft() {
210 | if (cardFromRight()) {
211 | if (position.x <= CARD_MIN_MARGIN + CARD_TO_ARROW_OFFSET + cardLeftOffset) {
212 | return (int) (cardLeftOffset + CARD_MIN_MARGIN);
213 | } else {
214 | return (int) (position.x - CARD_TO_ARROW_OFFSET);
215 | }
216 | } else {
217 | return 0;
218 | }
219 | }
220 |
221 | private int getCardMarginRight() {
222 | if (cardFromLeft()) {
223 | if (position.x >= getWidth() - CARD_MIN_MARGIN - CARD_TO_ARROW_OFFSET - cardRightOffset) {
224 | return (int) (cardRightOffset + CARD_MIN_MARGIN);
225 | } else {
226 | return (int) (getWidth() - CARD_TO_ARROW_OFFSET - position.x);
227 | }
228 | } else {
229 | return 0;
230 | }
231 | }
232 |
233 | private void configureCard(ViewGroup card) {
234 | cardContent.setMaxWidth(getCardWidth());
235 | cardContent.setPadding(
236 | CARD_PADDING_VERTICAL,
237 | CARD_PADDING_HORIZONTAL,
238 | CARD_PADDING_VERTICAL,
239 | CARD_PADDING_HORIZONTAL
240 | );
241 | cardContent.setLayoutParams(new FrameLayout.LayoutParams(
242 | LayoutParams.WRAP_CONTENT,
243 | LayoutParams.WRAP_CONTENT
244 | ));
245 |
246 | card.setBackgroundResource(getCardBackgroundDrawable());
247 | card.addView(cardContent);
248 |
249 | FrameLayout.LayoutParams cardLayoutParams = generateDefaultLayoutParams();
250 | cardLayoutParams.width = LayoutParams.WRAP_CONTENT;
251 | cardLayoutParams.height = LayoutParams.WRAP_CONTENT;
252 | cardLayoutParams.gravity = getCardGravity();
253 | cardLayoutParams.leftMargin = getCardMarginLeft();
254 | cardLayoutParams.topMargin = getCardMarginTop();
255 | cardLayoutParams.rightMargin = getCardMarginRight();
256 | cardLayoutParams.bottomMargin = getCardMarginBottom();
257 | card.setLayoutParams(cardLayoutParams);
258 | }
259 |
260 | private void showAfterMeasured(
261 | final Activity activity,
262 | final ViewGroup container,
263 | View measuredView
264 | ) {
265 | MeasuredUtils.afterOrAlreadyMeasured(measuredView, new MeasuredUtils.OnMeasuredHandler() {
266 | @Override
267 | public void onMeasured() {
268 |
269 | initCardOffsets(activity);
270 |
271 | List viewsToMeasure = new ArrayList<>();
272 |
273 | if (typedPosition != null && typedPosition instanceof ViewPosition) {
274 | viewsToMeasure.add(
275 | ((ViewPosition) typedPosition).getView()
276 | );
277 | }
278 |
279 | if (typedRadius != null && typedRadius instanceof ViewRadius) {
280 | viewsToMeasure.add(
281 | ((ViewRadius) typedRadius).getView()
282 | );
283 | }
284 |
285 | if (!viewsToMeasure.isEmpty()) {
286 | MeasuredUtils.afterOrAlreadyMeasuredViews(
287 | viewsToMeasure,
288 | new MeasuredUtils.OnMeasuredHandler() {
289 | @Override
290 | public void onMeasured() {
291 | position = typedPosition.getPosition(activity);
292 | radius = typedRadius.getRadius();
293 | show(container);
294 | }
295 | }
296 | );
297 | } else {
298 | position = typedPosition.getPosition(activity);
299 | radius = typedRadius.getRadius();
300 | show(container);
301 | }
302 | }
303 | });
304 | }
305 |
306 | /**
307 | * Hides the current card. Will still display the dark overlay still.
308 | */
309 | public void hideCard() {
310 | ((View) cardContent.getParent()).setVisibility(View.GONE);
311 | hideCard = true;
312 | invalidate();
313 | }
314 |
315 | /**
316 | * True to dismiss the card on touch/click. True by default.
317 | */
318 | public void setDismissOnTouch(boolean dismissOnTouch) {
319 | this.dismissOnTouch = dismissOnTouch;
320 | }
321 |
322 |
323 | @Override
324 | public void onDraw(Canvas canvas) {
325 | super.onDraw(canvas);
326 |
327 | canvas.drawPaint(overlayPaint);
328 |
329 | if (!hideCard) {
330 | canvas.drawCircle(position.x, position.y, radius, circlePaint);
331 | }
332 | }
333 |
334 | @Override
335 | public boolean onTouchEvent(MotionEvent event) {
336 |
337 | if (touchListener != null) {
338 | touchListener.onTouchEvent();
339 | }
340 |
341 | if (dismissOnTouch) {
342 | if (dismissListener != null) {
343 | dismissListener.onDismiss();
344 | }
345 | hide();
346 | }
347 | return !isTouchInCircle(event);
348 | }
349 |
350 | public void show(final ViewGroup container) {
351 | if (ViewUtils.findViewWithType(container, ShowCaseView.class) == null) {
352 | container.addView(this);
353 |
354 | final FrameLayout card = new FrameLayout(getContext());
355 | MeasuredUtils.afterOrAlreadyMeasured(
356 | card,
357 | new MeasuredUtils.OnMeasuredHandler() {
358 | @Override
359 | public void onMeasured() {
360 | configureCard(card);
361 |
362 | ObjectAnimator animator = ObjectAnimator.ofFloat(
363 | this,
364 | CARD_ANIMATION_PROPERTY,
365 | CARD_ANIMATION_OFFSET,
366 | 0F
367 | );
368 |
369 | animator.setStartDelay(ANIMATION_START_DELAY);
370 | animator.setDuration(CARD_ANIMATION_DURATION);
371 |
372 | animator.start();
373 | }
374 | }
375 | );
376 |
377 | addView(card);
378 | animate()
379 | .setStartDelay(ANIMATION_START_DELAY)
380 | .setDuration(VIEW_FADE_IN_DURATION)
381 | .alpha(1F);
382 | }
383 | }
384 |
385 | public void show(Activity activity) {
386 | ViewGroup container = (ViewGroup) activity.getWindow().getDecorView();
387 | showAfterMeasured(
388 | activity,
389 | container,
390 | container
391 | );
392 | }
393 |
394 | public void show(Fragment fragment) {
395 | showAfterMeasured(
396 | fragment.getActivity(),
397 | (ViewGroup) fragment.getActivity().getWindow().getDecorView(),
398 | fragment.getView()
399 | );
400 | }
401 |
402 | public void hide() {
403 | if (!hideAnimationPerforming) {
404 |
405 | animate()
406 | .setListener(new AnimatorListenerAdapter() {
407 | @Override
408 | public void onAnimationEnd(Animator animation) {
409 | hideAnimationPerforming = false;
410 | removeFromWindow();
411 | }
412 |
413 | @Override
414 | public void onAnimationStart(Animator animation) {
415 | super.onAnimationStart(animation);
416 | hideAnimationPerforming = true;
417 | }
418 | })
419 | .alpha(0F);
420 | }
421 | }
422 |
423 | public interface DismissListener {
424 |
425 | void onDismiss();
426 | }
427 |
428 | public interface TouchListener {
429 |
430 | void onTouchEvent();
431 | }
432 |
433 | public static class Builder {
434 |
435 | private Context context;
436 |
437 | @ColorRes
438 | private int color = R.color.black20;
439 | private ShowCaseRadius radius = new Radius(128F);
440 | private TextView contentView;
441 | private String contentText;
442 | private boolean dismissOnTouch = true;
443 | @Nullable
444 | private TouchListener touchListener;
445 | private DismissListener dismissListener;
446 | private ShowCasePosition position = new Position(
447 | new PointF(0F, 0F)
448 | );
449 |
450 | public Builder(Context context) {
451 | this.context = context;
452 | }
453 |
454 | public Builder withTypedRadius(ShowCaseRadius radius) {
455 | this.radius = radius;
456 | return this;
457 | }
458 |
459 | public Builder withTypedPosition(ShowCasePosition position) {
460 | this.position = position;
461 | return this;
462 | }
463 |
464 | public Builder withDismissListener(DismissListener listener) {
465 | this.dismissListener = listener;
466 | return this;
467 | }
468 |
469 | public Builder withColor(@ColorRes int overlayColor) {
470 | color = overlayColor;
471 | return this;
472 | }
473 |
474 | /**
475 | * True to dismiss the card on touch/click. True by default.
476 | */
477 | public Builder dismissOnTouch(boolean dismissOnTouch) {
478 | this.dismissOnTouch = dismissOnTouch;
479 | return this;
480 | }
481 |
482 | public Builder withTouchListener(TouchListener touchListener) {
483 | this.touchListener = touchListener;
484 | return this;
485 | }
486 |
487 | @SuppressLint("InflateParams")
488 | public Builder withContent(String cardText) {
489 | this.contentView = (TextView) LayoutInflater.from(context).inflate(
490 | R.layout.item_show_case_content,
491 | null
492 | );
493 | this.contentText = cardText;
494 |
495 | return this;
496 | }
497 |
498 | public ShowCaseView build() {
499 | ShowCaseView view = new ShowCaseView(context);
500 | view.dismissListener = this.dismissListener;
501 | view.typedRadius = this.radius;
502 | view.typedPosition = this.position;
503 | view.dismissOnTouch = this.dismissOnTouch;
504 | view.touchListener = this.touchListener;
505 | view.overlayPaint.setColor(ContextCompat.getColor(context, this.color));
506 |
507 | if (this.contentView != null && contentText != null) {
508 | view.setContent(this.contentView, this.contentText);
509 | }
510 |
511 | return view;
512 | }
513 | }
514 | }
515 |
--------------------------------------------------------------------------------