diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/BrushAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/BrushAdapter.kt
new file mode 100644
index 000000000000..020d4cddbbd6
--- /dev/null
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/BrushAdapter.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2025 Brayan Oliveira <69634269+brayandso@users.noreply.github.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+package com.ichi2.anki.ui.windows.reviewer.whiteboard
+
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.LayerDrawable
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.button.MaterialButton
+import com.ichi2.anki.R
+import kotlin.math.roundToInt
+
+/**
+ * Adapter for displaying a list of brushes in a RecyclerView.
+ *
+ * @param onBrushClick Callback when a brush color is clicked.
+ * @param onBrushLongClick Callback when a brush color is long-clicked.
+ */
+class BrushAdapter(
+ private val onBrushClick: (View, Int) -> Unit,
+ private val onBrushLongClick: (Int) -> Unit,
+) : RecyclerView.Adapter() {
+ private var brushes: List = emptyList()
+ private var activeIndex: Int = -1
+ private var isEraserActive: Boolean = false
+
+ fun updateData(
+ newBrushes: List,
+ newActiveIndex: Int,
+ eraserActive: Boolean,
+ ) {
+ brushes = newBrushes
+ activeIndex = newActiveIndex
+ isEraserActive = eraserActive
+ notifyDataSetChanged()
+ }
+
+ fun updateSelection(
+ newActiveIndex: Int,
+ eraserActive: Boolean,
+ ) {
+ val oldIndex = activeIndex
+ activeIndex = newActiveIndex
+ isEraserActive = eraserActive
+
+ if (oldIndex in brushes.indices) notifyItemChanged(oldIndex)
+ if (newActiveIndex in brushes.indices) notifyItemChanged(newActiveIndex)
+ }
+
+ override fun getItemCount(): Int = brushes.size
+
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int,
+ ): BrushViewHolder {
+ val inflater = LayoutInflater.from(parent.context)
+ val itemView = inflater.inflate(R.layout.button_color_brush, parent, false)
+ return BrushViewHolder(itemView)
+ }
+
+ override fun onBindViewHolder(
+ holder: BrushViewHolder,
+ position: Int,
+ ) {
+ val brush = brushes[position]
+ holder.bind(brush, position == activeIndex && !isEraserActive)
+ }
+
+ inner class BrushViewHolder(
+ itemView: View,
+ ) : RecyclerView.ViewHolder(itemView) {
+ private val button: MaterialButton = itemView as MaterialButton
+
+ fun bind(
+ brush: BrushInfo,
+ isSelected: Boolean,
+ ) = button.apply {
+ isCheckable = true
+ isChecked = isSelected
+ text = brush.width.roundToInt().toString()
+ iconTint = null
+
+ val layer = icon?.mutate() as? LayerDrawable
+ val fill = layer?.findDrawableByLayerId(R.id.brush_preview_fill) as? GradientDrawable
+ fill?.setColor(brush.color)
+
+ setOnClickListener { onBrushClick(it, bindingAdapterPosition) }
+ setOnLongClickListener {
+ onBrushLongClick(bindingAdapterPosition)
+ true
+ }
+ }
+ }
+}
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt
index 747c9005ce28..4493d0da5d94 100644
--- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardFragment.kt
@@ -15,33 +15,31 @@
*/
package com.ichi2.anki.ui.windows.reviewer.whiteboard
+import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
+import android.view.Gravity
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
-import android.widget.LinearLayout
+import android.widget.FrameLayout
import android.widget.PopupWindow
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu
-import androidx.appcompat.widget.ThemeUtils
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.core.view.children
+import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
-import com.google.android.material.button.MaterialButton
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.R
import com.ichi2.anki.databinding.FragmentWhiteboardBinding
import com.ichi2.anki.databinding.PopupBrushOptionsBinding
import com.ichi2.anki.databinding.PopupEraserOptionsBinding
import com.ichi2.anki.snackbar.showSnackbar
-import com.ichi2.compat.setTooltipTextCompat
import com.ichi2.themes.Themes
import com.ichi2.utils.dp
import com.ichi2.utils.increaseHorizontalPaddingOfMenuIcons
@@ -69,9 +67,6 @@ class WhiteboardFragment :
private var eraserPopup: PopupWindow? = null
private var brushConfigPopup: PopupWindow? = null
- /**
- * Sets up the view, observers, and event listeners.
- */
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
@@ -91,8 +86,10 @@ class WhiteboardFragment :
}
private fun setupUI() {
- binding.overflowMenuButton.setOnClickListener {
- val popupMenu = PopupMenu(requireContext(), binding.overflowMenuButton)
+ val toolbar = binding.whiteboardToolbar
+
+ toolbar.overflowButton.setOnClickListener {
+ val popupMenu = PopupMenu(requireContext(), toolbar.overflowButton)
requireActivity().menuInflater.inflate(R.menu.whiteboard, popupMenu.menu)
with(popupMenu.menu) {
findItem(R.id.action_toggle_stylus).isChecked = viewModel.isStylusOnlyMode.value
@@ -111,11 +108,11 @@ class WhiteboardFragment :
popupMenu.show()
}
- binding.undoButton.setOnClickListener { viewModel.undo() }
- binding.redoButton.setOnClickListener { viewModel.redo() }
- binding.eraserButton.setOnClickListener {
+ toolbar.undoButton.setOnClickListener { viewModel.undo() }
+ toolbar.redoButton.setOnClickListener { viewModel.redo() }
+ toolbar.eraserButton.setOnClickListener {
if (viewModel.isEraserActive.value) {
- binding.eraserButton.isChecked = true
+ toolbar.eraserButton.isChecked = true
if (eraserPopup?.isShowing == true) {
eraserPopup?.dismiss()
} else {
@@ -126,14 +123,33 @@ class WhiteboardFragment :
}
}
- viewModel.canUndo.onEach { binding.undoButton.isEnabled = it }.launchIn(lifecycleScope)
- viewModel.canRedo.onEach { binding.redoButton.isEnabled = it }.launchIn(lifecycleScope)
+ toolbar.onBrushClick = { view, index ->
+ if (viewModel.activeBrushIndex.value == index && !viewModel.isEraserActive.value) {
+ showBrushConfigurationPopup(view, index)
+ } else {
+ viewModel.setActiveBrush(index)
+ }
+ }
+
+ toolbar.onBrushLongClick = { index ->
+ if (viewModel.brushes.value.size > 1) {
+ showRemoveColorDialog(index)
+ } else {
+ Timber.i("Tried to remove the last brush of the whiteboard")
+ showSnackbar(R.string.cannot_remove_last_brush_message)
+ }
+ }
+
+ viewModel.canUndo.onEach { toolbar.undoButton.isEnabled = it }.launchIn(lifecycleScope)
+ viewModel.canRedo.onEach { toolbar.redoButton.isEnabled = it }.launchIn(lifecycleScope)
}
/**
- * Sets up observers for the ViewModel's state flows.
+ * Sets up observers for the ViewModel's flows.
*/
private fun observeViewModel(whiteboardView: WhiteboardView) {
+ val toolbar = binding.whiteboardToolbar
+
viewModel.paths.onEach(whiteboardView::setHistory).launchIn(lifecycleScope)
combine(
@@ -149,7 +165,7 @@ class WhiteboardFragment :
viewModel.eraserDisplayWidth,
) { isActive, mode, width ->
whiteboardView.isEraserActive = isActive
- binding.eraserButton.updateState(isActive, mode, width)
+ toolbar.eraserButton.updateState(isActive, mode, width)
whiteboardView.eraserMode = mode
if (!isActive) {
eraserPopup?.dismiss()
@@ -158,14 +174,17 @@ class WhiteboardFragment :
viewModel.brushes
.onEach { brushesInfo ->
- updateBrushToolbar(brushesInfo)
- updateToolbarSelection()
+ toolbar.setBrushes(brushesInfo, viewModel.activeBrushIndex.value, viewModel.isEraserActive.value)
+ }.launchIn(lifecycleScope)
+
+ viewModel.activeBrushIndex
+ .onEach {
+ toolbar.updateSelection(it, viewModel.isEraserActive.value)
}.launchIn(lifecycleScope)
- viewModel.activeBrushIndex.onEach { updateToolbarSelection() }.launchIn(lifecycleScope)
viewModel.isEraserActive
.onEach {
- updateToolbarSelection()
+ toolbar.updateSelection(viewModel.activeBrushIndex.value, it)
}.launchIn(lifecycleScope)
viewModel.isStylusOnlyMode
@@ -175,91 +194,13 @@ class WhiteboardFragment :
viewModel.toolbarAlignment
.onEach { alignment ->
- updateLayoutForAlignment(alignment)
+ toolbar.setAlignment(alignment)
+ updateToolbarPosition(alignment)
}.launchIn(lifecycleScope)
}
- private fun updateBrushToolbar(brushesInfo: List) {
- binding.brushToolbarContainerHorizontal.removeAllViews()
- binding.brushToolbarContainerVertical.removeAllViews()
- brushesInfo.forEachIndexed { index, brush ->
- val inflater = LayoutInflater.from(requireContext())
- val buttonHorizontal =
- inflater.inflate(
- R.layout.button_color_brush,
- binding.brushToolbarContainerHorizontal,
- false,
- ) as MaterialButton
- configureBrushButton(buttonHorizontal, brush, index)
- binding.brushToolbarContainerHorizontal.addView(buttonHorizontal)
-
- val buttonVertical =
- inflater.inflate(
- R.layout.button_color_brush,
- binding.brushToolbarContainerVertical,
- false,
- ) as MaterialButton
- configureBrushButton(buttonVertical, brush, index)
- binding.brushToolbarContainerVertical.addView(buttonVertical)
- }
- }
-
/**
- * Configures a brush button's properties and listeners.
- */
- private fun configureBrushButton(
- button: MaterialButton,
- brush: BrushInfo,
- index: Int,
- ) {
- button.isCheckable = true
- button.text = brush.width.roundToInt().toString()
- button.tag = index
- button.iconTint = null
-
- (button.icon?.mutate() as? LayerDrawable)?.let { layerDrawable ->
- (layerDrawable.findDrawableByLayerId(R.id.brush_preview_fill) as? GradientDrawable)?.setColor(brush.color)
- }
-
- button.setOnClickListener {
- if (viewModel.activeBrushIndex.value == index && !viewModel.isEraserActive.value) {
- button.isChecked = true
- showBrushConfigurationPopup(it, index)
- } else {
- viewModel.setActiveBrush(index)
- }
- }
-
- button.setOnLongClickListener {
- if (viewModel.brushes.value.size > 1) {
- showRemoveColorDialog(index)
- } else {
- Timber.i("Tried to remove the last brush of the whiteboard")
- showSnackbar(R.string.cannot_remove_last_brush_message)
- }
- true
- }
- }
-
- /**
- * Updates the selection state of the eraser and brush buttons.
- */
- private fun updateToolbarSelection() {
- val activeIndex = viewModel.activeBrushIndex.value
- val isEraserActive = viewModel.isEraserActive.value
-
- val configureSelection: (View) -> Unit = { view ->
- val button = view as MaterialButton
- val buttonIndex = button.tag as? Int
- button.isChecked = (buttonIndex == activeIndex && !isEraserActive)
- }
-
- binding.brushToolbarContainerHorizontal.children.forEach(configureSelection)
- binding.brushToolbarContainerVertical.children.forEach(configureSelection)
- }
-
- /**
- * Shows a popup for adding a new brush color.
+ * Shows a dialog for adding a new brush color.
*/
private fun showAddColorDialog() {
ColorPickerPopUp(context).run {
@@ -416,7 +357,7 @@ class WhiteboardFragment :
eraserPopup = PopupWindow(eraserWidthBinding.root, 360.dp.toPx(requireContext()), ViewGroup.LayoutParams.WRAP_CONTENT, true)
eraserPopup?.elevation = 8f
eraserPopup?.setOnDismissListener {
- updateToolbarSelection()
+ binding.whiteboardToolbar.updateSelection(viewModel.activeBrushIndex.value, viewModel.isEraserActive.value)
eraserPopup = null
}
@@ -426,71 +367,16 @@ class WhiteboardFragment :
eraserPopup?.showAsDropDown(anchorView, xOffset, yOffset)
}
- /**
- * Updates the toolbar's constraints and orientation.
- */
- private fun updateLayoutForAlignment(alignment: ToolbarAlignment) {
- val isVertical = alignment == ToolbarAlignment.LEFT || alignment == ToolbarAlignment.RIGHT
- binding.innerControlsLayout.orientation = if (isVertical) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
-
- if (isVertical) {
- binding.brushScrollViewHorizontal.visibility = View.GONE
- binding.brushScrollViewVertical.visibility = View.VISIBLE
- } else {
- binding.brushScrollViewHorizontal.visibility = View.VISIBLE
- binding.brushScrollViewVertical.visibility = View.GONE
- }
-
- val dp = 1.dp.toPx(requireContext())
- val dividerParams = binding.controlsDivider.layoutParams as LinearLayout.LayoutParams
- val dividerMargin = 4 * dp
- if (isVertical) {
- dividerParams.width = LinearLayout.LayoutParams.MATCH_PARENT
- dividerParams.height = 1 * dp
- dividerParams.setMargins(0, dividerMargin, 0, dividerMargin)
- } else {
- dividerParams.width = 1 * dp
- dividerParams.height = LinearLayout.LayoutParams.MATCH_PARENT
- dividerParams.setMargins(dividerMargin, 0, dividerMargin, 0)
- }
- binding.controlsDivider.layoutParams = dividerParams
-
- val constraintSet = ConstraintSet()
- constraintSet.clone(binding.root)
- val containerId = binding.controlsContainer.id
- constraintSet.clear(containerId)
-
- when (alignment) {
- ToolbarAlignment.BOTTOM -> {
- constraintSet.connect(containerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
- constraintSet.connect(containerId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
- constraintSet.connect(containerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
- constraintSet.constrainWidth(containerId, ConstraintSet.WRAP_CONTENT)
- constraintSet.constrainHeight(containerId, ConstraintSet.WRAP_CONTENT)
- constraintSet.constrainedWidth(containerId, true)
- constraintSet.setMargin(containerId, ConstraintSet.START, 24 * dp)
- constraintSet.setMargin(containerId, ConstraintSet.END, 24 * dp)
- constraintSet.setMargin(containerId, ConstraintSet.BOTTOM, 8 * dp)
- }
- ToolbarAlignment.RIGHT -> {
- constraintSet.connect(containerId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END)
- constraintSet.connect(containerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
- constraintSet.connect(containerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
- constraintSet.constrainWidth(containerId, ConstraintSet.WRAP_CONTENT)
- constraintSet.constrainHeight(containerId, ConstraintSet.WRAP_CONTENT)
- constraintSet.setMargin(containerId, ConstraintSet.END, 8 * dp)
- }
- ToolbarAlignment.LEFT -> {
- constraintSet.connect(containerId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START)
- constraintSet.connect(containerId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP)
- constraintSet.connect(containerId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM)
- constraintSet.constrainWidth(containerId, ConstraintSet.WRAP_CONTENT)
- constraintSet.constrainHeight(containerId, ConstraintSet.WRAP_CONTENT)
- constraintSet.setMargin(containerId, ConstraintSet.START, 8 * dp)
- }
+ @SuppressLint("RtlHardcoded")
+ private fun updateToolbarPosition(alignment: ToolbarAlignment) {
+ binding.whiteboardToolbar.updateLayoutParams {
+ gravity =
+ when (alignment) {
+ ToolbarAlignment.BOTTOM -> Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
+ ToolbarAlignment.LEFT -> Gravity.LEFT or Gravity.CENTER_VERTICAL
+ ToolbarAlignment.RIGHT -> Gravity.RIGHT or Gravity.CENTER_VERTICAL
+ }
}
-
- constraintSet.applyTo(binding.root)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardToolbar.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardToolbar.kt
new file mode 100644
index 000000000000..b7121bf5bc32
--- /dev/null
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/whiteboard/WhiteboardToolbar.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2025 Brayan Oliveira <69634269+brayandso@users.noreply.github.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3 of the License, or (at your option) any later
+ * version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ */
+package com.ichi2.anki.ui.windows.reviewer.whiteboard
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.view.updateLayoutParams
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.card.MaterialCardView
+import com.ichi2.anki.databinding.ViewWhiteboardToolbarBinding
+import com.ichi2.utils.dp
+
+/**
+ * Tools configuration bar to be used along [WhiteboardView]
+ */
+class WhiteboardToolbar : MaterialCardView {
+ private val binding: ViewWhiteboardToolbarBinding
+ private val brushAdapter: BrushAdapter
+
+ constructor(context: Context) : this(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, com.google.android.material.R.attr.materialCardViewStyle)
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
+ binding = ViewWhiteboardToolbarBinding.inflate(LayoutInflater.from(context), this)
+
+ brushAdapter =
+ BrushAdapter(
+ onBrushClick = { view, index -> onBrushClick?.invoke(view, index) },
+ onBrushLongClick = { index -> onBrushLongClick?.invoke(index) },
+ )
+
+ binding.brushRecyclerView.apply {
+ layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ adapter = brushAdapter
+ }
+ }
+
+ val undoButton get() = binding.undoButton
+ val redoButton get() = binding.redoButton
+ val eraserButton get() = binding.eraserButton
+ val overflowButton get() = binding.overflowMenuButton
+
+ var onBrushClick: ((view: View, index: Int) -> Unit)? = null
+ var onBrushLongClick: ((index: Int) -> Unit)? = null
+
+ /**
+ * Updates the internal layout based on the toolbar alignment.
+ * Switches the RecyclerView orientation and the main layout orientation.
+ */
+ fun setAlignment(alignment: ToolbarAlignment) {
+ val isVertical = alignment == ToolbarAlignment.LEFT || alignment == ToolbarAlignment.RIGHT
+
+ binding.innerControlsLayout.orientation = if (isVertical) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
+
+ val layoutManager = binding.brushRecyclerView.layoutManager as? LinearLayoutManager
+ layoutManager?.orientation = if (isVertical) LinearLayoutManager.VERTICAL else LinearLayoutManager.HORIZONTAL
+
+ val dp = 1.dp.toPx(context)
+ val dividerMargin = 4 * dp
+ val dividerParams = binding.controlsDivider.layoutParams as LinearLayout.LayoutParams
+ if (isVertical) {
+ dividerParams.width = LinearLayout.LayoutParams.MATCH_PARENT
+ dividerParams.height = 1 * dp
+ dividerParams.setMargins(0, dividerMargin, 0, dividerMargin)
+ binding.innerControlsLayout.updateLayoutParams {
+ marginEnd = 0
+ }
+ } else {
+ dividerParams.width = 1 * dp
+ dividerParams.height = LinearLayout.LayoutParams.MATCH_PARENT
+ dividerParams.setMargins(dividerMargin, 0, dividerMargin, 0)
+ // leave some space after the brushes
+ binding.innerControlsLayout.updateLayoutParams {
+ marginEnd = dividerMargin
+ }
+ }
+ }
+
+ /**
+ * Updates the data in the RecyclerView adapter.
+ */
+ fun setBrushes(
+ brushes: List,
+ activeIndex: Int,
+ isEraserActive: Boolean,
+ ) {
+ brushAdapter.updateData(brushes, activeIndex, isEraserActive)
+ }
+
+ /**
+ * Updates the checked state of the brush buttons in the adapter.
+ */
+ fun updateSelection(
+ activeIndex: Int,
+ isEraserActive: Boolean,
+ ) {
+ brushAdapter.updateSelection(activeIndex, isEraserActive)
+ }
+}
diff --git a/AnkiDroid/src/main/res/layout/fragment_whiteboard.xml b/AnkiDroid/src/main/res/layout/fragment_whiteboard.xml
index 37c5db67c23a..c3093a52d20d 100644
--- a/AnkiDroid/src/main/res/layout/fragment_whiteboard.xml
+++ b/AnkiDroid/src/main/res/layout/fragment_whiteboard.xml
@@ -1,128 +1,19 @@
-
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
-
+ android:layout_margin="8dp"
+ android:layout_gravity="bottom|center_horizontal"
+ />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/AnkiDroid/src/main/res/layout/view_whiteboard_toolbar.xml b/AnkiDroid/src/main/res/layout/view_whiteboard_toolbar.xml
new file mode 100644
index 000000000000..86bd983cf432
--- /dev/null
+++ b/AnkiDroid/src/main/res/layout/view_whiteboard_toolbar.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file