Skip to content

HoltChas/focus-trail

Repository files navigation

Focus Trail

English | 简体中文

A lightweight Android TV focus effect library for View and Jetpack Compose. Add delayed focus borders, moving highlight trails, glow, and scale animations to TV cards with a drop-in View or pure Compose wrapper.

Focus Trail demo

The reusable code lives in focus-trail and focus-trail-compose. The app module is only a TV-style sample page.

  • Plain Android View drawing.
  • Overlay helper for existing Views that cannot be wrapped.
  • Optional pure Compose Canvas implementation.
  • No AppCompat or Material dependency.
  • Works with XML or programmatic View layouts.

Modules

  • focus-trail: reusable Android Library module.
  • focus-trail-compose: pure Compose Canvas implementation for Compose callers.
  • app: sample app that shows View and Compose demos.

Build Stack

  • Android Gradle Plugin: 9.2.1
  • Gradle: 9.5.1
  • Kotlin / Compose compiler plugin: 2.4.0
  • Jetpack Compose BOM: 2026.06.01
  • Activity Compose: 1.13.0
  • Java toolchain for local builds: JDK 21
  • compileSdk: 37
  • targetSdk: 37

Install

Use JitPack after pushing a GitHub release tag:

repositories {
    maven { url = uri("https://jitpack.io") }
}

dependencies {
    implementation "com.github.holtchas.focus-trail:focus-trail:0.2.0"

    // Optional Compose implementation.
    implementation "com.github.holtchas.focus-trail:focus-trail-compose:0.2.0"
}

If the library is later released to Maven Central:

dependencies {
    implementation "dev.holtchas:focus-trail:0.2.0"
    implementation "dev.holtchas:focus-trail-compose:0.2.0"
}

For local development, publish the libraries to Maven Local:

./gradlew :focus-trail:publishToMavenLocal :focus-trail-compose:publishToMavenLocal

Then consume the View library:

repositories {
    mavenLocal()
}

dependencies {
    implementation "dev.holtchas:focus-trail:0.2.0"
}

Or consume the Compose implementation:

repositories {
    mavenLocal()
}

dependencies {
    implementation "dev.holtchas:focus-trail-compose:0.2.0"
}

Inside this repo, the sample app uses project dependencies:

dependencies {
    implementation project(":focus-trail")
    implementation project(":focus-trail-compose")
}

Quick Start

If you control the card layout, wrap your content with FocusTrailLayout. This is the simplest drop-in View entry point and uses the same delayed focus-trail timing as the sample app:

<dev.holtchas.focustrail.FocusTrailLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="240dp"
    android:layout_height="160dp"
    android:focusable="true"
    android:padding="16dp"
    app:focusTrailStartDelay="1000"
    app:focusTrailCornerRadius="20dp"
    app:focusTrailPadding="12dp"
    app:focusTrailBorderWidth="2.8dp"
    app:focusTrailStaticBorderWidth="2.8dp"
    app:focusTrailGlowWidth="12dp"
    app:focusTrailStaticColor="#7EFFFFFF"
    app:focusTrailColor="#FFFFFFFF"
    app:focusTrailBaseAlpha="126"
    app:focusTrailGlowAlpha="50"
    app:focusTrailHighlightAlpha="255"
    app:focusTrailLengthRatio="0.16"
    app:focusTrailMinLength="42dp"
    app:focusTrailDrawOppositeTrail="true"
    app:focusTrailScale="1.08">

    <!-- Your poster/card content. -->

</dev.holtchas.focustrail.FocusTrailLayout>

If the page already has production Views and you do not want to wrap them, bind the effect directly:

FocusTrailHelper.bindTrailScaleFocus(
        focusedView -> updatePreview(focusedView),
        FocusTrailHelper.DEFAULT_FOCUSED_SCALE,
        posterCard,
        secondPosterCard,
        thirdPosterCard);

View Usage

Wrap Content

Use FocusTrailLayout when you control the layout:

<dev.holtchas.focustrail.FocusTrailLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="240dp"
    android:layout_height="160dp"
    android:focusable="true"
    android:padding="16dp"
    app:focusTrailShape="roundRect"
    app:focusTrailDuration="10000"
    app:focusTrailStartDelay="1000"
    app:focusTrailCornerRadius="20dp"
    app:focusTrailPadding="12dp"
    app:focusTrailBorderWidth="2.8dp"
    app:focusTrailStaticBorderWidth="2.8dp"
    app:focusTrailGlowWidth="12dp"
    app:focusTrailStaticColor="#7EFFFFFF"
    app:focusTrailColor="#FFFFFFFF"
    app:focusTrailBaseAlpha="126"
    app:focusTrailGlowAlpha="50"
    app:focusTrailHighlightAlpha="255"
    app:focusTrailLengthRatio="0.16"
    app:focusTrailMinLength="42dp"
    app:focusTrailDrawInnerTrail="true"
    app:focusTrailDrawOppositeTrail="true"
    app:focusTrailScale="1.08">

    <!-- Your ImageView/TextView/card content here. -->

</dev.holtchas.focustrail.FocusTrailLayout>

For a circle:

app:focusTrailShape="circle"
app:focusTrailDuration="6500"
app:focusTrailLengthRatio="0.28"

Programmatic configuration is also supported:

FocusTrailLayout focus = new FocusTrailLayout(context);
focus.setShape(FocusTrailLayout.SHAPE_ROUND_RECT);
focus.setDurationMs(10000L);
focus.setStartDelayMs(1000L);
focus.setCornerRadius(dp(20));
focus.setTrailPadding(dp(12));
focus.setBorderWidth(dp(2.8f));
focus.setStaticBorderWidth(dp(2.8f));
focus.setGlowWidth(dp(12));
focus.setBaseAlpha(126);
focus.setGlowAlpha(50);
focus.setHighlightAlpha(255);
focus.setTrailLengthRatio(0.16f);
focus.setMinTrailLength(dp(42));
focus.setDrawOppositeTrail(true);
focus.setFocusScale(1.08f);

Attach To Existing Views

Use FocusTrailHelper when a production page already has its own layout. This path gives existing cards the same scale animation and overlay trail without changing their parent layout:

FocusTrailHelper.bindTrailScaleFocus(
        view -> lastFocusedId = view.getId(),
        FocusTrailHelper.DEFAULT_FOCUSED_SCALE,
        cardView);

For banner or pill-style controls, use the bundled TV presets:

bannerView.setOnFocusChangeListener((view, hasFocus) ->
        FocusTrailHelper.applyBannerTrailFocus(view, hasFocus));

statusView.setOnFocusChangeListener((view, hasFocus) ->
        FocusTrailHelper.applyStatusPillTrail(
                view,
                hasFocus,
                FocusTrailHelper.WIDE_BUTTON_FOCUSED_SCALE));

You can still tune the overlay trail parameters directly. These values are in dp so they stay close to visual design specs:

FocusTrailEffect.Config config = FocusTrailHelper
        .newTrailConfigBuilder(FocusTrailHelper.DEFAULT_FOCUSED_SCALE)
        .trailPaddingDp(12f)
        .borderWidthDp(2.8f)
        .glowWidthDp(12f)
        .baseAlpha(126)
        .glowAlpha(50)
        .highlightAlpha(255)
        .highlightLengthFraction(0.16f)
        .minHighlightLengthDp(42f)
        .headRadiusDp(4.8f)
        .build();

cardView.setOnFocusChangeListener((view, hasFocus) ->
        FocusTrailHelper.applyTrailScale(
                view,
                hasFocus,
                FocusTrailHelper.DEFAULT_FOCUSED_SCALE,
                config));

For matching rounded content and focus radius, use the provided base widgets:

<dev.holtchas.focustrail.FocusTrailRoundedImageView
    android:id="@+id/poster"
    android:layout_width="180dp"
    android:layout_height="120dp"
    android:scaleType="centerCrop"
    android:focusable="true"
    app:focusTrailClipRadius="20dp" />

Compose Usage

Wrap any composable content with FocusTrailBox. For most apps, create a reusable style once and pass it to every card:

val focusStyle = FocusTrailDefaults.tvCardStyle(
    shape = FocusTrailShape.ROUND_RECT,
    durationMs = 10_000L,
    startDelayMs = 1_000L,
    cornerRadius = 20.dp,
    borderWidth = 2.8.dp,
    trailPadding = 12.dp,
    glowWidth = 12.dp,
    baseAlpha = 126,
    glowAlpha = 50,
    highlightAlpha = 255,
    trailLengthRatio = 0.16f,
    minTrailLength = 42.dp,
    drawOppositeTrail = true,
    focusScale = 1.08f
)

FocusTrailBox(
    modifier = Modifier.size(width = 240.dp, height = 160.dp),
    active = isFocused,
    style = focusStyle
) {
    // Your Compose card content here.
}

If you need lower-level control, FocusTrailBox still exposes the raw px-based parameters directly.

Configurable Options

  • focusTrailShape: roundRect or circle.
  • focusTrailDuration: one full loop duration in milliseconds.
  • focusTrailStartDelay: delay before the base border begins fading in.
  • focusTrailCornerRadius: rounded-rectangle corner radius.
  • focusTrailPadding: space reserved around content for border and glow.
  • focusTrailBorderWidth: moving inner highlight width.
  • focusTrailStaticBorderWidth: base border width.
  • focusTrailGlowWidth: outer glow stroke width.
  • focusTrailStaticColor: base border color. In overlay mode, alpha is controlled by focusTrailBaseAlpha.
  • focusTrailColor: moving highlight and glow color.
  • focusTrailBaseAlpha: base border opacity, 0-255.
  • focusTrailGlowAlpha: outer glow opacity, 0-255.
  • focusTrailHighlightAlpha: inner moving highlight opacity, 0-255.
  • focusTrailLengthRatio: highlight length as a ratio of the full path.
  • focusTrailMinLength: minimum moving highlight length.
  • focusTrailDrawInnerTrail: whether to draw the inner border/highlight.
  • focusTrailDrawOppositeTrail: whether to draw a second highlight opposite the first one.
  • focusTrailScale: scale applied while focused.
  • focusTrailUseOverlayEffect: keep true for the overlay engine; set false only for the legacy self-drawing container.
  • focusTrailAutoRaiseZ: whether the focused view is raised above siblings while focused.
  • focusTrailDisableAncestorClipping: whether ancestor ViewGroup clipping is disabled so glow can draw outside the card bounds.
  • focusTrailAutoStart: legacy self-drawing mode only.
  • focusTrailSegmentCount: legacy self-drawing mode and Compose only.
  • focusTrailClipRadius: rounded clipping radius for FocusTrailRoundedImageView and FocusTrailRoundedFrameLayout.

The Compose API mirrors the same core options through FocusTrailStyle, FocusTrailDefaults.tvCardStyle(...), and raw FocusTrailBox(...) parameters, plus active for caller-owned focus state.

Build

./gradlew :focus-trail:assembleRelease :focus-trail-compose:assembleRelease :app:assembleDebug

Publish Locally

./gradlew :focus-trail:publishToMavenLocal :focus-trail-compose:publishToMavenLocal

Install To A Device

adb install -r app/build/outputs/apk/debug/app-debug.apk
adb shell am start -n dev.holtchas.focuslab/com.elep.focuslab.FocusCompareActivity

Notes

The sample page borrows the Android TV immersive-list structure: preview background, metadata panel, and a horizontal focusable content row. Demo artwork is AI-generated fictional content art with no real film, game, logo, or brand reference.

The sample app has a small Native View / Compose switch so both implementations show the same focus-trail effect without extra visual modes.

License

MIT

About

Android TV focus effects for View and Jetpack Compose | 安卓 TV 焦点动效库

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages