Unit 2 - Notes

CSE227 11 min read

Unit 2: Advanced Graphics

1. The Canvas API

The Canvas API in Android is a low-level 2D drawing framework that allows you to draw anything you want directly onto a bitmap or a View. It's the foundation for creating custom views, charts, graphs, games, and other complex visual elements.

1.1. Core Concepts

  • Canvas: The Canvas object is your "drawing board." It provides the methods for drawing, such as drawRect(), drawPath(), drawText(), etc. You are given a Canvas object in the onDraw() method of a custom View.
  • Paint: The Paint object is your "brush." It holds the style and color information about how to draw. This includes properties like color, stroke width, text size, anti-aliasing, and shading effects. You should always create and configure Paint objects once during initialization, not inside onDraw(), as onDraw() is called frequently and object creation is expensive.
  • Bitmap: A Bitmap is the actual surface you are drawing onto. When you draw on a View's Canvas, you are implicitly drawing on the Bitmap that backs that View.

1.2. Creating a Custom View

To use the Canvas, you typically create a custom class that extends View and override its onDraw() method.

KOTLIN
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View

class MyCustomView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    // Initialize Paint objects here to avoid object creation in onDraw()
    private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
        color = Color.BLUE
    }

    private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.WHITE
        textSize = 60f
        textAlign = Paint.Align.CENTER
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // For simplicity, we'll use a fixed size.
        // In a real app, you should calculate size based on MeasureSpec.
        setMeasuredDimension(300, 300)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        // The canvas is provided for you. Do not create a new one.

        val viewWidth = width.toFloat()
        val viewHeight = height.toFloat()
        val centerX = viewWidth / 2
        val centerY = viewHeight / 2
        val radius = viewWidth / 3

        // Draw a blue circle in the center
        canvas.drawCircle(centerX, centerY, radius, circlePaint)

        // Draw text inside the circle
        canvas.drawText("Hello", centerX, centerY, textPaint)
    }
}

1.3. Drawing Primitives

The Canvas object provides methods to draw various shapes.

  • drawRect(left, top, right, bottom, paint): Draws a rectangle.
  • drawCircle(cx, cy, radius, paint): Draws a circle at center (cx, cy).
  • drawLine(startX, startY, stopX, stopY, paint): Draws a line.
  • drawOval(left, top, right, bottom, paint): Draws an oval within the specified bounding box.
  • drawArc(oval, startAngle, sweepAngle, useCenter, paint): Draws a segment of an oval.
  • drawText(text, x, y, paint): Draws text.
  • drawBitmap(bitmap, left, top, paint): Draws a bitmap.

1.4. The Path Object

For complex, non-standard shapes, you use the Path object. A Path is a series of drawing commands (lines, curves) that can then be drawn onto the Canvas with a single call.

  • moveTo(x, y): Moves the "cursor" to a new point without drawing a line.
  • lineTo(x, y): Draws a line from the current point to the specified point.
  • quadTo(controlX, controlY, endX, endY): Draws a quadratic Bezier curve.
  • cubicTo(...): Draws a cubic Bezier curve.
  • arcTo(...): Draws a segment of an arc.
  • close(): Closes the path by drawing a line from the current point to the starting point.

KOTLIN
// In a custom View class
private val trianglePath = Path()
private val pathPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.STROKE
    color = Color.RED
    strokeWidth = 10f
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    // Define a triangle path
    trianglePath.moveTo(width / 2f, height / 4f) // Top point
    trianglePath.lineTo(width / 4f, height * 3 / 4f) // Bottom-left point
    trianglePath.lineTo(width * 3 / 4f, height * 3 / 4f) // Bottom-right point
    trianglePath.close() // Connects back to the top point

    canvas.drawPath(trianglePath, pathPaint)
}

1.5. Canvas Transformations

The Canvas maintains a transformation matrix that modifies the coordinate system. This allows you to move, rotate, or scale the entire canvas before drawing, which is often easier than calculating new coordinates for every object.

  • translate(dx, dy): Moves the canvas origin by (dx, dy).
  • rotate(degrees, px, py): Rotates the canvas by degrees around a pivot point (px, py). If no pivot is supplied, it rotates around the current origin (0,0).
  • scale(sx, sy, px, py): Scales the canvas by factors sx and sy around a pivot point (px, py).
  • skew(sx, sy): Skews the canvas.

IMPORTANT: save() and restore()

Transformations are cumulative and affect all subsequent drawing operations. To isolate a transformation, you must wrap it in canvas.save() and canvas.restore() calls.

  • canvas.save(): Saves the current state of the canvas (its transformation matrix, clip region, etc.) onto a stack.
  • canvas.restore(): Pops the last saved state from the stack, effectively undoing all changes made since the last save().

KOTLIN
override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val rectWidth = 200f
    val rectHeight = 100f

    // Save the initial state of the canvas
    canvas.save()

    // Move the canvas origin to the center of the view
    canvas.translate(width / 2f, height / 2f)

    // Draw a rectangle centered at the new origin (0,0)
    canvas.drawRect(-rectWidth/2, -rectHeight/2, rectWidth/2, rectHeight/2, myPaint)

    // Now rotate the canvas by 45 degrees around its current origin
    canvas.rotate(45f)

    // Draw the same rectangle again. It will appear rotated.
    // Notice we are using the same coordinates as before.
    myPaint.color = Color.argb(150, 255, 0, 0) // semi-transparent red
    canvas.drawRect(-rectWidth/2, -rectHeight/2, rectWidth/2, rectHeight/2, myPaint)

    // Restore the canvas to its original state (before translate and rotate)
    canvas.restore()

    // Draw one more rectangle at the top-left to show restore worked
    myPaint.color = Color.GREEN
    canvas.drawRect(0f, 0f, 100f, 100f, myPaint)
}


2. Animations

Android provides powerful and flexible animation frameworks to bring your UI to life. The modern and recommended framework is Property Animation.

2.1. Property Animation Framework

Introduced in Android 3.0 (API 11), this framework can animate any property of any object (not just Views) over a specified duration. It is the most flexible and powerful system.

Core Components:

  • Animator: The base class.
  • ValueAnimator: The core timing engine. It calculates animated values (e.g., numbers from 0 to 100) over a duration. It does not directly manipulate objects or properties. You must add a listener to receive the updated values and apply them to your objects manually.
  • ObjectAnimator: A subclass of ValueAnimator. It's more convenient as it directly animates a specific property of a target object (e.g., the alpha or translationX property of a View). It uses reflection or setter methods (e.g., setAlpha()) to do this.
  • AnimatorSet: Allows you to choreograph multiple animators together, playing them sequentially, in parallel, or after a specified delay.
  • Interpolator: Defines the rate of change of an animation. It controls the acceleration and deceleration. Examples: LinearInterpolator, AccelerateDecelerateInterpolator, BounceInterpolator.
  • TypeEvaluator: Defines how to calculate the value of a property at a given point in the animation. Android provides default evaluators for int, float, and color values (ArgbEvaluator).

ValueAnimator Example

Use this when you need to animate something that isn't a direct object property, like the drawing coordinates in a custom View.

KOTLIN
// Inside a custom View
private var animatedRadius = 50f

fun startCircleAnimation() {
    val valueAnimator = ValueAnimator.ofFloat(50f, 150f)
    valueAnimator.duration = 2000 // 2 seconds
    valueAnimator.interpolator = AccelerateDecelerateInterpolator()
    valueAnimator.repeatCount = ValueAnimator.INFINITE
    valueAnimator.repeatMode = ValueAnimator.REVERSE

    valueAnimator.addUpdateListener { animation ->
        animatedRadius = animation.animatedValue as Float
        // Invalidate the view to trigger a redraw with the new radius
        invalidate()
    }
    valueAnimator.start()
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    canvas.drawCircle(width/2f, height/2f, animatedRadius, circlePaint)
}

ObjectAnimator Example

This is the most common and convenient way to animate View properties.

KOTLIN
// In an Activity or Fragment
val myButton: Button = findViewById(R.id.my_button)

myButton.setOnClickListener { view ->
    // Animate the button moving down by 300 pixels
    val animator = ObjectAnimator.ofFloat(view, "translationY", 0f, 300f)
    animator.duration = 1000
    animator.start()
}

// Common View properties you can animate as strings:
// "alpha": 0f to 1f
// "translationX", "translationY": pixel offset from original position
// "rotation", "rotationX", "rotationY": degrees
// "scaleX", "scaleY": 1.0 is original size

AnimatorSet Example

For choreographing complex animations.

KOTLIN
val fadeOut = ObjectAnimator.ofFloat(myView, "alpha", 1f, 0f)
fadeOut.duration = 500

val moveUp = ObjectAnimator.ofFloat(myView, "translationY", 200f, 0f)
moveUp.duration = 1000

val animatorSet = AnimatorSet()
// Play fadeOut first, then play moveUp
animatorSet.playSequentially(fadeOut, moveUp)
// OR: animatorSet.play(moveUp).after(fadeOut)

// To play them at the same time:
// animatorSet.playTogether(fadeOut, moveUp)

animatorSet.start()

2.2. View Animation (Tween Animation)

This is the older animation framework. It's simpler but less flexible.

  • Limitations:
    • It only works on View objects.
    • It only animates a few properties (alpha, scale, translate, rotate).
    • Crucially, it only animates the drawing of the View, not its actual properties. For example, if you move a button, its visual representation moves, but its clickable area remains in the original position. This is why Property Animation is strongly preferred.

View animations can be defined in XML files in the res/anim/ directory.

res/anim/fade_in.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="0.0"
    android:toAlpha="1.0"
    android:duration="1000" />

Loading and running the animation in code:

KOTLIN
val myImageView: ImageView = findViewById(R.id.my_image)
val fadeInAnimation = AnimationUtils.loadAnimation(this, R.anim.fade_in)
myImageView.startAnimation(fadeInAnimation)


3. The Transition Framework

The Transition Framework helps you animate UI changes between different states, typically involving changes in the view hierarchy (adding, removing, or changing views). It is primarily used for two scenarios:

  1. Animating layout changes within a single activity (TransitionManager).
  2. Animating the transition between two activities (Activity Transitions).

3.1. Concepts

  • Scene: A "snapshot" of a view hierarchy at a specific point in time. A transition animates the change from a starting scene to an ending scene. A scene is usually defined by a layout file and a "scene root" ViewGroup where the scene will be applied.
  • Transition: An object that defines the animation logic. It knows how to capture start and end states of views and create an Animator to run between those states.
    • Common Transitions:
      • Fade: Fades views in or out.
      • Slide: Slides views in or out from an edge.
      • Explode: Moves views in or out from the center of the scene.
      • ChangeBounds: Animates changes to a view's layout bounds (position and size).
      • ChangeTransform: Animates changes to a view's scale and rotation.
      • AutoTransition: A convenient default that combines Fade, ChangeBounds, and ChangeTransform.

3.2. Animating Layout Changes with TransitionManager

This is used to animate UI changes within the current layout. You don't need to define explicit scenes. You simply tell the TransitionManager to begin a delayed transition, and then you make your UI changes. The framework will automatically capture the "before" and "after" states and animate between them.

KOTLIN
val sceneRoot: ViewGroup = findViewById(R.id.scene_root)
val myTextView: TextView = findViewById(R.id.my_text_view)

myButton.setOnClickListener {
    // Tell the TransitionManager to watch for changes in the sceneRoot
    // and animate them using an AutoTransition over 1 second.
    TransitionManager.beginDelayedTransition(sceneRoot, AutoTransition().apply { duration = 1000 })

    // Now, make the actual UI changes.
    // The framework will animate these changes automatically.
    val params = myTextView.layoutParams
    params.width = 500
    myTextView.layoutParams = params
    myTextView.visibility = if (myTextView.visibility == View.VISIBLE) View.GONE else View.VISIBLE
}

3.3. Activity Transitions

This allows for seamless visual transitions when starting a new activity.

Step 1: Enable Window Content Transitions
This needs to be done in your theme (res/values/styles.xml or themes.xml).

XML
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    <!-- ... other attributes ... -->
    <item name="android:windowContentTransitions">true</item>
</style>

Step 2: Define Transitions in the Calling and Called Activity
In the onCreate method of your activities (before setContentView), specify the enter, exit, reenter, and return transitions.

KOTLIN
// In both ActivityA and ActivityB
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // Must be called before setContentView
    with(window) {
        requestFeature(Window.FEATURE_CONTENT_TRANSITIONS)
        // Set an exit transition for the current activity
        exitTransition = Explode()
        // Set an enter transition for the new activity
        enterTransition = Slide(Gravity.END)
    }
    setContentView(R.layout.activity_main)
    // ...
}

Step 3: Start the Activity with Options
When starting the new activity, you must provide an ActivityOptions bundle.

KOTLIN
// In ActivityA
val intent = Intent(this, ActivityB::class.java)
val options = ActivityOptions.makeSceneTransitionAnimation(this)
startActivity(intent, options.toBundle())

3.4. Shared Element Transitions

This is the most powerful type of activity transition, creating a visual link for a specific View as it "moves" from one activity to the next.

Step 1: Assign a Common Transition Name
In the layouts for both the starting and destination activities, assign the same android:transitionName to the views you want to share.

layout_activity_a.xml

XML
<ImageView
    android:id="@+id/small_avatar"
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:transitionName="avatar_transition" />

layout_activity_b.xml

XML
<ImageView
    android:id="@+id/large_avatar"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:transitionName="avatar_transition" />

Step 2: Start the Activity with Shared Element Options
When creating the ActivityOptions, specify the shared view and its transition name.

KOTLIN
// In ActivityA, starting ActivityB
val intent = Intent(this, ActivityB::class.java)
val sharedView: View = findViewById(R.id.small_avatar)
val transitionName = ViewCompat.getTransitionName(sharedView)!!

val options = ActivityOptions.makeSceneTransitionAnimation(
    this,
    sharedView,
    transitionName
)

// For multiple shared elements, use Pair.create()
// val options = ActivityOptions.makeSceneTransitionAnimation(this,
//     Pair.create(view1, "name1"),
//     Pair.create(view2, "name2")
// )

startActivity(intent, options.toBundle())

Step 3: (Optional) Customize the Transition
In the theme or in code, you can specify which transition to use for the shared elements. The default is a combination of ChangeBounds, ChangeTransform, ChangeImageTransform, etc.

XML
<!-- In styles.xml/themes.xml -->
<item name="android:windowSharedElementEnterTransition">
    @transition/change_image_transform
</item>
<item name="android:windowSharedElementExitTransition">
    @transition/change_image_transform
</item>

<!-- res/transition/change_image_transform.xml -->
<transitionSet>
    <changeImageTransform/>
    <changeBounds/>
</transitionSet>