├── .travis.yml ├── LICENSE ├── README.md ├── demo ├── kivymd_logo.png ├── ttv_demo.py └── ttv_demo_2.gif ├── requirements.txt ├── setup.py ├── taptargetview ├── __init__.py └── taptargetview.py └── test └── test1.py /.travis.yml: -------------------------------------------------------------------------------- 1 | cache: pip 2 | 3 | matrix: 4 | fast_finish: true 5 | include: 6 | - name: "Black" 7 | env: RUN=black 8 | language: python 9 | python: 3.7 10 | os: linux 11 | dist: bionic 12 | 13 | script: python3 test/test1.py 14 | 15 | after_failure: 16 | - sleep 10; 17 | - echo == End == 18 | 19 | deploy: 20 | provider: pypi 21 | user: "__token__" 22 | password: 23 | secure: "pypi-AgEIcHlwaS5vcmcCJDdhMTkyODMzLWE1Y2MtNDc3My1hYzI0LTM2ZGU5NTRkNmY4MAACPnsicGVybWlzc2lvbnMiOiB7InByb2plY3RzIjogWyJ0YXB0YXJnZXR2aWV3Il19LCAidmVyc2lvbiI6IDF9AAAGIAnncDRYv5MyAxTGReIb8VblzDX1pWqXr5dDNL5OhBc3" 24 | distributions: "sdist bdist_wheel" 25 | on: 26 | tags: true 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Shashi Ranjan 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 | ![TapTargetView demo](demo/ttv_demo_2.gif) 2 | 3 | # TapTargetView [![Build Status](https://travis-ci.org/shashi278/TapTargetView.svg?branch=master)](https://travis-ci.org/shashi278/TapTargetView) 4 | ###### This is now being used in [KivyMD](https://github.com/HeaTTheatR/KivyMD) 5 | 6 | An attempt to mimic android's TapTargetView using Python and Kivy. 7 | 8 | Inspired by [Android's TapTargetView](https://github.com/KeepSafe/TapTargetView) 9 | 10 | ## Installation 11 | #### *Using Pip* 12 | * `pip install taptargetview` 13 | 14 | #### *Manually* 15 | 16 | * `git clone https://github.com/shashi278/TapTargetView.git` 17 | 18 | * `cd TapTargetView` 19 | 20 | * `python setup.py install` 21 | 22 | ## Simple Usage 23 | ```python 24 | 25 | TapTargetView( 26 | my_button, 27 | outer_circle_color= [0,1,1], 28 | outer_circle_alpha= .85, 29 | title_text= "My Button", 30 | description_text="It does something when pressed", 31 | widget_position="center", 32 | title_position="right_bottom", 33 | end= my_callback 34 | ).start() 35 | 36 | ``` 37 | Refer to [demo](demo/ttv_demo.py) for extensive usages. 38 | 39 | ### Sequencing 40 | Sequencing is easier. Just bind `start` of one instance to the `on_end` of another instance. 41 | ```python 42 | 43 | ttv2= TapTargetView( 44 | my_button2, 45 | outer_circle_color= [1,0,1], 46 | outer_circle_alpha= .05, 47 | title_text= "My Second Button", 48 | description_text="It too does something when pressed", 49 | widget_position="left", 50 | end= my_callback 51 | ) 52 | 53 | ttv1= TapTargetView( 54 | my_button1, 55 | outer_circle_color= [0,1,1], 56 | outer_circle_alpha= .85, 57 | title_text= "My First Button", 58 | description_text="It does something when pressed", 59 | widget_position="center", 60 | title_position="right_bottom", 61 | end= ttv2.start 62 | ) 63 | 64 | ttv1.start() 65 | 66 | ``` 67 | 68 | ### Customizable attributes: 69 | ```python 70 | """ 71 | widget: widget to add TapTargetView upon 72 | outer_radius: (optional), Radius for outer circle, defaults to dp(300) 73 | outer_circle_color: (optional), Color for the outer circle, defaults to [1,0,0] 74 | outer_circle_alpha: (optional), Alpha value for outer circle, defaults to .96 75 | target_radius: (optional), Radius for target circle, defaults to dp(45) 76 | target_circle_color: (optional), Color for target circle, defaults to [1,1,1] 77 | title_text: (optional), Title to be shown on the view, defaults to '' 78 | title_text_size: (optional), Text size for title, defaults to dp(25) 79 | title_text_color: (optional), Text color for title, defaults to [1,1,1,1] 80 | title_text_bold: (optional), Whether title should be bold, defaults to `True` 81 | description_text: (optional), Description to be shown below the title(Keep it short), 82 | defaults to '' 83 | description_text_size: (optional), Text size for description text, defaults to dp(20) 84 | description_text_color: (optional), Text color for description text, defaults to [.9,.9,.9,1] 85 | description_text_bold: (optional), Whether description should be bold, defaults to False 86 | draw_shadow: (optional), Whether to show shadow, defaults to False 87 | cancelable: (optional), Whether clicking outside the outer circle dismisses the view, 88 | defaults to False 89 | widget_position: (optional), Sets the position of the widget on the outer_circle. 90 | Can be one of "left","right","top","bottom","left_top","right_top", 91 | "left_bottom","right_bottom", and "center", defaults to "left" 92 | title_position: (optional), Sets the position of `title_text` on the outer circle. 93 | Only works if `widget_position` is set to "center". In all other cases, 94 | it calculates the `title_position` itself. 95 | Must be set to other than "auto" when `widget_position` is set to "center". 96 | Can be one of "left","right","top","bottom","left_top","right_top", 97 | "left_bottom", and "right_bottom", defaults to "auto" (since `widget_position` is "left") 98 | stop_on_outer_touch: (optional), whether clicking on outer circle stops the animation, 99 | defaults to False 100 | stop_on_target_touch: (optional), whether clicking on target circle should stop the animation, 101 | defaults to True 102 | end: (optional), Function to be called when the animation stops, defaults to None 103 | """ 104 | ``` 105 | -------------------------------------------------------------------------------- /demo/kivymd_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shashi278/TapTargetView/eca53c282a1492c372c3d130043c110576c51f53/demo/kivymd_logo.png -------------------------------------------------------------------------------- /demo/ttv_demo.py: -------------------------------------------------------------------------------- 1 | from kivy.animation import Animation 2 | from kivy.lang import Builder 3 | from kivy.metrics import dp 4 | from kivy.uix.boxlayout import BoxLayout 5 | 6 | from kivymd.app import MDApp 7 | from kivymd.uix.behaviors import ( 8 | RectangularElevationBehavior, 9 | SpecificBackgroundColorBehavior, 10 | ) 11 | from taptargetview.taptargetview import TapTargetView 12 | 13 | example_kv = """ 14 | Screen: 15 | 16 | Image: 17 | id: logo 18 | source: "kivymd_logo.png" 19 | 20 | CustomToolbar: 21 | id: toolbar 22 | size_hint_y: None 23 | height: app.theme_cls.standard_increment 24 | md_bg_color: app.theme_cls.primary_color 25 | elevation: 10 26 | padding: "8dp", 0, 0, 0 27 | pos_hint: {"top": 1} 28 | 29 | MDIconButton: 30 | id: menu_btn 31 | icon: "menu" 32 | theme_text_color: "Custom" 33 | text_color: toolbar.specific_text_color 34 | md_bg_color: app.theme_cls.primary_color 35 | pos_hint: {"center_y": .5} 36 | 37 | Widget: 38 | size_hint_x: None 39 | width: "25dp" 40 | 41 | MDLabel: 42 | text: "TapTargetView" 43 | shorten: True 44 | font_style: 'H6' 45 | theme_text_color: "Custom" 46 | text_color: toolbar.specific_text_color 47 | 48 | MDIconButton: 49 | id: search_btn 50 | icon: "magnify" 51 | md_bg_color: 0, 0, 0, 0 52 | theme_text_color: "Custom" 53 | text_color: 1, 1, 1, 1 54 | pos_hint: {"center_y": .5} 55 | 56 | MDIconButton: 57 | id: info_btn 58 | icon: "information-outline" 59 | md_bg_color: 0, 0, 0, 0 60 | theme_text_color: "Custom" 61 | text_color: 1, 1, 1, 1 62 | pos_hint: {"center_y": .5} 63 | 64 | MDLabel: 65 | id: lbl 66 | text: "Congrats! You're" + "\\n" + "educated now!!" 67 | opacity: 0 68 | font_size: "24sp" 69 | halign: "center" 70 | 71 | MDFloatingActionButton: 72 | id: add_btn 73 | icon: "plus" 74 | pos: 10, 10 75 | """ 76 | 77 | 78 | class CustomToolbar( 79 | RectangularElevationBehavior, SpecificBackgroundColorBehavior, BoxLayout 80 | ): 81 | pass 82 | 83 | 84 | class TapTargetViewDemo(MDApp): 85 | def build(self): 86 | self.screen = Builder.load_string(example_kv) 87 | 88 | ttv4 = TapTargetView( 89 | widget=self.screen.ids.add_btn, 90 | outer_radius=dp(320), 91 | cancelable=True, 92 | outer_circle_color=self.theme_cls.primary_color[:-1], 93 | outer_circle_alpha=0.9, 94 | title_text="This is an add button", 95 | description_text="You can cancel it by clicking outside", 96 | widget_position="left_bottom", 97 | end=self.complete, 98 | ) 99 | 100 | ttv3 = TapTargetView( 101 | widget=self.screen.ids.info_btn, 102 | outer_radius=dp(440), 103 | outer_circle_color=self.theme_cls.primary_color[:-1], 104 | outer_circle_alpha=0.8, 105 | target_circle_color=[255 / 255, 34 / 255, 212 / 255], 106 | title_text="This is the info button", 107 | description_text="No information available yet!", 108 | widget_position="center", 109 | title_position="left_bottom", 110 | end=ttv4.start, 111 | ) 112 | 113 | ttv2 = TapTargetView( 114 | widget=self.screen.ids.search_btn, 115 | outer_circle_color=[155 / 255, 89 / 255, 182 / 255], 116 | target_circle_color=[0.2, 0.2, 0.2], 117 | title_text="This is the search button", 118 | description_text="It won't search anything for now.", 119 | widget_position="center", 120 | title_position="left_bottom", 121 | end=ttv3.start, 122 | ) 123 | 124 | ttv1 = TapTargetView( 125 | widget=self.screen.ids.menu_btn, 126 | outer_circle_color=self.theme_cls.primary_color[:-1], 127 | outer_circle_alpha=0.85, 128 | title_text="Menu Button", 129 | description_text="Opens up the drawer", 130 | widget_position="center", 131 | title_position="right_bottom", 132 | end=ttv2.start, 133 | ) 134 | ttv1.start() 135 | 136 | return self.screen 137 | 138 | def complete(self, *args): 139 | Animation(opacity=0.3, d=0.2).start(self.screen.ids.logo) 140 | Animation(opacity=0.3, d=0.2).start(self.screen.ids.lbl) 141 | 142 | 143 | TapTargetViewDemo().run() 144 | -------------------------------------------------------------------------------- /demo/ttv_demo_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shashi278/TapTargetView/eca53c282a1492c372c3d130043c110576c51f53/demo/ttv_demo_2.gif -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | kivy 2 | kivymd 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name="TapTargetView", 8 | version="0.1.2", 9 | packages=["taptargetview"], 10 | scripts=["taptargetview/taptargetview.py"], 11 | package_data={"taptargetview": ["*.py"],}, 12 | # metadata to display on PyPI 13 | author="Shashi Ranjan", 14 | author_email="shashiranjankv@gmail.com", 15 | description="Attempt to mimic Android's TapTargetView using Kivy and Python", 16 | long_description=long_description, 17 | long_description_content_type="text/markdown", 18 | keywords="Kivy Python TapTargetView", 19 | url="https://github.com/shashi278/TapTargetView", 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | ], 25 | install_requires=["kivy",], 26 | python_requires=">=3.6", 27 | ) 28 | -------------------------------------------------------------------------------- /taptargetview/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shashi278/TapTargetView/eca53c282a1492c372c3d130043c110576c51f53/taptargetview/__init__.py -------------------------------------------------------------------------------- /taptargetview/taptargetview.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. seealso:: 3 | 4 | `TapTargetView, GitHub `_ 5 | 6 | `TapTargetView, Material archive https://material.io/archive/guidelines/growth-communications/feature-discovery.html#>`_ 7 | 8 | .. rubric:: Attempt to mimic the working of Android's TapTargetView using Kivy and Python.. 9 | """ 10 | 11 | from kivy.animation import Animation 12 | from kivy.metrics import dp 13 | from kivy.graphics import Color, Ellipse, Rectangle 14 | from kivy.event import EventDispatcher 15 | from kivy.properties import ( 16 | ObjectProperty, 17 | NumericProperty, 18 | ListProperty, 19 | StringProperty, 20 | BooleanProperty, 21 | OptionProperty, 22 | ) 23 | from kivy.uix.label import Label 24 | 25 | 26 | class TapTargetView(EventDispatcher): 27 | """Rough try to mimic the working of Android's TapTargetView.""" 28 | 29 | widget = ObjectProperty() 30 | """ 31 | Widget to add ``TapTargetView`` upon. 32 | 33 | :attr:`widget` is an :class:`~kivy.properties.ObjectProperty` 34 | and defaults to `None`. 35 | """ 36 | 37 | outer_radius = NumericProperty(dp(300)) 38 | """ 39 | Radius for outer circle. 40 | 41 | :attr:`outer_radius` is an :class:`~kivy.properties.NumericProperty` 42 | and defaults to `dp(300)`. 43 | """ 44 | 45 | outer_circle_color = ListProperty([1, 0, 0]) 46 | """ 47 | Color for the outer circle. 48 | 49 | :attr:`outer_circle_color` is an :class:`~kivy.properties.ListProperty` 50 | and defaults to `[1, 0, 0]`. 51 | """ 52 | 53 | outer_circle_alpha = NumericProperty(0.96) 54 | """ 55 | Alpha value for outer circle. 56 | 57 | :attr:`outer_circle_alpha` is an :class:`~kivy.properties.NumericProperty` 58 | and defaults to `0.96`. 59 | """ 60 | 61 | target_radius = NumericProperty(dp(45)) 62 | """ 63 | Radius for target circle. 64 | 65 | :attr:`target_radius` is an :class:`~kivy.properties.NumericProperty` 66 | and defaults to `dp(45)`. 67 | """ 68 | 69 | target_circle_color = ListProperty([1, 1, 1]) 70 | """ 71 | Color for target circle. 72 | 73 | :attr:`target_circle_color` is an :class:`~kivy.properties.ListProperty` 74 | and defaults to `[1, 1, 1]`. 75 | """ 76 | 77 | title_text = StringProperty() 78 | """ 79 | Title to be shown on the view. 80 | 81 | :attr:`title_text` is an :class:`~kivy.properties.StringProperty` 82 | and defaults to `''`. 83 | """ 84 | 85 | title_text_size = NumericProperty(dp(25)) 86 | """ 87 | Text size for title. 88 | 89 | :attr:`title_text_size` is an :class:`~kivy.properties.NumericProperty` 90 | and defaults to `dp(25)`. 91 | """ 92 | 93 | title_text_color = ListProperty([1, 1, 1, 1]) 94 | """ 95 | Text color for title. 96 | 97 | :attr:`title_text_color` is an :class:`~kivy.properties.ListProperty` 98 | and defaults to `[1, 1, 1, 1]`. 99 | """ 100 | 101 | title_text_bold = BooleanProperty(True) 102 | """ 103 | Whether title should be bold. 104 | 105 | :attr:`title_text_bold` is an :class:`~kivy.properties.BooleanProperty` 106 | and defaults to `True`. 107 | """ 108 | 109 | description_text = StringProperty() 110 | """ 111 | Description to be shown below the title (keep it short). 112 | 113 | :attr:`description_text` is an :class:`~kivy.properties.StringProperty` 114 | and defaults to `''`. 115 | """ 116 | 117 | description_text_size = NumericProperty(dp(20)) 118 | """ 119 | Text size for description text. 120 | 121 | :attr:`description_text_size` is an :class:`~kivy.properties.NumericProperty` 122 | and defaults to `dp(20)`. 123 | """ 124 | 125 | description_text_color = ListProperty([0.9, 0.9, 0.9, 1]) 126 | """ 127 | Text size for description text. 128 | 129 | :attr:`description_text_color` is an :class:`~kivy.properties.ListProperty` 130 | and defaults to `[0.9, 0.9, 0.9, 1]`. 131 | """ 132 | 133 | description_text_bold = BooleanProperty(False) 134 | """ 135 | Whether description should be bold. 136 | 137 | :attr:`description_text_bold` is an :class:`~kivy.properties.BooleanProperty` 138 | and defaults to `False`. 139 | """ 140 | 141 | draw_shadow = BooleanProperty(False) 142 | """ 143 | Whether to show shadow. 144 | 145 | :attr:`draw_shadow` is an :class:`~kivy.properties.BooleanProperty` 146 | and defaults to `False`. 147 | """ 148 | 149 | cancelable = BooleanProperty(False) 150 | """ 151 | Whether clicking outside the outer circle dismisses the view. 152 | 153 | :attr:`cancelable` is an :class:`~kivy.properties.BooleanProperty` 154 | and defaults to `False`. 155 | """ 156 | 157 | widget_position = OptionProperty( 158 | "left", 159 | options=[ 160 | "left", 161 | "right", 162 | "top", 163 | "bottom", 164 | "left_top", 165 | "right_top", 166 | "left_bottom", 167 | "right_bottom", 168 | "center", 169 | ], 170 | ) 171 | """ 172 | Sets the position of the widget on the outer_circle. Available options are 173 | `'left`', `'right`', `'top`', `'bottom`', `'left_top`', `'right_top`', 174 | `'left_bottom`', `'right_bottom`', `'center`'. 175 | 176 | :attr:`widget_position` is an :class:`~kivy.properties.OptionProperty` 177 | and defaults to `'left'`. 178 | """ 179 | 180 | title_position = OptionProperty( 181 | "auto", 182 | options=[ 183 | "auto", 184 | "left", 185 | "right", 186 | "top", 187 | "bottom", 188 | "left_top", 189 | "right_top", 190 | "left_bottom", 191 | "right_bottom", 192 | ], 193 | ) 194 | """ 195 | Sets the position of :attr`~title_text` on the outer circle. Only works if 196 | :attr`~widget_position` is set to `'center'`. In all other cases, it 197 | calculates the :attr`~title_position` itself. 198 | Must be set to other than `'auto`' when :attr`~widget_position` is set 199 | to `'center`'. 200 | Available options are `'auto'`, `'left`', `'right`', `'top`', `'bottom`', 201 | `'left_top`', `'right_top`', `'left_bottom`', `'right_bottom`', `'center`'. 202 | 203 | :attr:`title_position` is an :class:`~kivy.properties.OptionProperty` 204 | and defaults to `'auto'`. 205 | """ 206 | 207 | stop_on_outer_touch = BooleanProperty(False) 208 | """ 209 | Whether clicking on outer circle stops the animation. 210 | 211 | :attr:`stop_on_outer_touch` is an :class:`~kivy.properties.BooleanProperty` 212 | and defaults to `False`. 213 | """ 214 | 215 | stop_on_target_touch = BooleanProperty(True) 216 | """ 217 | Whether clicking on target circle should stop the animation. 218 | 219 | :attr:`stop_on_target_touch` is an :class:`~kivy.properties.BooleanProperty` 220 | and defaults to `True`. 221 | """ 222 | 223 | end = ObjectProperty() 224 | """ 225 | Function to be called when the animation stops. 226 | 227 | :attr:`end` is an :class:`~kivy.properties.ObjectProperty` 228 | and defaults to `None`. 229 | """ 230 | 231 | def __init__(self, **kwargs): 232 | self.ripple_max_dist = dp(90) 233 | self.outer_radius *= 2 234 | self.target_radius *= 2 235 | 236 | self.core_title_text = Label( 237 | markup=True, size_hint=(None, None), bold=self.title_text_bold 238 | ) 239 | self.core_title_text.bind(texture_size=self.core_title_text.setter("size")) 240 | self.core_description_text = Label(markup=True, size_hint=(None, None)) 241 | self.core_description_text.bind( 242 | texture_size=self.core_description_text.setter("size") 243 | ) 244 | 245 | super().__init__(**kwargs) 246 | self.register_event_type("on_outer_touch") 247 | self.register_event_type("on_target_touch") 248 | self.register_event_type("on_outside_click") 249 | 250 | def _initialize(self): 251 | setattr(self.widget, "outer_radius", 0) 252 | setattr(self.widget, "target_radius", 0) 253 | setattr(self.widget, "target_ripple_radius", 0) 254 | setattr(self.widget, "target_ripple_alpha", 0) 255 | 256 | # Bind some function on widget event when this function is called 257 | # instead of when the class itself is initialized to prevent all 258 | # widgets of all instances to get bind at once and start messing up. 259 | self.widget.bind(on_touch_down=self._some_func) 260 | 261 | def _draw_canvas(self): 262 | _pos = self._ttv_pos() 263 | self.widget.canvas.before.clear() 264 | 265 | with self.widget.canvas.before: 266 | # Outer circle. 267 | Color(*self.outer_circle_color, self.outer_circle_alpha, group="ttv_group") 268 | _rad1 = self.widget.outer_radius 269 | Ellipse(size=(_rad1, _rad1), pos=_pos[0], group="ttv_group") 270 | 271 | # Title text. 272 | Color(*self.title_text_color, group="ttv_group") 273 | Rectangle( 274 | size=self.core_title_text.texture.size, 275 | texture=self.core_title_text.texture, 276 | pos=_pos[1], 277 | group="ttv_group", 278 | ) 279 | 280 | # Description text. 281 | Color(*self.description_text_color, group="ttv_group") 282 | Rectangle( 283 | size=self.core_description_text.texture.size, 284 | texture=self.core_description_text.texture, 285 | pos=(_pos[1][0], _pos[1][1] - self.core_description_text.size[1] - 5), 286 | group="ttv_group", 287 | ) 288 | 289 | # Target circle. 290 | Color(*self.target_circle_color, group="ttv_group") 291 | _rad2 = self.widget.target_radius 292 | Ellipse( 293 | size=(_rad2, _rad2), 294 | pos=( 295 | self.widget.x - (_rad2 / 2 - self.widget.size[0] / 2), 296 | self.widget.y - (_rad2 / 2 - self.widget.size[0] / 2), 297 | ), 298 | group="ttv_group", 299 | ) 300 | 301 | # Target ripple. 302 | Color( 303 | *self.target_circle_color, 304 | self.widget.target_ripple_alpha, 305 | group="ttv_group", 306 | ) 307 | _rad3 = self.widget.target_ripple_radius 308 | Ellipse( 309 | size=(_rad3, _rad3), 310 | pos=( 311 | self.widget.x - (_rad3 / 2 - self.widget.size[0] / 2), 312 | self.widget.y - (_rad3 / 2 - self.widget.size[0] / 2), 313 | ), 314 | group="ttv_group", 315 | ) 316 | 317 | def stop(self, *args): 318 | # It needs a better implementation. 319 | self.anim_ripple.unbind(on_complete=self._repeat_ripple) 320 | self.description_text_color = [1, 1, 1, 0] 321 | self.title_text_color = [1, 1, 1, 0] 322 | anim = Animation( 323 | d=0.15, 324 | t="in_cubic", 325 | **dict( 326 | zip( 327 | ["outer_radius", "target_radius", "target_ripple_radius"], [0, 0, 0] 328 | ) 329 | ), 330 | ) 331 | anim.bind(on_complete=self._after_stop) 332 | anim.start(self.widget) 333 | 334 | def _after_stop(self, *args): 335 | self.widget.canvas.before.remove_group("ttv_group") 336 | args[0].stop_all(self.widget) 337 | elev = getattr(self.widget, "elevation", None) 338 | 339 | if elev: 340 | self._fix_elev() 341 | if self.end: 342 | self.end(self) 343 | 344 | # Don't forget to unbind the function or it'll mess 345 | # up with other next bindings. 346 | self.widget.unbind(on_touch_down=self._some_func) 347 | 348 | def _fix_elev(self): 349 | with self.widget.canvas.before: 350 | Color(a=self.widget._soft_shadow_a) 351 | Rectangle( 352 | texture=self.widget._soft_shadow_texture, 353 | size=self.widget._soft_shadow_size, 354 | pos=self.widget._soft_shadow_pos, 355 | ) 356 | Color(a=self.widget._hard_shadow_a) 357 | Rectangle( 358 | texture=self.widget._hard_shadow_texture, 359 | size=self.widget._hard_shadow_size, 360 | pos=self.widget._hard_shadow_pos, 361 | ) 362 | 363 | Color(a=1) 364 | 365 | def start(self, *args): 366 | self._initialize() 367 | self._animate_outer() 368 | 369 | def _animate_outer(self): 370 | anim = Animation( 371 | d=0.2, 372 | t="out_cubic", 373 | **dict( 374 | zip( 375 | ["outer_radius", "target_radius"], 376 | [self.outer_radius, self.target_radius], 377 | ) 378 | ), 379 | ) 380 | anim.cancel_all(self.widget) 381 | anim.bind(on_progress=lambda x, y, z: self._draw_canvas()) 382 | anim.bind(on_complete=self._animate_ripple) 383 | anim.start(self.widget) 384 | setattr(self.widget, "target_ripple_radius", self.target_radius) 385 | setattr(self.widget, "target_ripple_alpha", 1) 386 | 387 | def _animate_ripple(self, *args): 388 | self.anim_ripple = Animation( 389 | d=1, 390 | t="in_cubic", 391 | target_ripple_radius=self.target_radius + self.ripple_max_dist, 392 | target_ripple_alpha=0, 393 | ) 394 | self.anim_ripple.stop_all(self.widget) 395 | self.anim_ripple.bind(on_progress=lambda x, y, z: self._draw_canvas()) 396 | self.anim_ripple.bind(on_complete=self._repeat_ripple) 397 | self.anim_ripple.start(self.widget) 398 | 399 | def _repeat_ripple(self, *args): 400 | setattr(self.widget, "target_ripple_radius", self.target_radius) 401 | setattr(self.widget, "target_ripple_alpha", 1) 402 | self._animate_ripple() 403 | 404 | def on_description_text(self, instance, value): 405 | self.core_description_text.text = value 406 | 407 | def on_description_text_size(self, instance, value): 408 | self.core_description_text.font_size = value 409 | 410 | def on_description_text_bold(self, instance, value): 411 | self.core_description_text.bold = value 412 | 413 | def on_title_text(self, instance, value): 414 | self.core_title_text.text = value 415 | 416 | def on_title_text_size(self, instance, value): 417 | self.core_title_text.font_size = value 418 | 419 | def on_title_text_bold(self, instance, value): 420 | self.core_title_text.bold = value 421 | 422 | def on_target_touch(self): 423 | if self.stop_on_target_touch: 424 | self.stop() 425 | 426 | def on_outer_touch(self): 427 | if self.stop_on_outer_touch: 428 | self.stop() 429 | 430 | def on_outside_click(self): 431 | if self.cancelable: 432 | self.stop() 433 | 434 | def _some_func(self, wid, touch): 435 | """ 436 | This function decides which one to dispatch based on the touch 437 | position. 438 | """ 439 | 440 | if self._check_pos_target(touch.pos): 441 | self.dispatch("on_target_touch") 442 | elif self._check_pos_outer(touch.pos): 443 | self.dispatch("on_outer_touch") 444 | else: 445 | self.dispatch("on_outside_click") 446 | 447 | def _check_pos_outer(self, pos): 448 | """ 449 | Checks if a given `pos` coordinate is within the :attr:`~outer_radius`. 450 | """ 451 | 452 | cx = self.circ_pos[0] + self.outer_radius / 2 453 | cy = self.circ_pos[1] + self.outer_radius / 2 454 | r = self.outer_radius / 2 455 | h, k = pos 456 | 457 | lhs = (cx - h) ** 2 + (cy - k) ** 2 458 | rhs = r ** 2 459 | if lhs <= rhs: 460 | return True 461 | return False 462 | 463 | def _check_pos_target(self, pos): 464 | """ 465 | Checks if a given `pos` coordinate is within the 466 | :attr:`~target_radius`. 467 | """ 468 | 469 | cx = self.widget.pos[0] + self.widget.width / 2 470 | cy = self.widget.pos[1] + self.widget.height / 2 471 | r = self.target_radius / 2 472 | h, k = pos 473 | 474 | lhs = (cx - h) ** 2 + (cy - k) ** 2 475 | rhs = r ** 2 476 | if lhs <= rhs: 477 | return True 478 | return False 479 | 480 | def _ttv_pos(self): 481 | """ 482 | Calculates the `pos` value for outer circle and text 483 | based on the position provided. 484 | 485 | :returns: A tuple containing pos for the circle and text. 486 | """ 487 | 488 | _rad1 = self.widget.outer_radius 489 | _center_x = self.widget.x - (_rad1 / 2 - self.widget.size[0] / 2) 490 | _center_y = self.widget.y - (_rad1 / 2 - self.widget.size[0] / 2) 491 | 492 | if self.widget_position == "left": 493 | circ_pos = (_center_x + _rad1 / 3, _center_y) 494 | title_pos = (_center_x + _rad1 / 1.4, _center_y + _rad1 / 1.4) 495 | elif self.widget_position == "right": 496 | circ_pos = (_center_x - _rad1 / 3, _center_y) 497 | title_pos = (_center_x - _rad1 / 10, _center_y + _rad1 / 1.4) 498 | elif self.widget_position == "top": 499 | circ_pos = (_center_x, _center_y - _rad1 / 3) 500 | title_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 4) 501 | elif self.widget_position == "bottom": 502 | circ_pos = (_center_x, _center_y + _rad1 / 3) 503 | title_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 1.2) 504 | # Corner ones need to be at a little smaller distance 505 | # than edge ones that's why _rad1/4. 506 | elif self.widget_position == "left_top": 507 | circ_pos = (_center_x + _rad1 / 4, _center_y - _rad1 / 4) 508 | title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 4) 509 | elif self.widget_position == "right_top": 510 | circ_pos = (_center_x - _rad1 / 4, _center_y - _rad1 / 4) 511 | title_pos = (_center_x - _rad1 / 10, _center_y + _rad1 / 4) 512 | elif self.widget_position == "left_bottom": 513 | circ_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 4) 514 | title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 1.2) 515 | elif self.widget_position == "right_bottom": 516 | circ_pos = (_center_x - _rad1 / 4, _center_y + _rad1 / 4) 517 | title_pos = (_center_x, _center_y + _rad1 / 1.2) 518 | else: 519 | # Center. 520 | circ_pos = (_center_x, _center_y) 521 | 522 | if self.title_position == "auto": 523 | raise ValueError( 524 | "widget_position='center' requires title_position to be set." 525 | ) 526 | elif self.title_position == "left": 527 | title_pos = (_center_x + _rad1 / 10, _center_y + _rad1 / 2) 528 | elif self.title_position == "right": 529 | title_pos = (_center_x + _rad1 / 1.6, _center_y + _rad1 / 2) 530 | elif self.title_position == "top": 531 | title_pos = (_center_x + _rad1 / 2.5, _center_y + _rad1 / 1.3) 532 | elif self.title_position == "bottom": 533 | title_pos = (_center_x + _rad1 / 2.5, _center_y + _rad1 / 4) 534 | elif self.title_position == "left_top": 535 | title_pos = (_center_x + _rad1 / 8, _center_y + _rad1 / 1.4) 536 | elif self.title_position == "right_top": 537 | title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 1.3) 538 | elif self.title_position == "left_bottom": 539 | title_pos = (_center_x + _rad1 / 8, _center_y + _rad1 / 4) 540 | elif self.title_position == "right_bottom": 541 | title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 3.5) 542 | else: 543 | raise ValueError( 544 | f"'{self.title_position}'" 545 | f"is not a valid value for title_position" 546 | ) 547 | 548 | self.circ_pos = circ_pos 549 | return circ_pos, title_pos 550 | -------------------------------------------------------------------------------- /test/test1.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append("../") 4 | 5 | """ 6 | from taptargetview.taptargetview import TapTargetView 7 | """ 8 | --------------------------------------------------------------------------------