Kivy:如何将背景触摸与小部件触摸分开?

Kivy: How to separate background touch from a widget touch?

在我的应用中,我想分别处理背景触摸和小部件触摸。 Widget documentation 忽略了如何防止 .kv 事件冒泡。这是一个小测试用例:

from kivy.app import App

class TestApp(App):

  def on_background_touch(self):
    print("Background Touched")
    return True

  def on_button_touch(self):
    print("Button Touched")

if __name__ == "__main__":
  TestApp().run()

和.kv:

#:kivy 1.8.0

BoxLayout:
  orientation: "vertical"
  on_touch_down: app.on_background_touch()
  padding: 50, 50

  Button:
    text: "Touch me!"
    on_touch_down: app.on_button_touch()

结果:触摸背景或按钮都会触发两个处理程序。我应该执行碰撞检测,还是有其他方法?

您应该执行碰撞检测。例如,在 class 定义中:

class YourWidget(SomeWidget):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            do_stuff()

编辑:实际上,您的方法无论如何都行不通,因为 Button 与 BoxLayout 重叠。我可能会改为创建一个 BoxLayout subclass 并覆盖 on_touch_down,首先调用 super 然后如果它 returns False(表示尚未使用触摸)进行 BoxLayout 交互。

我想要一个允许我绑定来自 .kv 文件的事件的解决方案。 @inclement 解决方案不允许我这样做,因为一旦你从 .kv 绑定事件,你就不能再 return True 告诉 parent您处理了事件:

Button:
  # you can't return True here, neither from the handler itself
  on_touch_down: app.button_touched()

所以我所做的是在 parent 执行碰撞检测,仅当它没有击中任何 children 时才发出自定义 on_really_touch_down,并执行碰撞在 child 处再次检测到,因为所有 children 无论如何都会收到触摸(我知道这是一团糟)。这是完整的解决方案(需要 Kivy >= 1.9.0,因为使用 walk 方法):

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout

class CustomTouchMixin(object):

  def __init__(self, *args, **kwargs):
    super(CustomTouchMixin, self).__init__(*args, **kwargs)
    self.register_event_type("on_really_touch_down")

  def on_really_touch_down(self, touch):
    pass

class CustomTouchWidgetMixin(CustomTouchMixin):

  def on_touch_down(self, touch):
    if self.collide_point(*touch.pos):
      self.dispatch("on_really_touch_down", touch)
    return super(CustomTouchWidgetMixin, self).on_touch_down(touch)

class CustomTouchLayoutMixin(CustomTouchMixin):

  def on_touch_down(self, touch):
    for child in self.walk():
      if child is self: continue
      if child.collide_point(*touch.pos):
        # let the touch propagate to children
        return super(CustomTouchLayoutMixin, self).on_touch_down(touch)
    else:
      super(CustomTouchLayoutMixin, self).dispatch("on_really_touch_down", touch)
      return True

class TouchHandlerBoxLayout(CustomTouchLayoutMixin, BoxLayout):
  pass

class TouchAwareButton(CustomTouchWidgetMixin, Button):
  pass

class TestApp(App):

  def on_background_touch(self):
    print("Background Touched")

  def on_button_touch(self, button_text):
    print("'{}' Touched".format(button_text))

if __name__ == "__main__":
  TestApp().run()

.kv:

#:kivy 1.9.0

TouchHandlerBoxLayout:

  padding: 50, 50
  on_really_touch_down: app.on_background_touch()

  TouchAwareButton:
    text: "Button One"
    on_really_touch_down: app.on_button_touch(self.text)

  TouchAwareButton:
    text: "Button Two"
    on_really_touch_down: app.on_button_touch(self.text)

所以这允许我绑定来自 .kv 的触摸。

通过.kv file/string语法绑定触摸事件的方法是可行的,这里有一个例子,当检测到碰撞时修改调用者的背景。

<cLabel@Label>:
    padding: 5, 10
    default_background_color: 0, 0, 0, 0
    selected_background_color: 0, 1, 0, 1
    on_touch_down:
        ## First & second arguments passed when touches happen
        caller = args[0]
        touch = args[1]
        ## True or False for collisions & caller state
        caller_touched = caller.collide_point(*touch.pos)
        background_defaulted = caller.background_color == caller.default_background_color
        ## Modify caller state if touched
        if caller_touched and background_defaulted: caller.background_color = self.selected_background_color
        elif caller_touched and not background_defaulted: caller.background_color = caller.default_background_color

    background_color: 0, 0, 0, 0
    canvas.before:
        Color:
            rgba: self.background_color
        Rectangle:
            pos: self.pos
            size: self.size

为了完整起见,以下是如何在只有 children 的 none(或 grandchildren 等)具有触摸激活的布局中使用上述代码也撞上了同样的事件。

<cGrid@GridLayout>:
    on_touch_down:
        caller = args[0]
        touch = args[1]
        caller_touched = caller.collide_point(*touch.pos)
        spawn_touched = [x.collide_point(*touch.pos) for x in self.walk(restrict = True) if x is not self]
        ## Do stuff if touched and none of the spawn have been touched
        if caller_touched and True not in spawn_touched: print('caller -> {0}\ntouch -> {1}'.format(caller, touch))
    cols: 2
    size_hint_y: None
    height: sorted([x.height + x.padding[1] for x in self.children])[-1]
    cLabel:
        text: 'Foo'
        size_hint_y: None
        height: self.texture_size[1]
    cLabel:
        text: 'Bar'
        size_hint_y: None
        height: self.texture_size[1] * 2

I may have gotten the texture_size's backwards, or perhaps not, but the height trickery can be ignored for the most part as it's purpose is to aid in making the parent layout more clickable.

The color changing and printing of caller & touch objects should be replaced with do_stuff() or similar methods, as they're there to make the example self contained, and show another way handling caller saved state when touched.