Skip to main content

Overview

Android Code Studio includes a powerful UI Designer that allows you to visually design Android XML layouts using an intuitive drag-and-drop interface. Design layouts directly on your Android device without writing XML code.

Getting Started

The UI Designer is accessible from the editor when working with Android layout XML files:
class UIDesignerActivity : BaseIDEActivity() {
  private val viewModel by viewModels<WorkspaceViewModel>()
  
  companion object {
    const val EXTRA_FILE = "layout_file"
    
    fun launch(context: Context, layoutFile: File) {
      val intent = Intent(context, UIDesignerActivity::class.java)
      intent.putExtra(EXTRA_FILE, layoutFile)
      context.startActivity(intent)
    }
  }
}
Launch the UI Designer by selecting “Open in Designer” from the context menu of any layout XML file.

Features

Drag & Drop

Drag widgets from palette and drop onto canvas

Visual Editing

Edit view properties through an intuitive interface

Widget Hierarchy

View and manage the view hierarchy

Live Preview

See changes in real-time as you design

Attribute Editor

Comprehensive attribute editing with auto-completion

XML Generation

Generate clean, formatted XML code

Core Components

Designer Workspace

The main design surface where you build your layout:
class DesignerWorkspaceFragment : Fragment() {
  val workspaceView: ViewGroup
    get() = binding.designerCanvas
  
  private fun setupWorkspace() {
    // Setup canvas
    workspaceView.setOnDragListener(WidgetDragListener())
    
    // Enable touch interactions
    enableViewSelection()
    enableViewDragging()
  }
  
  fun addView(widget: View, params: ViewGroup.LayoutParams) {
    workspaceView.addView(widget, params)
    refreshHierarchy()
  }
}

Widget Palette

Browsable collection of Android widgets organized by category:
  • LinearLayout (Horizontal/Vertical)
  • RelativeLayout
  • FrameLayout
  • ConstraintLayout
  • GridLayout
  • ScrollView

Widget Adapter

class WidgetsItemAdapter(
  private val widgets: List<WidgetInfo>
) : RecyclerView.Adapter<WidgetViewHolder>() {
  
  override fun onBindViewHolder(holder: WidgetViewHolder, position: Int) {
    val widget = widgets[position]
    holder.bind(widget)
    
    // Setup drag behavior
    holder.itemView.setOnLongClickListener {
      val dragData = ClipData.newPlainText("widget", widget.className)
      val shadowBuilder = WidgetDragShadowBuilder(it)
      it.startDragAndDrop(dragData, shadowBuilder, widget, 0)
      true
    }
  }
}

Drag and Drop System

Widget Touch Listener

class WidgetTouchListener(
  private val workspace: ViewGroup
) : View.OnTouchListener {
  
  override fun onTouch(view: View, event: MotionEvent): Boolean {
    return when (event.action) {
      MotionEvent.ACTION_DOWN -> {
        // Prepare for drag
        val dragShadow = WidgetDragShadowBuilder(view)
        val data = ClipData.newPlainText("view_id", view.id.toString())
        view.startDragAndDrop(data, dragShadow, view, 0)
        true
      }
      else -> false
    }
  }
}

Drag Listener

class WidgetDragListener(
  private val workspace: ViewGroup
) : View.OnDragListener {
  
  override fun onDrag(view: View, event: DragEvent): Boolean {
    return when (event.action) {
      DragEvent.ACTION_DRAG_STARTED -> true
      
      DragEvent.ACTION_DRAG_ENTERED -> {
        // Highlight drop target
        view.background = highlightDrawable
        true
      }
      
      DragEvent.ACTION_DROP -> {
        // Handle drop
        val widget = createWidget(event.clipData)
        val params = calculateLayoutParams(event.x, event.y)
        workspace.addView(widget, params)
        true
      }
      
      DragEvent.ACTION_DRAG_ENDED -> {
        view.background = null
        true
      }
      
      else -> false
    }
  }
}

Custom Drag Shadow

class WidgetDragShadowBuilder(
  view: View
) : View.DragShadowBuilder(view) {
  
  private val shadow = ColorDrawable(Color.LTGRAY)
  
  override fun onProvideShadowMetrics(
    outShadowSize: Point,
    outShadowTouchPoint: Point
  ) {
    val width = view.width / 2
    val height = view.height / 2
    
    outShadowSize.set(width, height)
    outShadowTouchPoint.set(width / 2, height / 2)
  }
  
  override fun onDrawShadow(canvas: Canvas) {
    shadow.setBounds(0, 0, view.width, view.height)
    shadow.draw(canvas)
  }
}
Long-press on any widget in the palette to start dragging it to the canvas.

Attribute Editing

View Info Fragment

Display and edit selected view properties:
class ViewInfoFragment : Fragment() {
  private var selectedView: View? = null
  
  fun setSelectedView(view: View) {
    selectedView = view
    displayAttributes(view)
  }
  
  private fun displayAttributes(view: View) {
    val attrs = extractAttributes(view)
    binding.attributeList.adapter = ViewAttrListAdapter(attrs) { attr, value ->
      applyAttribute(view, attr, value)
    }
  }
  
  private fun applyAttribute(view: View, attr: String, value: String) {
    when (attr) {
      "android:text" -> (view as? TextView)?.text = value
      "android:textSize" -> (view as? TextView)?.textSize = value.toFloat()
      "android:layout_width" -> updateLayoutParam(view, "width", value)
      "android:layout_height" -> updateLayoutParam(view, "height", value)
      // ... more attributes
    }
  }
}

Attribute Adapter

class ViewAttrListAdapter(
  private val attributes: List<Attribute>,
  private val onAttributeChanged: (String, String) -> Unit
) : RecyclerView.Adapter<AttrViewHolder>() {
  
  override fun onBindViewHolder(holder: AttrViewHolder, position: Int) {
    val attr = attributes[position]
    holder.nameText.text = attr.name
    holder.valueInput.setText(attr.value)
    
    holder.valueInput.addTextChangedListener { text ->
      onAttributeChanged(attr.name, text.toString())
    }
  }
}

Add Attribute Dialog

class AddAttrFragment : DialogFragment() {
  
  private fun showAttributePicker() {
    val availableAttrs = listOf(
      "android:id",
      "android:text",
      "android:textColor",
      "android:textSize",
      "android:background",
      "android:padding",
      "android:margin",
      "android:layout_width",
      "android:layout_height",
      "android:gravity",
      // ... more attributes
    )
    
    binding.attrList.adapter = AddAttrListAdapter(availableAttrs) { attr ->
      addAttributeToView(attr)
      dismiss()
    }
  }
}
The attribute editor supports all standard Android view attributes:
  • Layout: width, height, margins, padding, gravity
  • Text: text, textSize, textColor, fontFamily
  • Appearance: background, foreground, elevation
  • Behavior: visibility, enabled, clickable
  • Constraints: layout_constraint* (for ConstraintLayout)

Hierarchy Management

Show Hierarchy Action

class ShowHierarchyAction : UiDesignerAction() {
  override fun execAction(data: ActionData): Boolean {
    val hierarchy = buildHierarchy(workspace.rootView)
    showHierarchyDialog(hierarchy)
    return true
  }
  
  private fun buildHierarchy(view: View): TreeNode {
    val node = TreeNode(view)
    if (view is ViewGroup) {
      for (i in 0 until view.childCount) {
        node.addChild(buildHierarchy(view.getChildAt(i)))
      }
    }
    return node
  }
}

View Hierarchy

ConstraintLayout
├── Toolbar
│   └── TextView (title)
├── LinearLayout
│   ├── ImageView
│   ├── TextView
│   └── Button
└── FloatingActionButton

XML Generation

View to XML Converter

object ViewToXml {
  fun generateXml(
    context: Context,
    view: View,
    onSuccess: (String) -> Unit,
    onError: (Result<String>, Throwable?) -> Unit
  ) {
    try {
      val xml = StringBuilder()
      xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
      
      writeView(view, xml, 0)
      
      onSuccess(formatXml(xml.toString()))
    } catch (e: Exception) {
      onError(Result.failure(e), e)
    }
  }
  
  private fun writeView(view: View, xml: StringBuilder, indent: Int) {
    val tag = getXmlTag(view)
    val attrs = extractAttributes(view)
    
    xml.append("  ".repeat(indent))
    xml.append("<$tag")
    
    // Write attributes
    attrs.forEach { (name, value) ->
      xml.append("\n")
      xml.append("  ".repeat(indent + 1))
      xml.append("$name=\"$value\"")
    }
    
    // Write children
    if (view is ViewGroup && view.childCount > 0) {
      xml.append(">\n")
      
      for (i in 0 until view.childCount) {
        writeView(view.getChildAt(i), xml, indent + 1)
      }
      
      xml.append("  ".repeat(indent))
      xml.append("</$tag>\n")
    } else {
      xml.append(" />\n")
    }
  }
}

Generated XML Example

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  android:padding="16dp">
  
  <TextView
    android:id="@+id/title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Hello World"
    android:textSize="24sp" />
  
  <Button
    android:id="@+id/button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Click Me" />
</LinearLayout>

Designer Actions

Undo/Redo System

class UndoAction : UiDesignerAction() {
  override fun execAction(data: ActionData): Boolean {
    val viewModel = data.get(WorkspaceViewModel::class.java)
    return viewModel.undo()
  }
}

class RedoAction : UiDesignerAction() {
  override fun execAction(data: ActionData): Boolean {
    val viewModel = data.get(WorkspaceViewModel::class.java)
    return viewModel.redo()
  }
}

Show XML Action

class ShowXmlAction : UiDesignerAction() {
  override fun execAction(data: ActionData): Boolean {
    val workspace = getWorkspace(data)
    
    ViewToXml.generateXml(
      context = data.requireContext(),
      view = workspace.workspaceView,
      onSuccess = { xml ->
        showXmlPreview(xml)
      },
      onError = { _, error ->
        showError("Failed to generate XML: ${error?.message}")
      }
    )
    
    return true
  }
}

Undo

Revert the last change to the layout

Redo

Reapply an undone change

Show XML

Preview generated XML code

Show Hierarchy

View the complete view hierarchy

Visual Enhancements

Layered Foreground

Visual indicator for selected views:
class UiViewLayeredForeground : Drawable() {
  private val borderPaint = Paint().apply {
    style = Paint.Style.STROKE
    strokeWidth = 4f
    color = Color.BLUE
  }
  
  private val cornerPaint = Paint().apply {
    style = Paint.Style.FILL
    color = Color.BLUE
  }
  
  override fun draw(canvas: Canvas) {
    // Draw border
    canvas.drawRect(bounds, borderPaint)
    
    // Draw corner handles
    val handleSize = 16f
    canvas.drawCircle(bounds.left.toFloat(), bounds.top.toFloat(), 
                     handleSize, cornerPaint)
    canvas.drawCircle(bounds.right.toFloat(), bounds.top.toFloat(), 
                     handleSize, cornerPaint)
    canvas.drawCircle(bounds.left.toFloat(), bounds.bottom.toFloat(), 
                     handleSize, cornerPaint)
    canvas.drawCircle(bounds.right.toFloat(), bounds.bottom.toFloat(), 
                     handleSize, cornerPaint)
  }
}

Best Practices

Save Frequently

Generate and save XML regularly to avoid losing work

Use Hierarchy

Check hierarchy view to ensure proper nesting

Test on Device

Preview layouts on actual device for accurate results

Validate XML

Review generated XML for correctness

Workflow Example

// 1. Open layout file in designer
val layoutFile = File(projectDir, "app/src/main/res/layout/activity_main.xml")
UIDesignerActivity.launch(context, layoutFile)

// 2. User designs layout with drag-and-drop
// - Drag LinearLayout from palette
// - Drop onto canvas
// - Drag TextView and Button into LinearLayout
// - Edit attributes in property panel

// 3. Generate XML
ViewToXml.generateXml(context, rootView) { xml ->
  // 4. Save to file
  layoutFile.writeText(xml)
  
  // 5. Return to editor
  finish()
}
The UI Designer works best with simple to moderately complex layouts. For very complex layouts with custom views, consider editing XML directly.

Build docs developers (and LLMs) love