Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions flutter_local_notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ A cross platform plugin for displaying local notifications.
- [Handling notifications whilst the app is in the foreground](#handling-notifications-whilst-the-app-is-in-the-foreground)
- **[❓ Usage](#-usage)**
- [Notification Actions](#notification-actions)
- [[Android] Title text styling (API 24+)](#android-title-text-styling-api-24)
- [Example app](#example-app)
- [API reference](#api-reference)
- **[Initialisation](#initialisation)**
Expand Down Expand Up @@ -630,6 +631,36 @@ Future<void> _showNotificationWithActions() async {

Each notification will have a internal ID & an public action title.

### [Android] Title text styling (API 24+)

Style only the notification title on Android 7.0+ using a decorated custom
layout. Older Android versions will silently fall back to the default system
template.

```dart
final androidDetails = AndroidNotificationDetails(
'reminders',
'Reminders',
titleStyle: const AndroidNotificationTitleStyle(
color: 0xFF58CC02,
sizeSp: 16,
bold: true,
italic: false,
iconSpacing: 2,
),
descriptionStyle: const AndroidNotificationDescriptionStyle(
color: 0xFFFF0000,
sizeSp: 14,
bold: false,
italic: true,
),
);
```

> [!NOTE]
> These options affect the title and body text respectively. Ensure sufficient contrast for
> accessibility.

### Example app

The [`example`](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications/example) directory has a sample application that demonstrates the features of this plugin.
Expand Down
22 changes: 15 additions & 7 deletions flutter_local_notifications/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ buildscript {

dependencies {
classpath 'com.android.tools.build:gradle:8.6.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0"
}
}

Expand All @@ -20,20 +21,26 @@ rootProject.allprojects {
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
namespace 'com.dexterous.flutterlocalnotifications'
compileSdk 35
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
compileSdk 36

defaultConfig {
multiDexEnabled true
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}

compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = "17"
}

lintOptions {
Expand All @@ -46,6 +53,7 @@ dependencies {
implementation "androidx.core:core:1.3.0"
implementation "androidx.media:media:1.1.0"
implementation "com.google.code.gson:gson:2.12.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.0"

testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:3.10.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.dexterous.flutterlocalnotifications.models;

import androidx.annotation.Keep;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;

@Keep
public class DescriptionStyle implements Serializable {
@SerializedName("color")
public Integer color;

@SerializedName("sizeSp")
public Double sizeSp;

@SerializedName("bold")
public Boolean bold;

@SerializedName("italic")
public Boolean italic;
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dexterous.flutterlocalnotifications.models;

import androidx.annotation.Keep;
import com.google.gson.annotations.SerializedName;
import java.io.Serializable;

@Keep
public class TitleStyle implements Serializable {
@SerializedName("color")
public Integer color;

@SerializedName("sizeSp")
public Double sizeSp;

@SerializedName("bold")
public Boolean bold;

@SerializedName("italic")
public Boolean italic;

// Distance in DP between the notification's icon and the title/body.
@SerializedName("iconSpacingDp")
public Double iconSpacingDp;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.dexterous.flutterlocalnotifications

import android.content.Context
import android.graphics.Typeface
import android.os.Build
import android.text.SpannableString
import android.text.Spanned
import android.text.style.StyleSpan
import android.util.Log
import android.util.TypedValue
import android.view.View
import android.widget.RemoteViews
import com.dexterous.flutterlocalnotifications.models.TitleStyle
import com.dexterous.flutterlocalnotifications.models.DescriptionStyle

internal object TitleStyler {
private const val TAG = "TitleStyler"
private const val MAX_SIZE_SP = 26f
private const val MIN_SIZE_SP = 8f


// Builds a RemoteViews that renders a styled title (and optional body).
// API 24+ only. Returns null if title is empty or style is null.

fun build(
context: Context,
title: CharSequence?,
body: CharSequence?,
titleStyle: TitleStyle?,
descriptionStyle: DescriptionStyle?
): RemoteViews? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return null
if (title.isNullOrEmpty()) return null
if (titleStyle == null && descriptionStyle == null) return null

val rv = RemoteViews(context.packageName, R.layout.fln_notif_title_only)
val titleId = R.id.fln_title
val bodyId = R.id.fln_body
val rootId = R.id.fln_root

// Build styled title (bold/italic via spans)
val styledTitle: CharSequence = if ((titleStyle?.bold == true) || (titleStyle?.italic == true)) {
val s = SpannableString(title)
when {
titleStyle.bold == true && titleStyle.italic == true ->
s.setSpan(StyleSpan(Typeface.BOLD_ITALIC), 0, s.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
titleStyle.bold == true ->
s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
titleStyle.italic == true ->
s.setSpan(StyleSpan(Typeface.ITALIC), 0, s.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
s
} else {
title
}

// Apply title color/size
titleStyle?.color?.let { rv.setTextColor(titleId, it) }
titleStyle?.sizeSp?.let {
if (it > 0.0) {
val sp = it.toFloat().coerceIn(MIN_SIZE_SP, MAX_SIZE_SP)
rv.setTextViewTextSize(titleId, TypedValue.COMPLEX_UNIT_SP, sp)
} else {
Log.d(TAG, "Ignoring non-positive sizeSp: $it")
}
}

// Set title last (ensures spans + color/size stick)
rv.setTextViewText(titleId, styledTitle)

// Adjust spacing between icon and text if requested (RTL-safe)
titleStyle?.iconSpacingDp?.let { spacing ->
if (spacing >= 0) {
val metrics = context.resources.displayMetrics
val startPx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
spacing.toFloat(),
metrics
).toInt()

// Defaults from resources (match XML)
val defaultStart = context.resources.getDimensionPixelSize(R.dimen.fln_padding_start)
val defaultTop = context.resources.getDimensionPixelSize(R.dimen.fln_padding_top)
val defaultEnd = context.resources.getDimensionPixelSize(R.dimen.fln_padding_end)
val defaultBottom = context.resources.getDimensionPixelSize(R.dimen.fln_padding_bottom)

val isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL

val left = if (isRtl) defaultStart else startPx
val right = if (isRtl) startPx else defaultEnd

rv.setViewPadding(rootId, left, defaultTop, right, defaultBottom)
} else {
Log.d(TAG, "Ignoring negative iconSpacingDp: $spacing")
}
}

// Body/description styled if provided
if (!body.isNullOrEmpty()) {
val styledBody: CharSequence = if ((descriptionStyle?.bold == true) || (descriptionStyle?.italic == true)) {
val s = SpannableString(body)
when {
descriptionStyle.bold == true && descriptionStyle.italic == true ->
s.setSpan(StyleSpan(Typeface.BOLD_ITALIC), 0, s.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
descriptionStyle.bold == true ->
s.setSpan(StyleSpan(Typeface.BOLD), 0, s.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
descriptionStyle.italic == true ->
s.setSpan(StyleSpan(Typeface.ITALIC), 0, s.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
s
} else {
body
}

descriptionStyle?.color?.let { rv.setTextColor(bodyId, it) }
descriptionStyle?.sizeSp?.let {
if (it > 0.0) {
val sp = it.toFloat().coerceIn(MIN_SIZE_SP, MAX_SIZE_SP)
rv.setTextViewTextSize(bodyId, TypedValue.COMPLEX_UNIT_SP, sp)
} else {
Log.d(TAG, "Ignoring non-positive sizeSp: $it")
}
}

rv.setViewVisibility(bodyId, View.VISIBLE)
rv.setTextViewText(bodyId, styledBody)
} else {
rv.setViewVisibility(bodyId, View.GONE)
}
return rv
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Compact custom content for collapsed / expanded / heads-up -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fln_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/fln_padding_start"
android:paddingEnd="@dimen/fln_padding_end"
android:paddingTop="@dimen/fln_padding_top"
android:paddingBottom="@dimen/fln_padding_bottom">

<!-- Title (styled via RemoteViews) -->
<TextView
android:id="@+id/fln_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.Material.Notification.Title"
android:maxLines="2"
android:ellipsize="end" />

<!-- Body / description (default style) -->
<TextView
android:id="@+id/fln_body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="@android:style/TextAppearance.Material.Notification"
android:maxLines="4"
android:ellipsize="end"
android:visibility="gone" />

</LinearLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="fln_padding_start">16dp</dimen>
<dimen name="fln_padding_end">16dp</dimen>
<dimen name="fln_padding_top">8dp</dimen>
<dimen name="fln_padding_bottom">8dp</dimen>
</resources>
Loading