├── .github
└── images
│ └── sample_screenshot.png
├── .gitignore
├── .idea
├── .gitignore
├── compiler.xml
└── vcs.xml
├── LICENSE
├── README.md
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
├── publish.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── github
│ └── skgmn
│ └── composetooltip
│ ├── AnchorEdge.kt
│ ├── EdgePosition.kt
│ ├── TooltipConstraintLayout.kt
│ ├── TooltipPopup.kt
│ └── TooltipStyle.kt
├── sample
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── github
│ │ └── skgmn
│ │ └── composetooltip
│ │ └── sample
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── skgmn
│ │ │ └── composetooltip
│ │ │ └── sample
│ │ │ ├── ExampleConstraintLayout.kt
│ │ │ ├── ExamplePopup.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainScreen.kt
│ │ │ └── ui
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Shape.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable-xxxhdpi
│ │ └── dummy.png
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── github
│ └── skgmn
│ └── composetooltip
│ └── sample
│ └── ExampleUnitTest.kt
└── settings.gradle
/.github/images/sample_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/.github/images/sample_screenshot.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 skgmn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Setup
4 |
5 | ```gradle
6 | implementation "com.github.skgmn:composetooltip:0.2.0"
7 | ```
8 |
9 | # Tooltip method
10 |
11 | `Tooltip` can be either used inside ConstraintLayout or displayed as a popup.
12 |
13 | # ConstraintLayout
14 |
15 | ## Basic example
16 |
17 | ```kotlin
18 | ConstraintLayout {
19 | val someImage = createRef()
20 | Image(
21 | painter = painterResource(R.drawable.some_image),
22 | contentDescription = "Some image"
23 | modifier = Modifier.constrainAs(someImage) {
24 | // Place it where you want
25 | }
26 | )
27 | Tooltip(
28 | anchor = someImage,
29 | anchorEdge = AnchorEdge.Top,
30 | ) {
31 | Text("This is my image!")
32 | }
33 | }
34 | ```
35 |
36 | ## Method signatures
37 |
38 | ```kotlin
39 | fun ConstraintLayoutScope.Tooltip(
40 | anchor: ConstrainedLayoutReference,
41 | anchorEdge: AnchorEdge,
42 | modifier: Modifier = Modifier,
43 | tooltipStyle: TooltipStyle = rememberTooltipStyle(),
44 | tipPosition: EdgePosition = remember { EdgePosition() },
45 | anchorPosition: EdgePosition = remember { EdgePosition() },
46 | margin: Dp = 8.dp,
47 | content: @Composable RowScope.() -> Unit
48 | )
49 | ```
50 |
51 | * `anchor` - `ConstrainedLayoutReference` to locate this tooltip nearby
52 | * `anchorEdge` - Can be either of `AnchorEdge.Start`, `AnchorEdge.Top`, `AnchorEdge.End`, or `AnchorEdge.Bottom`
53 | * `tooltipStyle` - Style for tooltip. Can be created by `rememberTooltipStyle`
54 | * `tipPosition` - Tip position relative to balloon
55 | * `anchorPosition` - Position on the `anchor`'s edge where the tip points out.
56 | * `margin` - Margin between tip and `anchor`
57 | * `content` - Content inside balloon. Typically `Text`.
58 |
59 | `Tooltip` also supports enter/exit transition using `AnimatedVisibility`. Because `AnimatedVisibility` is experimental, this method is also experimental.
60 |
61 | ```kotlin
62 | fun ConstraintLayoutScope.Tooltip(
63 | anchor: ConstrainedLayoutReference,
64 | anchorEdge: AnchorEdge,
65 | enterTransition: EnterTransition,
66 | exitTransition: ExitTransition,
67 | modifier: Modifier = Modifier,
68 | visible: Boolean = true,
69 | tooltipStyle: TooltipStyle = rememberTooltipStyle(),
70 | tipPosition: EdgePosition = remember { EdgePosition() },
71 | anchorPosition: EdgePosition = remember { EdgePosition() },
72 | margin: Dp = 8.dp,
73 | content: @Composable RowScope.() -> Unit
74 | )
75 | ```
76 |
77 | * `enterTransition` - `EnterTransition` to be applied when the `visible` becomes true. Types of `EnterTransition` are listed [here](https://developer.android.com/jetpack/compose/animation#entertransition).
78 | * `exitTransition` - `ExitTransition` to be applied when the `visible` becomes false. Types of `ExitTransition` are listed [here](https://developer.android.com/jetpack/compose/animation#exittransition).
79 |
80 | # Popup
81 |
82 | ```kotlin
83 | Box {
84 | // Some other composables here
85 |
86 | // This Box is a conatiner for anchoring
87 | Box {
88 | Image(
89 | painter = painterResource(R.drawable.some_image),
90 | contentDescription = "Some image"
91 | )
92 | Tooltip(
93 | anchorEdge = AnchorEdge.Top,
94 | ) {
95 | Text("This is my image!")
96 | }
97 | }
98 | }
99 | ```
100 |
101 | When `Tooltip` is being displayed as a popup, an anchor and `Tooltip` should be put altogether inside one composable.
102 |
103 | ## Method signatures
104 |
105 | ```kotlin
106 | fun Tooltip(
107 | anchorEdge: AnchorEdge,
108 | modifier: Modifier = Modifier,
109 | tooltipStyle: TooltipStyle = rememberTooltipStyle(),
110 | tipPosition: EdgePosition = remember { EdgePosition() },
111 | anchorPosition: EdgePosition = remember { EdgePosition() },
112 | margin: Dp = 8.dp,
113 | onDismissRequest: (() -> Unit)? = null,
114 | properties: PopupProperties = remember { PopupProperties() },
115 | content: @Composable RowScope.() -> Unit
116 | )
117 | ```
118 |
119 | * `onDismissRequest` - Executes when the user clicks outside of the tooltip.
120 | * `properties` - `PopupProperties` for further customization of this tooltip's behavior.
121 |
122 | Other parameteres are same as for ConstraintLayout.
123 |
124 | There is also an another version of supporting transition.
125 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext {
4 | compose_version = '1.0.2'
5 | sdk_version = 30
6 | }
7 | repositories {
8 | google()
9 | mavenCentral()
10 | }
11 | dependencies {
12 | classpath "com.android.tools.build:gradle:7.0.2"
13 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21'
14 |
15 | // NOTE: Do not place your application dependencies here; they belong
16 | // in the individual module build.gradle files
17 | }
18 | }
19 |
20 | task clean(type: Delete) {
21 | delete rootProject.buildDir
22 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Sep 09 09:44:21 KST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | compileSdk sdk_version
8 |
9 | defaultConfig {
10 | minSdk 21
11 | targetSdk sdk_version
12 |
13 | consumerProguardFiles "consumer-rules.pro"
14 | }
15 |
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | compileOptions {
23 | sourceCompatibility JavaVersion.VERSION_1_8
24 | targetCompatibility JavaVersion.VERSION_1_8
25 | }
26 | kotlinOptions {
27 | jvmTarget = '1.8'
28 | useIR = true
29 | }
30 | buildFeatures {
31 | compose true
32 | }
33 | composeOptions {
34 | kotlinCompilerExtensionVersion compose_version
35 | }
36 | packagingOptions {
37 | resources {
38 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
39 | }
40 | }
41 | }
42 |
43 | dependencies {
44 | implementation "androidx.compose.ui:ui:$compose_version"
45 | implementation "androidx.compose.material:material:$compose_version"
46 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
47 | implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-beta02"
48 | implementation 'com.google.android.material:material:1.4.0'
49 | }
50 |
51 | apply from: "publish.gradle"
--------------------------------------------------------------------------------
/library/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/library/consumer-rules.pro
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/library/publish.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "maven-publish"
2 | apply plugin: "signing"
3 |
4 | task sourceJar(type: Jar) {
5 | from android.sourceSets.main.java.srcDirs
6 | classifier "sources"
7 | }
8 |
9 | afterEvaluate {
10 | publishing {
11 | repositories {
12 | maven {
13 | url "https://oss.sonatype.org/service/local/staging/deploy/maven2"
14 | credentials {
15 | username findProperty("OSSRH_ID") ?: ""
16 | password findProperty("OSSRH_PASSWORD") ?: ""
17 | }
18 | }
19 | }
20 | publications {
21 | release(MavenPublication) {
22 | from components.release
23 |
24 | groupId = 'com.github.skgmn'
25 | artifactId = 'composetooltip'
26 | version = "0.2.0"
27 |
28 | artifact(sourceJar)
29 |
30 | pom {
31 | name = "ComposeTooltip"
32 | description = "Tooltip library for Jetpack Compose"
33 | url = "https://github.com/skgmn/ComposeTooltip"
34 | licenses {
35 | license {
36 | name = "MIT License"
37 | url = "http://opensource.org/licenses/MIT"
38 | }
39 | }
40 | developers {
41 | developer {
42 | id = "skgmn"
43 | name = "skgmn"
44 | }
45 | }
46 | scm {
47 | connection = "scm:git@github.com:skgmn/ComposeTooltip.git"
48 | developerConnection = "scm:git@github.com:skgmn/ComposeTooltip.git"
49 | url = "https://github.com/skgmn/ComposeTooltip"
50 | }
51 | }
52 | }
53 | }
54 | }
55 | signing {
56 | sign publishing.publications.release
57 | }
58 | }
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/library/src/main/java/com/github/skgmn/composetooltip/AnchorEdge.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.geometry.Size
7 | import androidx.compose.ui.graphics.Path
8 | import androidx.compose.ui.unit.*
9 | import androidx.constraintlayout.compose.ConstrainScope
10 | import androidx.constraintlayout.compose.ConstrainedLayoutReference
11 | import androidx.constraintlayout.compose.ConstraintLayout
12 | import kotlin.math.roundToInt
13 |
14 | abstract class AnchorEdge {
15 | @Composable
16 | internal abstract fun TooltipContainer(
17 | modifier: Modifier,
18 | cornerRadius: Dp,
19 | tipPosition: EdgePosition,
20 | tip: @Composable () -> Unit,
21 | content: @Composable () -> Unit
22 | )
23 |
24 | internal open fun calculatePopupPosition(
25 | density: Density,
26 | tooltipStyle: TooltipStyle,
27 | tipPosition: EdgePosition,
28 | anchorPosition: EdgePosition,
29 | margin: Dp,
30 | anchorBounds: IntRect,
31 | layoutDirection: LayoutDirection,
32 | popupContentSize: IntSize
33 | ): IntOffset = IntOffset(0, 0)
34 |
35 | internal abstract fun ConstrainScope.outside(anchor: ConstrainedLayoutReference, margin: Dp)
36 | internal abstract fun ConstrainScope.align(anchor: ConstrainedLayoutReference, bias: Float)
37 | internal abstract fun ConstrainScope.nextTo(anchor: ConstrainedLayoutReference, margin: Dp)
38 | internal abstract fun ConstrainScope.beforeTo(anchor: ConstrainedLayoutReference, margin: Dp)
39 |
40 | internal abstract fun selectWidth(width: Dp, height: Dp): Dp
41 | internal abstract fun selectHeight(width: Dp, height: Dp): Dp
42 | internal abstract fun Modifier.minSize(tooltipStyle: TooltipStyle): Modifier
43 | internal abstract fun Path.drawTip(size: Size, layoutDirection: LayoutDirection)
44 |
45 | abstract class VerticalAnchorEdge : AnchorEdge() {
46 | override fun ConstrainScope.align(anchor: ConstrainedLayoutReference, bias: Float) {
47 | linkTo(anchor.top, anchor.bottom, bias = bias)
48 | }
49 |
50 | override fun ConstrainScope.nextTo(anchor: ConstrainedLayoutReference, margin: Dp) {
51 | top.linkTo(anchor.bottom, margin)
52 | }
53 |
54 | override fun ConstrainScope.beforeTo(anchor: ConstrainedLayoutReference, margin: Dp) {
55 | bottom.linkTo(anchor.top, margin)
56 | }
57 |
58 | override fun selectWidth(width: Dp, height: Dp): Dp {
59 | return min(width, height)
60 | }
61 |
62 | override fun selectHeight(width: Dp, height: Dp): Dp {
63 | return max(width, height)
64 | }
65 |
66 | override fun Modifier.minSize(tooltipStyle: TooltipStyle): Modifier = with(tooltipStyle) {
67 | return heightIn(min = cornerRadius * 2 + max(tipWidth, tipHeight))
68 | }
69 |
70 | protected fun calculatePopupPositionY(
71 | density: Density,
72 | anchorBounds: IntRect,
73 | anchorPosition: EdgePosition,
74 | tooltipStyle: TooltipStyle,
75 | tipPosition: EdgePosition,
76 | popupContentSize: IntSize
77 | ): Float = with(density) {
78 | val contactPointY = anchorBounds.top +
79 | anchorBounds.height * anchorPosition.percent +
80 | anchorPosition.offset.toPx()
81 | val tangentHeight = (tooltipStyle.cornerRadius * 2 +
82 | tipPosition.offset.absoluteValue * 2 +
83 | max(tooltipStyle.tipWidth, tooltipStyle.tipHeight)).toPx()
84 | val tangentY = contactPointY - tangentHeight / 2
85 | val tipMarginY = (popupContentSize.height - tangentHeight) * tipPosition.percent
86 | val y = tangentY - tipMarginY
87 | return y
88 | }
89 | }
90 |
91 | abstract class HorizontalAnchorEdge : AnchorEdge() {
92 | override fun ConstrainScope.align(anchor: ConstrainedLayoutReference, bias: Float) {
93 | linkTo(anchor.start, anchor.end, bias = bias)
94 | }
95 |
96 | override fun ConstrainScope.nextTo(anchor: ConstrainedLayoutReference, margin: Dp) {
97 | start.linkTo(anchor.end, margin)
98 | }
99 |
100 | override fun ConstrainScope.beforeTo(anchor: ConstrainedLayoutReference, margin: Dp) {
101 | end.linkTo(anchor.start, margin)
102 | }
103 |
104 | override fun selectWidth(width: Dp, height: Dp): Dp {
105 | return max(width, height)
106 | }
107 |
108 | override fun selectHeight(width: Dp, height: Dp): Dp {
109 | return min(width, height)
110 | }
111 |
112 | override fun Modifier.minSize(tooltipStyle: TooltipStyle): Modifier = with(tooltipStyle) {
113 | return widthIn(min = cornerRadius * 2 + max(tipWidth, tipHeight))
114 | }
115 |
116 | protected fun calculatePopupPositionX(
117 | density: Density,
118 | layoutDirection: LayoutDirection,
119 | anchorBounds: IntRect,
120 | anchorPosition: EdgePosition,
121 | tooltipStyle: TooltipStyle,
122 | tipPosition: EdgePosition,
123 | popupContentSize: IntSize
124 | ): Float = with(density) {
125 | val contactPointX = if (layoutDirection == LayoutDirection.Ltr) {
126 | anchorBounds.left +
127 | anchorBounds.width * anchorPosition.percent +
128 | anchorPosition.offset.toPx()
129 | } else {
130 | anchorBounds.right -
131 | anchorBounds.width * anchorPosition.percent -
132 | anchorPosition.offset.toPx()
133 | }
134 | val tangentWidth = (tooltipStyle.cornerRadius * 2 +
135 | tipPosition.offset.absoluteValue * 2 +
136 | max(tooltipStyle.tipWidth, tooltipStyle.tipHeight)).toPx()
137 | val tangentLeft = contactPointX - tangentWidth / 2
138 | val tipMarginLeft = (popupContentSize.width - tangentWidth) *
139 | if (layoutDirection == LayoutDirection.Ltr) {
140 | tipPosition.percent
141 | } else {
142 | 1f - tipPosition.percent
143 | }
144 | val x = tangentLeft - tipMarginLeft
145 | return x
146 | }
147 | }
148 |
149 | object Start : VerticalAnchorEdge() {
150 | override fun ConstrainScope.outside(anchor: ConstrainedLayoutReference, margin: Dp) {
151 | end.linkTo(anchor.start, margin)
152 | }
153 |
154 | override fun Path.drawTip(size: Size, layoutDirection: LayoutDirection) {
155 | when (layoutDirection) {
156 | LayoutDirection.Ltr -> {
157 | moveTo(0f, 0f)
158 | lineTo(size.width, size.height / 2f)
159 | lineTo(0f, size.height)
160 | lineTo(0f, 0f)
161 | }
162 | LayoutDirection.Rtl -> {
163 | moveTo(size.width, 0f)
164 | lineTo(0f, size.height / 2f)
165 | lineTo(size.width, size.height)
166 | lineTo(size.width, 0f)
167 | }
168 | }
169 | }
170 |
171 | @Composable
172 | override fun TooltipContainer(
173 | modifier: Modifier,
174 | cornerRadius: Dp,
175 | tipPosition: EdgePosition,
176 | tip: @Composable () -> Unit,
177 | content: @Composable () -> Unit
178 | ) {
179 | val tipPositionOffset = tipPosition.offset
180 | ConstraintLayout(modifier = modifier) {
181 | val (contentContainer, tipContainer) = createRefs()
182 | Box(
183 | modifier = Modifier
184 | .constrainAs(contentContainer) {
185 | start.linkTo(parent.start)
186 | top.linkTo(parent.top)
187 | bottom.linkTo(parent.bottom)
188 | }
189 | .padding(
190 | top = if (tipPositionOffset < 0.dp) tipPositionOffset * -2 else 0.dp,
191 | bottom = if (tipPositionOffset > 0.dp) tipPositionOffset * 2 else 0.dp
192 | )
193 | ) {
194 | content()
195 | }
196 | val tipPadding = cornerRadius + tipPositionOffset.absoluteValue
197 | Box(
198 | modifier = Modifier
199 | .constrainAs(tipContainer) {
200 | linkTo(
201 | contentContainer.top,
202 | contentContainer.bottom,
203 | bias = tipPosition.percent
204 | )
205 | start.linkTo(contentContainer.end)
206 | }
207 | .padding(top = tipPadding, bottom = tipPadding)
208 | ) {
209 | tip()
210 | }
211 | }
212 | }
213 |
214 | override fun calculatePopupPosition(
215 | density: Density,
216 | tooltipStyle: TooltipStyle,
217 | tipPosition: EdgePosition,
218 | anchorPosition: EdgePosition,
219 | margin: Dp,
220 | anchorBounds: IntRect,
221 | layoutDirection: LayoutDirection,
222 | popupContentSize: IntSize
223 | ): IntOffset = with(density) {
224 | val y = calculatePopupPositionY(
225 | density,
226 | anchorBounds,
227 | anchorPosition,
228 | tooltipStyle,
229 | tipPosition,
230 | popupContentSize
231 | )
232 | val x = if (layoutDirection == LayoutDirection.Ltr) {
233 | anchorBounds.left - margin.toPx() - popupContentSize.width
234 | } else {
235 | anchorBounds.right + margin.toPx()
236 | }
237 | return IntOffset(x.roundToInt(), y.roundToInt())
238 | }
239 | }
240 |
241 | object Top : HorizontalAnchorEdge() {
242 | override fun ConstrainScope.outside(anchor: ConstrainedLayoutReference, margin: Dp) {
243 | bottom.linkTo(anchor.top, margin)
244 | }
245 |
246 | override fun Path.drawTip(size: Size, layoutDirection: LayoutDirection) {
247 | moveTo(0f, 0f)
248 | lineTo(size.width, 0f)
249 | lineTo(size.width / 2f, size.height)
250 | lineTo(0f, 0f)
251 | }
252 |
253 | @Composable
254 | override fun TooltipContainer(
255 | modifier: Modifier,
256 | cornerRadius: Dp,
257 | tipPosition: EdgePosition,
258 | tip: @Composable () -> Unit,
259 | content: @Composable () -> Unit
260 | ) {
261 | val tipPositionOffset = tipPosition.offset
262 | ConstraintLayout(modifier = modifier) {
263 | val (contentContainer, tipContainer) = createRefs()
264 | Box(
265 | modifier = Modifier
266 | .constrainAs(contentContainer) {
267 | start.linkTo(parent.start)
268 | top.linkTo(parent.top)
269 | end.linkTo(parent.end)
270 | }
271 | .padding(
272 | start = if (tipPositionOffset < 0.dp) tipPositionOffset * -2 else 0.dp,
273 | end = if (tipPositionOffset > 0.dp) tipPositionOffset * 2 else 0.dp
274 | )
275 | ) {
276 | content()
277 | }
278 | val tipPadding = cornerRadius + tipPositionOffset.absoluteValue
279 | Box(
280 | modifier = Modifier
281 | .constrainAs(tipContainer) {
282 | linkTo(
283 | contentContainer.start,
284 | contentContainer.end,
285 | bias = tipPosition.percent
286 | )
287 | top.linkTo(contentContainer.bottom)
288 | }
289 | .padding(start = tipPadding, end = tipPadding)
290 | ) {
291 | tip()
292 | }
293 | }
294 | }
295 |
296 | override fun calculatePopupPosition(
297 | density: Density,
298 | tooltipStyle: TooltipStyle,
299 | tipPosition: EdgePosition,
300 | anchorPosition: EdgePosition,
301 | margin: Dp,
302 | anchorBounds: IntRect,
303 | layoutDirection: LayoutDirection,
304 | popupContentSize: IntSize
305 | ): IntOffset = with(density) {
306 | val x = calculatePopupPositionX(
307 | density,
308 | layoutDirection,
309 | anchorBounds,
310 | anchorPosition,
311 | tooltipStyle,
312 | tipPosition,
313 | popupContentSize
314 | )
315 | val y = anchorBounds.top - margin.toPx() - popupContentSize.height
316 | return IntOffset(x.roundToInt(), y.roundToInt())
317 | }
318 | }
319 |
320 | object End : VerticalAnchorEdge() {
321 | override fun ConstrainScope.outside(anchor: ConstrainedLayoutReference, margin: Dp) {
322 | start.linkTo(anchor.end, margin)
323 | }
324 |
325 | override fun Path.drawTip(size: Size, layoutDirection: LayoutDirection) {
326 | when (layoutDirection) {
327 | LayoutDirection.Ltr -> {
328 | moveTo(size.width, 0f)
329 | lineTo(0f, size.height / 2f)
330 | lineTo(size.width, size.height)
331 | lineTo(size.width, 0f)
332 | }
333 | LayoutDirection.Rtl -> {
334 | moveTo(0f, 0f)
335 | lineTo(size.width, size.height / 2f)
336 | lineTo(0f, size.height)
337 | lineTo(0f, 0f)
338 | }
339 | }
340 | }
341 |
342 | @Composable
343 | override fun TooltipContainer(
344 | modifier: Modifier,
345 | cornerRadius: Dp,
346 | tipPosition: EdgePosition,
347 | tip: @Composable () -> Unit,
348 | content: @Composable () -> Unit
349 | ) {
350 | val tipPositionOffset = tipPosition.offset
351 | ConstraintLayout(modifier = modifier) {
352 | val (contentContainer, tipContainer) = createRefs()
353 | Box(
354 | modifier = Modifier
355 | .constrainAs(contentContainer) {
356 | top.linkTo(parent.top)
357 | end.linkTo(parent.end)
358 | bottom.linkTo(parent.bottom)
359 | }
360 | .padding(
361 | top = if (tipPositionOffset < 0.dp) tipPositionOffset * -2 else 0.dp,
362 | bottom = if (tipPositionOffset > 0.dp) tipPositionOffset * 2 else 0.dp
363 | )
364 | ) {
365 | content()
366 | }
367 | val tipPadding = cornerRadius + tipPositionOffset.absoluteValue
368 | Box(
369 | modifier = Modifier
370 | .constrainAs(tipContainer) {
371 | linkTo(
372 | contentContainer.top,
373 | contentContainer.bottom,
374 | bias = tipPosition.percent
375 | )
376 | end.linkTo(contentContainer.start)
377 | }
378 | .padding(top = tipPadding, bottom = tipPadding)
379 | ) {
380 | tip()
381 | }
382 | }
383 | }
384 |
385 | override fun calculatePopupPosition(
386 | density: Density,
387 | tooltipStyle: TooltipStyle,
388 | tipPosition: EdgePosition,
389 | anchorPosition: EdgePosition,
390 | margin: Dp,
391 | anchorBounds: IntRect,
392 | layoutDirection: LayoutDirection,
393 | popupContentSize: IntSize
394 | ): IntOffset = with(density) {
395 | val y = calculatePopupPositionY(
396 | density,
397 | anchorBounds,
398 | anchorPosition,
399 | tooltipStyle,
400 | tipPosition,
401 | popupContentSize
402 | )
403 | val x = if (layoutDirection == LayoutDirection.Ltr) {
404 | anchorBounds.right + margin.toPx()
405 | } else {
406 | anchorBounds.left - margin.toPx() - popupContentSize.width
407 | }
408 | return IntOffset(x.roundToInt(), y.roundToInt())
409 | }
410 | }
411 |
412 | object Bottom : HorizontalAnchorEdge() {
413 | override fun ConstrainScope.outside(anchor: ConstrainedLayoutReference, margin: Dp) {
414 | top.linkTo(anchor.bottom, margin)
415 | }
416 |
417 | override fun Path.drawTip(size: Size, layoutDirection: LayoutDirection) {
418 | moveTo(0f, size.height)
419 | lineTo(size.width / 2f, 0f)
420 | lineTo(size.width, size.height)
421 | lineTo(0f, size.height)
422 | }
423 |
424 | @Composable
425 | override fun TooltipContainer(
426 | modifier: Modifier,
427 | cornerRadius: Dp,
428 | tipPosition: EdgePosition,
429 | tip: @Composable () -> Unit,
430 | content: @Composable () -> Unit
431 | ) {
432 | val tipPositionOffset = tipPosition.offset
433 | ConstraintLayout(modifier = modifier) {
434 | val (contentContainer, tipContainer) = createRefs()
435 | Box(
436 | modifier = Modifier
437 | .constrainAs(contentContainer) {
438 | start.linkTo(parent.start)
439 | end.linkTo(parent.end)
440 | bottom.linkTo(parent.bottom)
441 | }
442 | .padding(
443 | start = if (tipPositionOffset < 0.dp) tipPositionOffset * -2 else 0.dp,
444 | end = if (tipPositionOffset > 0.dp) tipPositionOffset * 2 else 0.dp
445 | )
446 | ) {
447 | content()
448 | }
449 | val tipPadding = cornerRadius + tipPositionOffset.absoluteValue
450 | Box(
451 | modifier = Modifier
452 | .constrainAs(tipContainer) {
453 | linkTo(
454 | contentContainer.start,
455 | contentContainer.end,
456 | bias = tipPosition.percent
457 | )
458 | bottom.linkTo(contentContainer.top)
459 | }
460 | .padding(start = tipPadding, end = tipPadding)
461 | ) {
462 | tip()
463 | }
464 | }
465 | }
466 |
467 | override fun calculatePopupPosition(
468 | density: Density,
469 | tooltipStyle: TooltipStyle,
470 | tipPosition: EdgePosition,
471 | anchorPosition: EdgePosition,
472 | margin: Dp,
473 | anchorBounds: IntRect,
474 | layoutDirection: LayoutDirection,
475 | popupContentSize: IntSize
476 | ): IntOffset = with(density) {
477 | val x = calculatePopupPositionX(
478 | density,
479 | layoutDirection,
480 | anchorBounds,
481 | anchorPosition,
482 | tooltipStyle,
483 | tipPosition,
484 | popupContentSize
485 | )
486 | val y = anchorBounds.bottom + margin.toPx()
487 | return IntOffset(x.roundToInt(), y.roundToInt())
488 | }
489 | }
490 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/skgmn/composetooltip/EdgePosition.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip
2 |
3 | import androidx.annotation.FloatRange
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.compose.ui.unit.Dp
8 | import androidx.compose.ui.unit.dp
9 |
10 | /**
11 | * Specifies a position on an edge.
12 | */
13 | class EdgePosition(
14 | @FloatRange(from = 0.0, to = 1.0)
15 | percent: Float = 0.5f,
16 | offset: Dp = 0.dp
17 | ) {
18 | /**
19 | * When it comes to either [AnchorEdge.Top] or [AnchorEdge.Bottom],
20 | * percent 0.0 means the horizontal start position of the anchor,
21 | * and percent 1.0 means the horizontal end position of the anchor.
22 | *
23 | * If it comes to either [AnchorEdge.Start] or [AnchorEdge.End],
24 | * percent 0.0 means the top of the anchor, and percent 1.0 means the bottom of the anchor.
25 | */
26 | @get:FloatRange(from = 0.0, to = 1.0)
27 | @setparam:FloatRange(from = 0.0, to = 1.0)
28 | var percent by mutableStateOf(percent)
29 |
30 | /**
31 | * Amount of dps from the percentage position on the edge.
32 | *
33 | * For example, if [percent] is 0.5 and [offset] is 10.dp, tip will point out the location
34 | * of 10.dp from the center of the edge.
35 | *
36 | * This allows negative value.
37 | */
38 | var offset by mutableStateOf(offset)
39 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/skgmn/composetooltip/TooltipConstraintLayout.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.EnterTransition
5 | import androidx.compose.animation.ExitTransition
6 | import androidx.compose.animation.ExperimentalAnimationApi
7 | import androidx.compose.animation.core.MutableTransitionState
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.layout.*
10 | import androidx.compose.foundation.shape.GenericShape
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.LocalContentColor
13 | import androidx.compose.material.Text
14 | import androidx.compose.material.contentColorFor
15 | import androidx.compose.runtime.*
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.unit.Dp
19 | import androidx.compose.ui.unit.dp
20 | import androidx.compose.ui.unit.max
21 | import androidx.constraintlayout.compose.ConstrainedLayoutReference
22 | import androidx.constraintlayout.compose.ConstraintLayoutScope
23 |
24 | /**
25 | * Show a tooltip near to [anchor].
26 | *
27 | * @param anchor [ConstrainedLayoutReference] to locate this tooltip nearby
28 | * @param anchorEdge Can be either of [AnchorEdge.Start], [AnchorEdge.Top], [AnchorEdge.End],
29 | * or [AnchorEdge.Bottom]
30 | * @param modifier Modifier for tooltip. Do not use layout-related modifiers except size
31 | * constraints.
32 | * @param tooltipStyle Style for tooltip. Can be created by [rememberTooltipStyle]
33 | * @param tipPosition Tip position relative to balloon
34 | * @param anchorPosition Position on the [anchor]'s edge where the tip points out.
35 | * @param margin Margin between tip and [anchor]
36 | * @param content Content inside balloon. Typically [Text].
37 | */
38 | @Composable
39 | fun ConstraintLayoutScope.Tooltip(
40 | anchor: ConstrainedLayoutReference,
41 | anchorEdge: AnchorEdge,
42 | modifier: Modifier = Modifier,
43 | tooltipStyle: TooltipStyle = rememberTooltipStyle(),
44 | tipPosition: EdgePosition = remember { EdgePosition() },
45 | anchorPosition: EdgePosition = remember { EdgePosition() },
46 | margin: Dp = 8.dp,
47 | content: @Composable RowScope.() -> Unit,
48 | ) = with(anchorEdge) {
49 | val refs = remember { TooltipReferences(this@Tooltip) }
50 |
51 | AnchorHelpers(
52 | anchorEdge = anchorEdge,
53 | anchor = anchor,
54 | refs = refs,
55 | margin = margin,
56 | anchorPosition = anchorPosition,
57 | tipPosition = tipPosition,
58 | tooltipStyle = tooltipStyle
59 | )
60 | TooltipImpl(
61 | anchorEdge = anchorEdge,
62 | modifier = modifier.constrainAs(refs.tooltipContainer) {
63 | outside(refs.tangent, 0.dp)
64 | align(refs.tangent, tipPosition.percent)
65 | },
66 | tooltipStyle = tooltipStyle,
67 | tipPosition = tipPosition,
68 | content = content
69 | )
70 | }
71 |
72 | /**
73 | * Show a tooltip near to [anchor] with transition.
74 | * As [AnimatedVisibility] is experimental, this function is also experimental.
75 | *
76 | * @param anchor [ConstrainedLayoutReference] to locate this tooltip nearby
77 | * @param anchorEdge Can be either of [AnchorEdge.Start], [AnchorEdge.Top], [AnchorEdge.End],
78 | * or [AnchorEdge.Bottom]
79 | * @param enterTransition [EnterTransition] to be applied when the [visible] becomes true.
80 | * Types of [EnterTransition] are listed [here](https://developer.android.com/jetpack/compose/animation#entertransition).
81 | * @param exitTransition [ExitTransition] to be applied when the [visible] becomes false.
82 | * Types of [ExitTransition] are listed [here](https://developer.android.com/jetpack/compose/animation#exittransition).
83 | * @param modifier Modifier for tooltip. Do not use layout-related modifiers except size
84 | * constraints.
85 | * @param visible Visibility of tooltip
86 | * @param tooltipStyle Style for tooltip. Can be created by [rememberTooltipStyle]
87 | * @param tipPosition Tip position relative to balloon
88 | * @param anchorPosition Position on the [anchor]'s edge where the tip points out.
89 | * @param margin Margin between tip and [anchor]
90 | * @param content Content inside balloon. Typically [Text].
91 | */
92 | @ExperimentalAnimationApi
93 | @Composable
94 | fun ConstraintLayoutScope.Tooltip(
95 | anchor: ConstrainedLayoutReference,
96 | anchorEdge: AnchorEdge,
97 | enterTransition: EnterTransition,
98 | exitTransition: ExitTransition,
99 | modifier: Modifier = Modifier,
100 | visible: Boolean = true,
101 | tooltipStyle: TooltipStyle = rememberTooltipStyle(),
102 | tipPosition: EdgePosition = remember { EdgePosition() },
103 | anchorPosition: EdgePosition = remember { EdgePosition() },
104 | margin: Dp = 8.dp,
105 | content: @Composable RowScope.() -> Unit,
106 | ) = with(anchorEdge) {
107 | // I don't know why but AnimatedVisibility without MutableTransitionState does'nt work
108 | val visibleState = remember { MutableTransitionState(visible) }
109 | visibleState.targetState = visible
110 |
111 | val refs = remember { TooltipReferences(this@Tooltip) }
112 |
113 | AnchorHelpers(
114 | anchorEdge = anchorEdge,
115 | anchor = anchor,
116 | refs = refs,
117 | margin = margin,
118 | anchorPosition = anchorPosition,
119 | tipPosition = tipPosition,
120 | tooltipStyle = tooltipStyle
121 | )
122 | AnimatedVisibility(
123 | visibleState = visibleState,
124 | modifier = Modifier.constrainAs(refs.tooltipContainer) {
125 | outside(refs.tangent, 0.dp)
126 | align(refs.tangent, tipPosition.percent)
127 | },
128 | enter = enterTransition,
129 | exit = exitTransition
130 | ) {
131 | TooltipImpl(
132 | modifier = modifier,
133 | anchorEdge = anchorEdge,
134 | tipPosition = tipPosition,
135 | tooltipStyle = tooltipStyle,
136 | content = content
137 | )
138 | }
139 | }
140 |
141 | @Composable
142 | private fun ConstraintLayoutScope.AnchorHelpers(
143 | anchorEdge: AnchorEdge,
144 | anchor: ConstrainedLayoutReference,
145 | refs: TooltipReferences,
146 | margin: Dp,
147 | anchorPosition: EdgePosition,
148 | tipPosition: EdgePosition,
149 | tooltipStyle: TooltipStyle
150 | ) {
151 | ContactPoint(anchor, anchorEdge, anchorPosition, refs, margin)
152 | Tangent(anchorEdge, tooltipStyle, refs, tipPosition)
153 | }
154 |
155 | @Composable
156 | private fun ConstraintLayoutScope.ContactPoint(
157 | anchor: ConstrainedLayoutReference,
158 | anchorEdge: AnchorEdge,
159 | anchorPosition: EdgePosition,
160 | refs: TooltipReferences,
161 | margin: Dp
162 | ) = with(anchorEdge) {
163 | val positionOffset = anchorPosition.offset
164 | if (positionOffset == 0.dp) {
165 | Spacer(
166 | modifier = Modifier
167 | .size(selectWidth(1.dp, 0.dp), selectHeight(1.dp, 0.dp))
168 | .constrainAs(refs.contactPoint) {
169 | outside(anchor, margin)
170 | align(anchor, anchorPosition.percent)
171 | }
172 | )
173 | } else {
174 | Spacer(
175 | modifier = Modifier
176 | .size(0.dp, 0.dp)
177 | .constrainAs(refs.contactPointOrigin) {
178 | align(anchor, anchorPosition.percent)
179 | }
180 | )
181 | Spacer(
182 | modifier = Modifier
183 | .size(0.dp, 0.dp)
184 | .constrainAs(refs.contactPoint) {
185 | outside(anchor, margin)
186 | if (positionOffset > 0.dp) {
187 | nextTo(refs.contactPointOrigin, positionOffset)
188 | } else {
189 | beforeTo(refs.contactPointOrigin, -positionOffset)
190 | }
191 | }
192 | )
193 | }
194 | }
195 |
196 | @Composable
197 | private fun ConstraintLayoutScope.Tangent(
198 | anchorEdge: AnchorEdge,
199 | tooltipStyle: TooltipStyle,
200 | refs: TooltipReferences,
201 | tipPosition: EdgePosition
202 | ) = with(anchorEdge) {
203 | val tangentWidth = tooltipStyle.cornerRadius * 2 +
204 | tipPosition.offset.absoluteValue * 2 +
205 | max(tooltipStyle.tipWidth, tooltipStyle.tipHeight)
206 | Spacer(
207 | modifier = Modifier
208 | .size(selectWidth(tangentWidth, 0.dp), selectHeight(tangentWidth, 0.dp))
209 | .constrainAs(refs.tangent) {
210 | outside(refs.contactPoint, 0.dp)
211 | align(refs.contactPoint, 0.5f)
212 | }
213 | )
214 | }
215 |
216 | @Composable
217 | private fun ConstraintLayoutScope.TooltipImpl(
218 | anchorEdge: AnchorEdge,
219 | tipPosition: EdgePosition,
220 | modifier: Modifier,
221 | tooltipStyle: TooltipStyle,
222 | content: @Composable RowScope.() -> Unit
223 | ) = with(anchorEdge) {
224 | TooltipContainer(
225 | modifier = modifier,
226 | cornerRadius = tooltipStyle.cornerRadius,
227 | tipPosition = tipPosition,
228 | tip = { Tip(anchorEdge, tooltipStyle) },
229 | content = { TooltipContentContainer(anchorEdge, tooltipStyle, content) }
230 | )
231 | }
232 |
233 | @Composable
234 | internal fun Tip(anchorEdge: AnchorEdge, tooltipStyle: TooltipStyle) = with(anchorEdge) {
235 | Box(modifier = Modifier
236 | .size(
237 | width = anchorEdge.selectWidth(
238 | tooltipStyle.tipWidth,
239 | tooltipStyle.tipHeight
240 | ),
241 | height = anchorEdge.selectHeight(
242 | tooltipStyle.tipWidth,
243 | tooltipStyle.tipHeight
244 | )
245 | )
246 | .background(
247 | color = tooltipStyle.color,
248 | shape = GenericShape { size, layoutDirection ->
249 | this.drawTip(size, layoutDirection)
250 | }
251 | )
252 | )
253 | }
254 |
255 | @Composable
256 | internal fun TooltipContentContainer(
257 | anchorEdge: AnchorEdge,
258 | tooltipStyle: TooltipStyle,
259 | content: @Composable (RowScope.() -> Unit)
260 | ) = with(anchorEdge) {
261 | Row(
262 | modifier = Modifier.Companion
263 | .minSize(tooltipStyle)
264 | .background(
265 | color = tooltipStyle.color,
266 | shape = RoundedCornerShape(tooltipStyle.cornerRadius)
267 | )
268 | .padding(tooltipStyle.contentPadding),
269 | horizontalArrangement = Arrangement.Center,
270 | verticalAlignment = Alignment.CenterVertically
271 | ) {
272 | CompositionLocalProvider(
273 | LocalContentColor provides contentColorFor(tooltipStyle.color)
274 | ) {
275 | content()
276 | }
277 | }
278 | }
279 |
280 | private class TooltipReferences(scope: ConstraintLayoutScope) {
281 | val contactPointOrigin = scope.createRef()
282 | val contactPoint = scope.createRef()
283 | val tangent = scope.createRef()
284 | val tooltipContainer = scope.createRef()
285 | }
286 |
287 | internal val Dp.absoluteValue get() = if (this < 0.dp) -this else this
--------------------------------------------------------------------------------
/library/src/main/java/com/github/skgmn/composetooltip/TooltipPopup.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.EnterTransition
5 | import androidx.compose.animation.ExitTransition
6 | import androidx.compose.animation.ExperimentalAnimationApi
7 | import androidx.compose.foundation.layout.RowScope
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.*
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.alpha
12 | import androidx.compose.ui.platform.LocalDensity
13 | import androidx.compose.ui.unit.*
14 | import androidx.compose.ui.window.Popup
15 | import androidx.compose.ui.window.PopupPositionProvider
16 | import androidx.compose.ui.window.PopupProperties
17 | import kotlinx.coroutines.delay
18 |
19 | private const val TRANSITION_INITIALIZE = 0
20 | private const val TRANSITION_ENTER = 1
21 | private const val TRANSITION_EXIT = 2
22 | private const val TRANSITION_GONE = 3
23 |
24 | /**
25 | * Show a tooltip as popup near to an anchor.
26 | * Anchor can be provided by putting the anchor and Tooltip altogether in one composable.
27 | *
28 | * Example:
29 | * ```kotlin
30 | * Box {
31 | * AnchorComposable()
32 | * Tooltip()
33 | * }
34 | * ```
35 | *
36 | * @param anchorEdge Can be either of [AnchorEdge.Start], [AnchorEdge.Top], [AnchorEdge.End],
37 | * or [AnchorEdge.Bottom]
38 | * @param modifier Modifier for tooltip. Do not use layout-related modifiers except size
39 | * constraints.
40 | * @param tooltipStyle Style for tooltip. Can be created by [rememberTooltipStyle]
41 | * @param tipPosition Tip position relative to balloon
42 | * @param anchorPosition Position on the anchor's edge where the tip points out.
43 | * @param margin Margin between tip and anchor
44 | * @param onDismissRequest Executes when the user clicks outside of the tooltip.
45 | * @param properties [PopupProperties] for further customization of this tooltip's behavior.
46 | * @param content Content inside balloon. Typically [Text].
47 | */
48 | @Composable
49 | fun Tooltip(
50 | anchorEdge: AnchorEdge,
51 | modifier: Modifier = Modifier,
52 | tooltipStyle: TooltipStyle = rememberTooltipStyle(),
53 | tipPosition: EdgePosition = remember { EdgePosition() },
54 | anchorPosition: EdgePosition = remember { EdgePosition() },
55 | margin: Dp = 8.dp,
56 | onDismissRequest: (() -> Unit)? = null,
57 | properties: PopupProperties = remember { PopupProperties() },
58 | content: @Composable RowScope.() -> Unit,
59 | ) = with(anchorEdge) {
60 | Popup(
61 | popupPositionProvider = TooltipPopupPositionProvider(
62 | LocalDensity.current,
63 | anchorEdge,
64 | tooltipStyle,
65 | tipPosition,
66 | anchorPosition,
67 | margin
68 | ),
69 | onDismissRequest = onDismissRequest,
70 | properties = properties
71 | ) {
72 | TooltipImpl(
73 | modifier = modifier,
74 | tooltipStyle = tooltipStyle,
75 | tipPosition = tipPosition,
76 | anchorEdge = anchorEdge,
77 | content = content
78 | )
79 | }
80 | }
81 |
82 | /**
83 | * Show a tooltip as popup near to an anchor with transition.
84 | * As [AnimatedVisibility] is experimental, this function is also experimental.
85 | * Anchor can be provided by putting the anchor and Tooltip altogether in one composable.
86 | *
87 | * Example:
88 | * ```kotlin
89 | * Box {
90 | * AnchorComposable()
91 | * Tooltip()
92 | * }
93 | * ```
94 | *
95 | * @param anchorEdge Can be either of [AnchorEdge.Start], [AnchorEdge.Top], [AnchorEdge.End],
96 | * or [AnchorEdge.Bottom]
97 | * @param enterTransition [EnterTransition] to be applied when the [visible] becomes true.
98 | * Types of [EnterTransition] are listed [here](https://developer.android.com/jetpack/compose/animation#entertransition).
99 | * @param exitTransition [ExitTransition] to be applied when the [visible] becomes false.
100 | * Types of [ExitTransition] are listed [here](https://developer.android.com/jetpack/compose/animation#exittransition).
101 | * @param modifier Modifier for tooltip. Do not use layout-related modifiers except size
102 | * constraints.
103 | * @param tooltipStyle Style for tooltip. Can be created by [rememberTooltipStyle]
104 | * @param tipPosition Tip position relative to balloon
105 | * @param anchorPosition Position on the anchor's edge where the tip points out.
106 | * @param margin Margin between tip and anchor
107 | * @param onDismissRequest Executes when the user clicks outside of the tooltip.
108 | * @param properties [PopupProperties] for further customization of this tooltip's behavior.
109 | * @param content Content inside balloon. Typically [Text].
110 | */
111 | @ExperimentalAnimationApi
112 | @Composable
113 | fun Tooltip(
114 | anchorEdge: AnchorEdge,
115 | enterTransition: EnterTransition,
116 | exitTransition: ExitTransition,
117 | modifier: Modifier = Modifier,
118 | visible: Boolean = true,
119 | tooltipStyle: TooltipStyle = rememberTooltipStyle(),
120 | tipPosition: EdgePosition = remember { EdgePosition() },
121 | anchorPosition: EdgePosition = remember { EdgePosition() },
122 | margin: Dp = 8.dp,
123 | onDismissRequest: (() -> Unit)? = null,
124 | properties: PopupProperties = remember { PopupProperties() },
125 | content: @Composable RowScope.() -> Unit,
126 | ) = with(anchorEdge) {
127 | var transitionState by remember { mutableStateOf(TRANSITION_GONE) }
128 | LaunchedEffect(visible) {
129 | if (visible) {
130 | when (transitionState) {
131 | TRANSITION_EXIT -> transitionState = TRANSITION_ENTER
132 | TRANSITION_GONE -> {
133 | transitionState = TRANSITION_INITIALIZE
134 | delay(1)
135 | transitionState = TRANSITION_ENTER
136 | }
137 | }
138 | } else {
139 | when (transitionState) {
140 | TRANSITION_INITIALIZE -> transitionState = TRANSITION_GONE
141 | TRANSITION_ENTER -> transitionState = TRANSITION_EXIT
142 | }
143 | }
144 | }
145 | if (transitionState != TRANSITION_GONE) {
146 | Popup(
147 | popupPositionProvider = TooltipPopupPositionProvider(
148 | LocalDensity.current,
149 | anchorEdge,
150 | tooltipStyle,
151 | tipPosition,
152 | anchorPosition,
153 | margin
154 | ),
155 | onDismissRequest = onDismissRequest,
156 | properties = properties
157 | ) {
158 | if (transitionState == TRANSITION_INITIALIZE) {
159 | TooltipImpl(
160 | tooltipStyle = tooltipStyle,
161 | tipPosition = tipPosition,
162 | anchorEdge = anchorEdge,
163 | modifier = modifier.alpha(0f),
164 | content = content,
165 | )
166 | }
167 | AnimatedVisibility(
168 | visible = transitionState == TRANSITION_ENTER,
169 | enter = enterTransition,
170 | exit = exitTransition
171 | ) {
172 | remember {
173 | object : RememberObserver {
174 | override fun onAbandoned() {
175 | transitionState = TRANSITION_GONE
176 | }
177 |
178 | override fun onForgotten() {
179 | transitionState = TRANSITION_GONE
180 | }
181 |
182 | override fun onRemembered() {
183 | }
184 | }
185 | }
186 | TooltipImpl(
187 | modifier = modifier,
188 | tooltipStyle = tooltipStyle,
189 | tipPosition = tipPosition,
190 | anchorEdge = anchorEdge,
191 | content = content
192 | )
193 | }
194 | }
195 | }
196 | }
197 |
198 | @Composable
199 | private fun AnchorEdge.TooltipImpl(
200 | tooltipStyle: TooltipStyle,
201 | tipPosition: EdgePosition,
202 | anchorEdge: AnchorEdge,
203 | modifier: Modifier = Modifier,
204 | content: @Composable (RowScope.() -> Unit)
205 | ) {
206 | TooltipContainer(
207 | modifier = modifier,
208 | cornerRadius = tooltipStyle.cornerRadius,
209 | tipPosition = tipPosition,
210 | tip = { Tip(anchorEdge, tooltipStyle) },
211 | content = {
212 | TooltipContentContainer(
213 | anchorEdge = anchorEdge,
214 | tooltipStyle = tooltipStyle,
215 | content = content
216 | )
217 | }
218 | )
219 | }
220 |
221 | private class TooltipPopupPositionProvider(
222 | private val density: Density,
223 | private val anchorEdge: AnchorEdge,
224 | private val tooltipStyle: TooltipStyle,
225 | private val tipPosition: EdgePosition,
226 | private val anchorPosition: EdgePosition,
227 | private val margin: Dp
228 | ) : PopupPositionProvider {
229 | override fun calculatePosition(
230 | anchorBounds: IntRect,
231 | windowSize: IntSize,
232 | layoutDirection: LayoutDirection,
233 | popupContentSize: IntSize
234 | ): IntOffset = anchorEdge.calculatePopupPosition(
235 | density,
236 | tooltipStyle,
237 | tipPosition,
238 | anchorPosition,
239 | margin,
240 | anchorBounds,
241 | layoutDirection,
242 | popupContentSize
243 | )
244 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/github/skgmn/composetooltip/TooltipStyle.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.runtime.*
6 | import androidx.compose.ui.graphics.Color
7 | import androidx.compose.ui.unit.Dp
8 | import androidx.compose.ui.unit.dp
9 |
10 | /**
11 | * Style of tooltip
12 | */
13 | class TooltipStyle internal constructor(
14 | color: Color,
15 | cornerRadius: Dp,
16 | tipWidth: Dp,
17 | tipHeight: Dp,
18 | contentPadding: PaddingValues
19 | ) {
20 | /**
21 | * Background color of tooltip.
22 | */
23 | var color by mutableStateOf(color)
24 |
25 | /**
26 | * Corner radius of balloon.
27 | */
28 | var cornerRadius by mutableStateOf(cornerRadius)
29 |
30 | /**
31 | * Width of tip.
32 | */
33 | var tipWidth by mutableStateOf(tipWidth)
34 |
35 | /**
36 | * Height of tip.
37 | */
38 | var tipHeight by mutableStateOf(tipHeight)
39 |
40 | /**
41 | * Padding between balloon and content.
42 | */
43 | var contentPadding by mutableStateOf(contentPadding)
44 | }
45 |
46 | /**
47 | * Create a [TooltipStyle] and remember it.
48 | *
49 | * @param color Background color of tooltip. By default, it uses the seconday color of
50 | * [MaterialTheme].
51 | * @param cornerRadius Corner radius of balloon.
52 | * @param tipWidth Width of tip.
53 | * @param tipHeight Height of tip.
54 | * @param contentPadding Padding between balloon and content.
55 | */
56 | @Composable
57 | fun rememberTooltipStyle(
58 | color: Color = MaterialTheme.colors.secondary,
59 | cornerRadius: Dp = 8.dp,
60 | tipWidth: Dp = 24.dp,
61 | tipHeight: Dp = 8.dp,
62 | contentPadding: PaddingValues = PaddingValues(12.dp),
63 | ): TooltipStyle {
64 | return remember { TooltipStyle(color, cornerRadius, tipWidth, tipHeight, contentPadding) }
65 | }
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | compileSdk sdk_version
8 |
9 | defaultConfig {
10 | applicationId "com.github.skgmn.composetooltip.sample"
11 | minSdk 21
12 | targetSdk sdk_version
13 | versionCode 1
14 | versionName "1.0"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | vectorDrawables {
18 | useSupportLibrary true
19 | }
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 | compileOptions {
29 | sourceCompatibility JavaVersion.VERSION_1_8
30 | targetCompatibility JavaVersion.VERSION_1_8
31 | }
32 | kotlinOptions {
33 | jvmTarget = '1.8'
34 | useIR = true
35 | freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
36 | }
37 | buildFeatures {
38 | compose true
39 | }
40 | composeOptions {
41 | kotlinCompilerExtensionVersion compose_version
42 | kotlinCompilerVersion '1.5.21'
43 | }
44 | packagingOptions {
45 | resources {
46 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
47 | }
48 | }
49 | }
50 |
51 | dependencies {
52 | implementation project(":library")
53 | implementation 'androidx.core:core-ktx:1.6.0'
54 | implementation 'androidx.appcompat:appcompat:1.3.1'
55 | implementation 'com.google.android.material:material:1.4.0'
56 | implementation "androidx.compose.ui:ui:$compose_version"
57 | implementation "androidx.compose.material:material:$compose_version"
58 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
59 | implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-beta02"
60 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
61 | implementation 'androidx.activity:activity-compose:1.3.1'
62 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
63 | }
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/src/androidTest/java/com/github/skgmn/composetooltip/sample/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.github.skgmn.composetooltip.sample", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/skgmn/composetooltip/sample/ExampleConstraintLayout.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.clickable
9 | import androidx.compose.foundation.gestures.detectDragGestures
10 | import androidx.compose.foundation.interaction.MutableInteractionSource
11 | import androidx.compose.foundation.layout.*
12 | import androidx.compose.material.Slider
13 | import androidx.compose.material.Text
14 | import androidx.compose.runtime.*
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.input.pointer.consumeAllChanges
19 | import androidx.compose.ui.input.pointer.pointerInput
20 | import androidx.compose.ui.layout.onSizeChanged
21 | import androidx.compose.ui.res.painterResource
22 | import androidx.compose.ui.unit.IntSize
23 | import androidx.compose.ui.unit.dp
24 | import androidx.constraintlayout.compose.ConstrainedLayoutReference
25 | import androidx.constraintlayout.compose.ConstraintLayout
26 | import androidx.constraintlayout.compose.ConstraintLayoutScope
27 | import com.github.skgmn.composetooltip.AnchorEdge
28 | import com.github.skgmn.composetooltip.EdgePosition
29 | import com.github.skgmn.composetooltip.Tooltip
30 |
31 | @OptIn(ExperimentalAnimationApi::class)
32 | @Composable
33 | fun ExampleConstraintLayout(contentPaddings: PaddingValues) {
34 | var screenSize by remember { mutableStateOf(IntSize.Zero) }
35 | ConstraintLayout(
36 | modifier = Modifier
37 | .fillMaxSize()
38 | .padding(contentPaddings)
39 | .onSizeChanged { screenSize = it }
40 | ) {
41 | val anchorEdgeState = remember { mutableStateOf(AnchorEdge.Top) }
42 |
43 | var imageBiasX by remember { mutableStateOf(0.5f) }
44 | var imageBiasY by remember { mutableStateOf(0.5f) }
45 |
46 | val tipPosition = remember { EdgePosition() }
47 | val anchorPosition = remember { EdgePosition() }
48 |
49 | val tooltipVisibleState = remember { mutableStateOf(true) }
50 |
51 | val anchor = createRef()
52 | val arrows = createRefs()
53 | val bottomPanel = createRef()
54 |
55 | BottomPanel(bottomPanel, tipPosition, anchorPosition)
56 |
57 | Image(
58 | painter = painterResource(R.drawable.dummy),
59 | contentDescription = "dummy",
60 | modifier = Modifier
61 | .constrainAs(anchor) {
62 | linkTo(
63 | parent.start, parent.top, parent.end, parent.bottom,
64 | horizontalBias = imageBiasX,
65 | verticalBias = imageBiasY
66 | )
67 | }
68 | .size(64.dp)
69 | .background(Color(0xffff0000))
70 | .pointerInput(Unit) {
71 | detectDragGestures { change, dragAmount ->
72 | change.consumeAllChanges()
73 | imageBiasX += dragAmount.x / screenSize.width
74 | imageBiasY += dragAmount.y / screenSize.height
75 | }
76 | }
77 | )
78 |
79 | AnchorChangeButtons(anchorEdgeState, tooltipVisibleState, arrows, anchor)
80 |
81 | Tooltip(
82 | anchor = anchor,
83 | anchorEdge = anchorEdgeState.value,
84 | visible = tooltipVisibleState.value,
85 | enterTransition = fadeIn(),
86 | exitTransition = fadeOut(),
87 | tipPosition = tipPosition,
88 | anchorPosition = anchorPosition,
89 | modifier = Modifier
90 | .clickable(remember { MutableInteractionSource() }, null) {
91 | tooltipVisibleState.value = false
92 | }
93 | ) {
94 | Text(
95 | text = "Drag icon to move it.\nTouch tooltip to dismiss it.\nTouch arrows to move anchor edge.",
96 | modifier = Modifier.widthIn(max = 200.dp)
97 | )
98 | }
99 | }
100 | }
101 |
102 | @Composable
103 | private fun ConstraintLayoutScope.BottomPanel(
104 | ref: ConstrainedLayoutReference,
105 | tipPosition: EdgePosition,
106 | anchorPosition: EdgePosition
107 | ) {
108 | val firstColumnWeight = 0.3f
109 | Column(
110 | modifier = Modifier
111 | .constrainAs(ref) {
112 | start.linkTo(parent.start)
113 | end.linkTo(parent.end)
114 | bottom.linkTo(parent.bottom)
115 | }
116 | ) {
117 | Row(
118 | modifier = Modifier
119 | .fillMaxWidth()
120 | .padding(start = 16.dp),
121 | verticalAlignment = Alignment.CenterVertically
122 | ) {
123 | Text(
124 | text = "Tip position",
125 | modifier = Modifier.weight(firstColumnWeight)
126 | )
127 | Slider(
128 | value = tipPosition.percent,
129 | onValueChange = { tipPosition.percent = it },
130 | modifier = Modifier.weight(1f - firstColumnWeight)
131 | )
132 | }
133 | Row(
134 | modifier = Modifier
135 | .fillMaxWidth()
136 | .padding(start = 16.dp),
137 | verticalAlignment = Alignment.CenterVertically
138 | ) {
139 | Text(
140 | text = "Anchor position",
141 | modifier = Modifier.weight(firstColumnWeight)
142 | )
143 | Slider(
144 | value = anchorPosition.percent,
145 | onValueChange = { anchorPosition.percent = it },
146 | modifier = Modifier.weight(1f - firstColumnWeight)
147 | )
148 | }
149 | }
150 | }
151 |
152 | @Composable
153 | private fun ConstraintLayoutScope.AnchorChangeButtons(
154 | anchorEdgeState: MutableState,
155 | tooltipVisibleState: MutableState,
156 | refs: ConstraintLayoutScope.ConstrainedLayoutReferences,
157 | anchor: ConstrainedLayoutReference
158 | ) {
159 | val padding = 8.dp
160 | val anchorEdge by anchorEdgeState
161 | if (!tooltipVisibleState.value || anchorEdge != AnchorEdge.Start) {
162 | Text(
163 | text = "⬅",
164 | modifier = Modifier
165 | .constrainAs(refs.component1()) {
166 | end.linkTo(anchor.start, 8.dp)
167 | centerVerticallyTo(anchor)
168 | }
169 | .clickable {
170 | anchorEdgeState.value = AnchorEdge.Start
171 | tooltipVisibleState.value = true
172 | }
173 | .padding(padding)
174 | )
175 | }
176 | if (!tooltipVisibleState.value || anchorEdge != AnchorEdge.Top) {
177 | Text(
178 | text = "⬆",
179 | modifier = Modifier
180 | .constrainAs(refs.component2()) {
181 | bottom.linkTo(anchor.top, 8.dp)
182 | centerHorizontallyTo(anchor)
183 | }
184 | .clickable {
185 | anchorEdgeState.value = AnchorEdge.Top
186 | tooltipVisibleState.value = true
187 | }
188 | .padding(padding)
189 | )
190 | }
191 | if (!tooltipVisibleState.value || anchorEdge != AnchorEdge.End) {
192 | Text(
193 | text = "➡",
194 | modifier = Modifier
195 | .constrainAs(refs.component3()) {
196 | start.linkTo(anchor.end, 8.dp)
197 | centerVerticallyTo(anchor)
198 | }
199 | .clickable {
200 | anchorEdgeState.value = AnchorEdge.End
201 | tooltipVisibleState.value = true
202 | }
203 | .padding(padding)
204 | )
205 | }
206 | if (!tooltipVisibleState.value || anchorEdge != AnchorEdge.Bottom) {
207 | Text(
208 | text = "⬇",
209 | modifier = Modifier
210 | .constrainAs(refs.component4()) {
211 | top.linkTo(anchor.bottom, 8.dp)
212 | centerHorizontallyTo(anchor)
213 | }
214 | .clickable {
215 | anchorEdgeState.value = AnchorEdge.Bottom
216 | tooltipVisibleState.value = true
217 | }
218 | .padding(padding)
219 | )
220 | }
221 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/skgmn/composetooltip/sample/ExamplePopup.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.gestures.detectDragGestures
8 | import androidx.compose.foundation.interaction.MutableInteractionSource
9 | import androidx.compose.foundation.layout.*
10 | import androidx.compose.material.ScaffoldState
11 | import androidx.compose.material.Slider
12 | import androidx.compose.material.Text
13 | import androidx.compose.runtime.*
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.draw.alpha
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.input.pointer.consumeAllChanges
19 | import androidx.compose.ui.input.pointer.pointerInput
20 | import androidx.compose.ui.layout.onSizeChanged
21 | import androidx.compose.ui.platform.LocalDensity
22 | import androidx.compose.ui.res.painterResource
23 | import androidx.compose.ui.unit.IntSize
24 | import androidx.compose.ui.unit.dp
25 | import com.github.skgmn.composetooltip.AnchorEdge
26 | import com.github.skgmn.composetooltip.EdgePosition
27 | import com.github.skgmn.composetooltip.Tooltip
28 | import kotlinx.coroutines.flow.dropWhile
29 | import kotlinx.coroutines.flow.firstOrNull
30 |
31 | @OptIn(ExperimentalAnimationApi::class)
32 | @Composable
33 | fun ExamplePopup(
34 | contentPaddings: PaddingValues,
35 | scaffoldState: ScaffoldState
36 | ) {
37 | Box(
38 | modifier = Modifier
39 | .padding(contentPaddings)
40 | .fillMaxSize()
41 | ) {
42 | var anchorEdge by remember { mutableStateOf(AnchorEdge.Top) }
43 |
44 | var imageOffsetX by remember { mutableStateOf(0.dp) }
45 | var imageOffsetY by remember { mutableStateOf(0.dp) }
46 |
47 | val tipPosition = remember { EdgePosition() }
48 | val anchorPosition = remember { EdgePosition() }
49 |
50 | var tooltipVisible by remember { mutableStateOf(false) }
51 |
52 | LaunchedEffect(Unit) {
53 | snapshotFlow { scaffoldState.drawerState.isClosed }
54 | .dropWhile { !it }
55 | .firstOrNull()
56 | tooltipVisible = true
57 | }
58 |
59 | BottomPanel(tipPosition, anchorPosition)
60 |
61 | Column(
62 | modifier = Modifier
63 | .align(Alignment.Center)
64 | .offset(imageOffsetX, imageOffsetY)
65 | ) {
66 | Text(
67 | text = "⬆",
68 | modifier = Modifier
69 | .align(Alignment.CenterHorizontally)
70 | .padding(bottom = 8.dp)
71 | .buttonInvisible(tooltipVisible && anchorEdge == AnchorEdge.Top) {
72 | anchorEdge = AnchorEdge.Top
73 | tooltipVisible = true
74 | }
75 | .padding(8.dp)
76 | )
77 | Row(
78 | verticalAlignment = Alignment.CenterVertically
79 | ) {
80 | Text(
81 | text = "⬅",
82 | modifier = Modifier
83 | .padding(end = 8.dp)
84 | .buttonInvisible(tooltipVisible && anchorEdge == AnchorEdge.Start) {
85 | anchorEdge = AnchorEdge.Start
86 | tooltipVisible = true
87 | }
88 | .padding(8.dp)
89 | )
90 | val density = LocalDensity.current
91 | Box(
92 | modifier = Modifier
93 | .size(64.dp)
94 | .background(Color(0xffff0000))
95 | .pointerInput(Unit) {
96 | detectDragGestures { change, dragAmount ->
97 | change.consumeAllChanges()
98 | with(density) {
99 | imageOffsetX += dragAmount.x.toDp()
100 | imageOffsetY += dragAmount.y.toDp()
101 | }
102 | }
103 | }
104 | ) {
105 | Image(
106 | painter = painterResource(R.drawable.dummy),
107 | contentDescription = "dummy",
108 | )
109 | Tooltip(
110 | anchorEdge = anchorEdge,
111 | visible = tooltipVisible,
112 | enterTransition = fadeIn(),
113 | exitTransition = fadeOut(),
114 | tipPosition = tipPosition,
115 | anchorPosition = anchorPosition,
116 | modifier = Modifier.clickable(
117 | remember { MutableInteractionSource() },
118 | null
119 | ) {
120 | tooltipVisible = false
121 | },
122 | onDismissRequest = {
123 | tooltipVisible = false
124 | }
125 | ) {
126 | Text(
127 | text = "Drag icon to move it.\nTouch tooltip to dismiss it.\nTouch arrows to move anchor edge.",
128 | modifier = Modifier.widthIn(max = 200.dp)
129 | )
130 | }
131 | }
132 | Text(
133 | text = "➡",
134 | modifier = Modifier
135 | .padding(start = 8.dp)
136 | .buttonInvisible(tooltipVisible && anchorEdge == AnchorEdge.End) {
137 | anchorEdge = AnchorEdge.End
138 | tooltipVisible = true
139 | }
140 | .padding(8.dp)
141 | )
142 | }
143 | Text(
144 | text = "⬇",
145 | modifier = Modifier
146 | .align(Alignment.CenterHorizontally)
147 | .padding(top = 8.dp)
148 | .buttonInvisible(tooltipVisible && anchorEdge == AnchorEdge.Bottom) {
149 | anchorEdge = AnchorEdge.Bottom
150 | tooltipVisible = true
151 | }
152 | .padding(8.dp)
153 | )
154 | }
155 | }
156 | }
157 |
158 | @Composable
159 | private fun BoxScope.BottomPanel(
160 | tipPosition: EdgePosition,
161 | anchorPosition: EdgePosition
162 | ) {
163 | val firstColumnWeight = 0.3f
164 | Column(
165 | modifier = Modifier
166 | .align(Alignment.BottomCenter)
167 | .fillMaxWidth()
168 | ) {
169 | Row(
170 | modifier = Modifier
171 | .fillMaxWidth()
172 | .padding(start = 16.dp),
173 | verticalAlignment = Alignment.CenterVertically
174 | ) {
175 | Text(
176 | text = "Tip position",
177 | modifier = Modifier.weight(firstColumnWeight)
178 | )
179 | Slider(
180 | value = tipPosition.percent,
181 | onValueChange = { tipPosition.percent = it },
182 | modifier = Modifier.weight(1f - firstColumnWeight)
183 | )
184 | }
185 | Row(
186 | modifier = Modifier
187 | .fillMaxWidth()
188 | .padding(start = 16.dp),
189 | verticalAlignment = Alignment.CenterVertically
190 | ) {
191 | Text(
192 | text = "Anchor position",
193 | modifier = Modifier.weight(firstColumnWeight)
194 | )
195 | Slider(
196 | value = anchorPosition.percent,
197 | onValueChange = { anchorPosition.percent = it },
198 | modifier = Modifier.weight(1f - firstColumnWeight)
199 | )
200 | }
201 | }
202 | }
203 |
204 | private fun Modifier.buttonInvisible(condition: Boolean, onClick: () -> Unit): Modifier {
205 | return if (condition) {
206 | alpha(0f)
207 | } else {
208 | clickable(onClick = onClick)
209 | }
210 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/skgmn/composetooltip/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.material.MaterialTheme
7 | import androidx.compose.material.Surface
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import com.github.skgmn.composetooltip.sample.ui.theme.ComposeTooltipTheme
12 |
13 | class MainActivity : ComponentActivity() {
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContent {
17 | ComposeTooltipTheme {
18 | MainScreen()
19 | }
20 | }
21 | }
22 | }
23 |
24 | @Preview(showSystemUi = true)
25 | @Composable
26 | fun MainScreenPreview() {
27 | ComposeTooltipTheme {
28 | MainScreen()
29 | }
30 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/skgmn/composetooltip/sample/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.*
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.Menu
8 | import androidx.compose.runtime.*
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.unit.dp
11 | import kotlinx.coroutines.CoroutineScope
12 | import kotlinx.coroutines.launch
13 | import java.lang.IllegalStateException
14 |
15 | private const val EXAMPLE_CONSTRAINT_LAYOUT = 0
16 | private const val EXAMPLE_POPUP = 1
17 |
18 | @Composable
19 | fun MainScreen() {
20 | val scaffoldState = rememberScaffoldState()
21 | val scope = rememberCoroutineScope()
22 | var exampleType by remember { mutableStateOf(EXAMPLE_CONSTRAINT_LAYOUT) }
23 | Scaffold(
24 | scaffoldState = scaffoldState,
25 | topBar = {
26 | TopAppBar(
27 | title = {
28 | Text(
29 | when (exampleType) {
30 | EXAMPLE_CONSTRAINT_LAYOUT -> "Tooltip with ConstraintLayout"
31 | EXAMPLE_POPUP -> "Tooltip as Popup"
32 | else -> throw IllegalStateException()
33 | }
34 | )
35 | },
36 | navigationIcon = {
37 | IconButton(onClick = { toggleDrawer(scaffoldState, scope) }) {
38 | Icon(
39 | imageVector = Icons.Default.Menu,
40 | contentDescription = "Menu"
41 | )
42 | }
43 | }
44 | )
45 | },
46 | drawerContent = {
47 | Text(
48 | text = "Tooltip with ConstraintLayout",
49 | modifier = Modifier
50 | .fillMaxWidth()
51 | .clickable {
52 | toggleDrawer(scaffoldState, scope)
53 | exampleType = EXAMPLE_CONSTRAINT_LAYOUT
54 | }
55 | .padding(16.dp)
56 | )
57 | Text(
58 | text = "Tooltip as Popup",
59 | modifier = Modifier
60 | .fillMaxWidth()
61 | .clickable {
62 | toggleDrawer(scaffoldState, scope)
63 | exampleType = EXAMPLE_POPUP
64 | }
65 | .padding(16.dp)
66 | )
67 | }
68 | ) {
69 | when (exampleType) {
70 | EXAMPLE_CONSTRAINT_LAYOUT -> ExampleConstraintLayout(contentPaddings = it)
71 | EXAMPLE_POPUP -> ExamplePopup(contentPaddings = it, scaffoldState = scaffoldState)
72 | }
73 | }
74 | }
75 |
76 | private fun toggleDrawer(
77 | scaffoldState: ScaffoldState,
78 | scope: CoroutineScope
79 | ) {
80 | if (!scaffoldState.drawerState.isAnimationRunning) {
81 | scope.launch {
82 | if (scaffoldState.drawerState.isClosed) {
83 | scaffoldState.drawerState.open()
84 | } else {
85 | scaffoldState.drawerState.close()
86 | }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/skgmn/composetooltip/sample/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/skgmn/composetooltip/sample/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/skgmn/composetooltip/sample/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun ComposeTooltipTheme(
32 | darkTheme: Boolean = isSystemInDarkTheme(),
33 | content: @Composable() () -> Unit
34 | ) {
35 | val colors = if (darkTheme) {
36 | DarkColorPalette
37 | } else {
38 | LightColorPalette
39 | }
40 |
41 | MaterialTheme(
42 | colors = colors,
43 | typography = Typography,
44 | shapes = Shapes,
45 | content = content
46 | )
47 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/github/skgmn/composetooltip/sample/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxxhdpi/dummy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/drawable-xxxhdpi/dummy.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skgmn/ComposeTooltip/7c4783308f55d3376039952e3dea5fdb6a5185c0/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | ComposeTooltip
3 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/sample/src/test/java/com/github/skgmn/composetooltip/sample/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.github.skgmn.composetooltip.sample
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | jcenter() // Warning: this repository is going to shut down soon
7 | }
8 | }
9 | rootProject.name = "ComposeTooltip"
10 | include ':sample'
11 | include ':library'
12 |
--------------------------------------------------------------------------------