Unit 2 - Notes
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: TheCanvasobject is your "drawing board." It provides the methods for drawing, such asdrawRect(),drawPath(),drawText(), etc. You are given aCanvasobject in theonDraw()method of a custom View.Paint: ThePaintobject 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 configurePaintobjects once during initialization, not insideonDraw(), asonDraw()is called frequently and object creation is expensive.Bitmap: ABitmapis the actual surface you are drawing onto. When you draw on aView'sCanvas, you are implicitly drawing on theBitmapthat backs thatView.
1.2. Creating a Custom View
To use the Canvas, you typically create a custom class that extends View and override its onDraw() method.
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.
// 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 bydegreesaround 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 factorssxandsyaround 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 lastsave().
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 ofValueAnimator. It's more convenient as it directly animates a specific property of a target object (e.g., thealphaortranslationXproperty of aView). 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 forint,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.
// 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.
// 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.
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
Viewobjects. - 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.
- It only works on
View animations can be defined in XML files in the res/anim/ directory.
res/anim/fade_in.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:
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:
- Animating layout changes within a single activity (
TransitionManager). - 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"
ViewGroupwhere 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
Animatorto 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 combinesFade,ChangeBounds, andChangeTransform.
- Common Transitions:
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.
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).
<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.
// 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.
// 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
<ImageView
android:id="@+id/small_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:transitionName="avatar_transition" />
layout_activity_b.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.
// 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.
<!-- 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>