Простой scrollbar в Defold

Как создать скролл бар


Компоненты скролл бара:

Viewport (Область просмотра / «Окно»)

  • Что это: Невидимая (или видимая) рамка, через которую пользователь смотрит на контент.
  • Зачем: Ограничивает видимую область. Всё, что выходит за её границы, автоматически обрезается.
  • В Defold: Обычно это Box -нода с включённым свойством Clipping: ON .

Content (Содержимое / «Длинная лента»)

  • Что это: Контейнер, в котором лежат все ваши элементы (строки списка, картинки, текст).
  • Зачем: Может быть в 2, 5 или 10 раз выше viewport. Когда вы «скроллите», вы на самом деле меняете position.y этого контейнера , двигая его внутри окна.
  • В Defold: Дочерний элемент viewport . Координаты его дочерних нод считаются относительно него, что упрощает вёрстку.

Scrollbar (Трек / Полоса прокрутки)

  • Что это: Статичный фон или «рельсы» справа/слева от контента.
  • Зачем:
    1. Визуально сообщает пользователю: «Здесь есть контент за пределами экрана» .
    2. Служит опорой для расчёта позиции ползунка. Его высота используется как 100% диапазона прокрутки.
  • В Defold: Обычная Box -нода. Не двигается, не обрабатывает ввод (если не хотите кликать по треку для прыжка).

Handle (Ползунок / Бегунок)

  • Что это: Подвижный элемент на треке, который можно тащить пальцем/мышью.
  • Зачем:
    1. Показывает текущую позицию внутри длинного списка.
    2. Даёт инструмент для быстрой навигации (drag & drop).
    3. Его высота обратно пропорциональна длине контента: чем длиннее список, тем короче ползунок (как в реальных приложениях).
  • В Defold: Box -нода. Двигается программно через gui.set_position() , синхронно с content , но в уменьшенном масштабе и с инверсией оси Y.
gui_script:
local FIXED_HANDLE_HEIGHT = 60
local INERTIA_DAMPING     = 5.0
local INERTIA_THRESHOLD   = 5.0
local VELOCITY_MIN        = 15.0

-- ======================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ========================

local function update_scroll(self, new_offset)
	self.scroll_offset = math.min(math.max(new_offset or 0, 0), self.scroll_max)

	gui.set_position(self.content, vmath.vector3(self.content_init_pos.x, self.content_init_pos.y + self.scroll_offset, 0))

	if self.scroll_max > 0 and self.scrollbar and self.handle then
		local track_h  = gui.get_size(self.scrollbar).y
		local handle_h = gui.get_size(self.handle).y
		local free     = track_h - handle_h
		local ratio    = self.scroll_offset / self.scroll_max

		local hx = self.scrollbar_init_pos.x
		local hy = self.scrollbar_init_pos.y + free * (1.0 - ratio)
		gui.set_position(self.handle, vmath.vector3(hx, hy, 0))
	end
end

local function recalculate_scroll(self)
	if not self.viewport or not self.content then return end

	local content_h  = gui.get_size(self.content).y
	local viewport_h = gui.get_size(self.viewport).y
	self.scroll_max  = math.max(0, content_h - viewport_h)

	local track_h  = gui.get_size(self.scrollbar).y
	local handle_h = math.min(FIXED_HANDLE_HEIGHT, track_h)
	gui.set_size(self.handle, vmath.vector3(gui.get_size(self.handle).x, handle_h, 0))
	gui.set_visible(self.handle, self.scroll_max > 0)

	update_scroll(self, self.scroll_offset)
end

-- ======================== ОСНОВНЫЕ ФУНКЦИИ ========================

function init(self)
	self.viewport  = gui.get_node("viewport")
	self.content   = gui.get_node("content")
	self.scrollbar = gui.get_node("scrollbar")
	self.handle    = gui.get_node("handle")

	self.content_init_pos   = gui.get_position(self.content)
	self.scrollbar_init_pos = gui.get_position(self.scrollbar)

	self.scroll_offset       = 0
	self.scroll_max          = 0
	self.is_dragging         = false
	self.drag_start_y        = 0
	self.scroll_start_offset = 0
	self.scroll_velocity     = 0
	self.scroll_inertial     = false
	self.last_touch_y        = 0
	self.last_touch_time     = os.clock() 
	self.row_count           = 0

	recalculate_scroll(self)
	update_scroll(self, 0)
	msg.post(".", "acquire_input_focus")
end

function update(self, dt)
	if self.scroll_inertial and self.scroll_max > 0 then
		self.scroll_offset = self.scroll_offset + self.scroll_velocity * dt
		self.scroll_velocity = self.scroll_velocity * math.exp(-INERTIA_DAMPING * dt)

		if self.scroll_offset <= 0 then
			self.scroll_offset = 0
			self.scroll_velocity = 0
			self.scroll_inertial = false
		elseif self.scroll_offset >= self.scroll_max then
			self.scroll_offset = self.scroll_max
			self.scroll_velocity = 0
			self.scroll_inertial = false
		elseif math.abs(self.scroll_velocity) < INERTIA_THRESHOLD then
			self.scroll_velocity = 0
			self.scroll_inertial = false
		end

		update_scroll(self, self.scroll_offset)
	end
end

function on_input(self, action_id, action)
	-- Поддержка тача и мыши
	if action_id ~= hash("touch") and action_id ~= hash("click") then return false end

	local x, y = action.x, action.y
	local on_handle   = self.handle   and gui.pick_node(self.handle, x, y)
	local on_viewport = self.viewport and gui.pick_node(self.viewport, x, y)

	if action.pressed and (on_handle or on_viewport) then
		self.is_dragging         = true
		self.scroll_inertial     = false
		self.drag_start_y        = y
		self.scroll_start_offset = self.scroll_offset
		self.scroll_velocity     = 0
		self.last_touch_y        = y
		self.last_touch_time     = os.clock()
		return true

	elseif self.is_dragging then
		if action.released then
			self.is_dragging = false
			if math.abs(self.scroll_velocity) > VELOCITY_MIN then
				self.scroll_inertial = true
			else
				self.scroll_velocity = 0
				self.scroll_inertial = false
			end
			return true
		end

		local now = os.clock() -- ← os.clock()
		local dt  = now - self.last_touch_time
		if dt > 0.001 then
			self.scroll_velocity = (self.last_touch_y - y) / dt
			self.last_touch_y    = y
			self.last_touch_time = now
		end

		local dy = self.drag_start_y - y
		update_scroll(self, self.scroll_start_offset + dy)
		return true
	end
	return false
end

GUI file:
script: "/main/scrollbar.gui_script"
fonts {
  name: "default"
  font: "/builtins/fonts/default.font"
}
nodes {
  position {
    x: 491.0
    y: 290.0
  }
  size {
    x: 400.0
    y: 400.0
  }
  type: TYPE_BOX
  id: "root_scrool"
  inherit_alpha: true
  size_mode: SIZE_MODE_AUTO
  visible: false
}
nodes {
  position {
    x: 155.0
    y: -169.0
  }
  size {
    x: 30.0
    y: 350.0
  }
  color {
    x: 0.4
    y: 0.4
    z: 0.4
  }
  type: TYPE_BOX
  id: "scrollbar"
  pivot: PIVOT_SW
  parent: "root_scrool"
  inherit_alpha: true
}
nodes {
  position {
    x: -252.40244
    y: -168.60919
  }
  size {
    x: 400.0
    y: 350.0
  }
  color {
    x: 0.6
    y: 0.0
    z: 0.0
  }
  type: TYPE_BOX
  id: "viewport"
  pivot: PIVOT_SW
  parent: "root_scrool"
  inherit_alpha: true
  clipping_mode: CLIPPING_MODE_STENCIL
  size_mode: SIZE_MODE_AUTO
}
nodes {
  position {
    x: 10.84433
    y: 346.75378
  }
  size {
    x: 350.0
    y: 600.0
  }
  color {
    x: 0.2
    y: 0.0
    z: 0.2
  }
  type: TYPE_BOX
  id: "content"
  pivot: PIVOT_NW
  parent: "viewport"
  inherit_alpha: true
}
nodes {
  position {
    x: 6.396591
    y: -4.561554
  }
  size {
    x: 200.0
    y: 100.0
  }
  type: TYPE_TEXT
  text: "<text>"
  font: "default"
  id: "text"
  pivot: PIVOT_NW
  parent: "content"
  inherit_alpha: true
}
nodes {
  position {
    x: 155.0
    y: -169.0
  }
  size {
    x: 30.0
    y: 30.0
  }
  type: TYPE_BOX
  id: "handle"
  pivot: PIVOT_SW
  parent: "root_scrool"
  inherit_alpha: true
  size_mode: SIZE_MODE_AUTO
}
material: "/builtins/materials/gui.material"
adjust_reference: ADJUST_REFERENCE_PARENT