Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active September 23, 2025 12:54
Show Gist options
  • Save PlugFox/ccc3d742a3c551bcb2ecb9b57a2bd34e to your computer and use it in GitHub Desktop.
Save PlugFox/ccc3d742a3c551bcb2ecb9b57a2bd34e to your computer and use it in GitHub Desktop.
Google Play and App Store logos
/*
* Google Play and App Store logos
* https://gist.github.com/PlugFox/ccc3d742a3c551bcb2ecb9b57a2bd34e
* https://dartpad.dev?id=ccc3d742a3c551bcb2ecb9b57a2bd34e
* Mike Matiunin <[email protected]>, 23 September 2025
*/
// ignore_for_file: curly_braces_in_flow_control_structures
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui hide Size;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show HapticFeedback;
import 'package:flutter/rendering.dart' show BoxHitTestResult;
void main() => runZonedGuarded<void>(
() => runApp(const App()),
(error, stackTrace) => print(error), // ignore: avoid_print
);
/// {@template app}
/// App widget.
/// {@endtemplate}
class App extends StatelessWidget {
/// {@macro app}
const App({super.key});
static void showSnackbar(BuildContext context, String message) =>
ScaffoldMessenger.maybeOf(context)
?..clearSnackBars()
..showSnackBar(
SnackBar(content: Text(message), duration: Duration(seconds: 5)),
);
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Google Play & App Store',
home: Scaffold(
appBar: AppBar(
title: const Text('Google Play & App Store'),
centerTitle: true,
),
body: SafeArea(
child: Padding(
padding: EdgeInsets.all(24.0),
child: Center(
child: SizedBox(
height: 52.0,
child: Builder(
builder: (context) => Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 16.0,
children: <Widget>[
Expanded(
child: GetAppStoreLogo(
label: 'Скачать в', // 'Download on the'
tooltip: 'Открыть в App Store', // 'Download on the App Store'
onTap: () {
HapticFeedback.mediumImpact().ignore();
showSnackbar(context, 'Открыть в App Store');
},
),
),
Expanded(
child: GetGooglePlayLogo(
label: 'ПОЛУЧИТЬ В', // 'GET IT ON'
tooltip: 'Открыть в Google Play', // 'Get it on Google Play'
onTap: () {
HapticFeedback.mediumImpact().ignore();
showSnackbar(context, 'Открыть в Google Play');
},
),
),
],
),
),
),
),
),
),
),
);
}
/// {@template get_app_store_logo}
/// GetAppStoreLogo widget.
/// {@endtemplate}
class GetAppStoreLogo extends StatelessWidget implements PreferredSizeWidget {
/// {@macro get_app_store_logo}
const GetAppStoreLogo({
this.label,
this.tooltip,
this.onTap,
super.key, // ignore: unused_element
});
/// The size of the logo.
static const Size size = Size(180, 52);
static final double targetAspectRatio = size.width / size.height;
@override
Size get preferredSize => size;
/// Called when the user taps this button.
final VoidCallback? onTap;
/// The label of the button.
/// For example: 'Download on the'
final String? label;
/// The tooltip of the button.
/// For example: 'Download on the App Store'
final String? tooltip;
static Widget _wrapWithTooltip({required Widget child, String? tooltip}) {
if (tooltip case String message when message.isNotEmpty)
return Tooltip(message: message, child: child);
return child;
}
@override
Widget build(BuildContext context) => Center(
child: SizedBox.fromSize(
size: size,
child: LayoutBuilder(
builder: (context, constraints) {
if (constraints.smallest.shortestSide < 24)
return const SizedBox.shrink();
final boxAspectRatio = constraints.maxWidth / constraints.maxHeight;
// If aspect ratio is too low, display only the logo.
// otherwise, display the full button with text.
if (boxAspectRatio < targetAspectRatio * 0.7) {
return Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Material(
color: Colors.transparent,
type: MaterialType.button,
borderRadius: BorderRadius.circular(12.0),
elevation: 4.0,
child: Ink(
decoration: ShapeDecoration(
color: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12.0),
child: _wrapWithTooltip(
tooltip: tooltip,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: AppStoreLogo()),
),
),
),
),
),
),
);
} else {
return FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: SizedBox.fromSize(
size: size,
child: Material(
color: Colors.transparent,
type: MaterialType.button,
borderRadius: BorderRadius.circular(12.0),
elevation: 4.0,
child: Ink(
decoration: ShapeDecoration(
color: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12.0),
child: _wrapWithTooltip(
tooltip: tooltip,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(
height: 32.0,
child: AppStoreLogo(),
),
const SizedBox(width: 12.0),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: <Widget>[
Text(
label ?? 'Download on the',
textAlign: TextAlign.start,
style: const TextStyle(
color: Colors.white,
fontSize: 10.0,
letterSpacing: 1.1,
fontWeight: FontWeight.w600,
height: 1.0,
overflow: TextOverflow.visible,
),
),
const SizedBox(height: 2.0),
const Text(
'App Store',
textAlign: TextAlign.start,
style: TextStyle(
color: Colors.white,
fontSize: 18.0,
fontWeight: FontWeight.w700,
height: 1.0,
overflow: TextOverflow.visible,
),
),
],
),
),
],
),
),
),
),
),
),
),
);
}
},
),
),
);
}
/// {@template app_store_logo}
/// AppStoreLogo widget.
/// {@endtemplate}
class AppStoreLogo extends LeafRenderObjectWidget
implements PreferredSizeWidget {
/// {@macro app_store_logo}
const AppStoreLogo({super.key});
/// The size of the logo.
static const Size size = Size(27.8, 33.2);
@override
Size get preferredSize => size;
@override
RenderObject createRenderObject(BuildContext context) =>
AppStoreLogoRenderObject().._targetSize = size;
@override
void updateRenderObject(
BuildContext context,
covariant AppStoreLogoRenderObject renderObject,
) {
if (renderObject case AppStoreLogoRenderObject object
when object._targetSize != size) {
renderObject
.._targetSize = size
..markNeedsLayout();
}
}
}
@internal
class AppStoreLogoRenderObject extends RenderBox {
AppStoreLogoRenderObject();
Size _targetSize = Size.zero;
double _scale = .0;
@override
bool get isRepaintBoundary => false;
@override
bool get alwaysNeedsCompositing => false;
@override
bool get sizedByParent => false;
@override
Size computeDryLayout(BoxConstraints constraints) =>
constraints.constrain(_targetSize);
@override
void performLayout() {
final size = super.size = computeDryLayout(constraints);
_scale = math.min(
size.width / AppStoreLogo.size.width,
size.height / AppStoreLogo.size.height,
);
}
@override
bool hitTestSelf(Offset position) => true;
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) =>
false;
@override
void paint(PaintingContext context, Offset offset) {
final scale = _scale;
final canvas = context.canvas..save();
if (scale < .01) {
// No need to paint if the scale is too small.
return;
} else if (scale < 1.0) {
// Move the logo to the center of the box.
canvas.translate(
offset.dx + (size.width - AppStoreLogo.size.width * scale) / 2,
offset.dy + (size.height - AppStoreLogo.size.height * scale) / 2,
);
} else if (scale == 1.0) {
// Move the logo to the center of the box.
canvas.translate(offset.dx, offset.dy);
} else {
// Move the center of the logo to the center of the box.
canvas.translate(
offset.dx + (size.width - AppStoreLogo.size.width * scale) / 2,
offset.dy + (size.height - AppStoreLogo.size.height * scale) / 2,
);
}
canvas
// ..clipRect(Offset.zero & size)
..scale(scale, scale)
..drawPicture(_$logoPicture)
..restore();
}
static final ui.Picture _$logoPicture = () {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
const size = AppStoreLogo.size;
final path_0 = Path();
path_0.moveTo(size.width * 0.8354143, size.height * 0.5211500);
path_0.cubicTo(
size.width * 0.8340286,
size.height * 0.3979176,
size.width * 0.9617821,
size.height * 0.3379647,
size.width * 0.9676214,
size.height * 0.3351735,
);
path_0.cubicTo(
size.width * 0.8952714,
size.height * 0.2509421,
size.width * 0.7831286,
size.height * 0.2394332,
size.width * 0.7437179,
size.height * 0.2385156,
);
path_0.cubicTo(
size.width * 0.6495321,
size.height * 0.2306009,
size.width * 0.5581714,
size.height * 0.2835185,
size.width * 0.5101893,
size.height * 0.2835185,
);
path_0.cubicTo(
size.width * 0.4612536,
size.height * 0.2835185,
size.width * 0.3873679,
size.height * 0.2392803,
size.width * 0.3077386,
size.height * 0.2405803,
);
path_0.cubicTo(
size.width * 0.2052679,
size.height * 0.2418421,
size.width * 0.1094050,
size.height * 0.2892156,
size.width * 0.05682857,
size.height * 0.3627794,
);
path_0.cubicTo(
size.width * -0.05167571,
size.height * 0.5127765,
size.width * 0.02924771,
size.height * 0.7332029,
size.width * 0.1332029,
size.height * 0.8544471,
);
path_0.cubicTo(
size.width * 0.1852046,
size.height * 0.9138265,
size.width * 0.2459689,
size.height * 0.9801265,
size.width * 0.3255036,
size.height * 0.9777941,
);
path_0.cubicTo(
size.width * 0.4033143,
size.height * 0.9752324,
size.width * 0.4323786,
size.height * 0.9381824,
size.width * 0.5262786,
size.height * 0.9381824,
);
path_0.cubicTo(
size.width * 0.6193179,
size.height * 0.9381824,
size.width * 0.6466107,
size.height * 0.9777941,
size.width * 0.7277250,
size.height * 0.9763029,
);
path_0.cubicTo(
size.width * 0.8112357,
size.height * 0.9752324,
size.width * 0.8638107,
size.height * 0.9166559,
size.width * 0.9139929,
size.height * 0.8567412,
);
path_0.cubicTo(
size.width * 0.9740857,
size.height * 0.7886824,
size.width * 0.9982214,
size.height * 0.7216559,
size.width * 0.9991786,
size.height * 0.7182147,
);
path_0.cubicTo(
size.width * 0.9972143,
size.height * 0.7176794,
size.width * 0.8369964,
size.height * 0.6688529,
size.width * 0.8354143,
size.height * 0.5211500,
);
path_0.close();
final paint0Fill = Paint()..style = PaintingStyle.fill;
paint0Fill.color = Colors.white;
canvas.drawPath(path_0, paint0Fill);
final path_1 = Path();
path_1.moveTo(size.width * 0.6821893, size.height * 0.1587568);
path_1.cubicTo(
size.width * 0.7240393,
size.height * 0.1169656,
size.width * 0.7526714,
size.height * 0.06010971,
size.width * 0.7447250,
size.height * 0.002412682,
);
path_1.cubicTo(
size.width * 0.6841500,
size.height * 0.004553853,
size.width * 0.6084000,
size.height * 0.03586853,
size.width * 0.5647786,
size.height * 0.07674206,
);
path_1.cubicTo(
size.width * 0.5261821,
size.height * 0.1127597,
size.width * 0.4917071,
size.height * 0.1717950,
size.width * 0.5006143,
size.height * 0.2273126,
);
path_1.cubicTo(
size.width * 0.5686571,
size.height * 0.2313656,
size.width * 0.6385179,
size.height * 0.1998979,
size.width * 0.6821893,
size.height * 0.1587568,
);
path_1.close();
final paint1Fill = Paint()..style = PaintingStyle.fill;
paint1Fill.color = Colors.white;
canvas.drawPath(path_1, paint1Fill);
return recorder.endRecording();
}();
}
/// {@template get_google_play_logo}
/// GetGooglePlayLogo widget.
/// {@endtemplate}
class GetGooglePlayLogo extends StatelessWidget implements PreferredSizeWidget {
/// {@macro get_google_play_logo}
const GetGooglePlayLogo({
this.label,
this.tooltip,
this.onTap,
super.key, // ignore: unused_element
});
/// The size of the logo.
static const Size size = Size(180, 52);
static final double targetAspectRatio = size.width / size.height;
@override
Size get preferredSize => size;
/// Called when the user taps this button.
final VoidCallback? onTap;
/// The label of the button.
/// For example: 'GET IT ON'
final String? label;
/// The tooltip of the button.
/// For example: 'Download on Google Play'
final String? tooltip;
static Widget _wrapWithTooltip({required Widget child, String? tooltip}) {
if (tooltip case String message when message.isNotEmpty)
return Tooltip(message: message, child: child);
return child;
}
@override
Widget build(BuildContext context) => Center(
child: SizedBox.fromSize(
size: size,
child: LayoutBuilder(
builder: (context, constraints) {
if (constraints.smallest.shortestSide < 24)
return const SizedBox.shrink();
final boxAspectRatio = constraints.maxWidth / constraints.maxHeight;
// If aspect ratio is too low, display only the logo.
// otherwise, display the full button with text.
if (boxAspectRatio < targetAspectRatio * 0.7) {
return Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Material(
color: Colors.transparent,
type: MaterialType.button,
borderRadius: BorderRadius.circular(12.0),
elevation: 4.0,
child: Ink(
decoration: ShapeDecoration(
color: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12.0),
child: _wrapWithTooltip(
tooltip: tooltip,
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Center(child: GooglePlayLogo()),
),
),
),
),
),
),
);
} else {
return FittedBox(
fit: BoxFit.contain,
alignment: Alignment.center,
child: SizedBox.fromSize(
size: size,
child: Material(
color: Colors.transparent,
type: MaterialType.button,
borderRadius: BorderRadius.circular(12.0),
elevation: 4.0,
child: Ink(
decoration: ShapeDecoration(
color: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12.0),
child: _wrapWithTooltip(
tooltip: tooltip,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 6.0,
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
const SizedBox(
height: 32.0,
child: GooglePlayLogo(),
),
const SizedBox(width: 12.0),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: <Widget>[
Text(
label ?? 'GET IT ON',
textAlign: TextAlign.start,
style: const TextStyle(
color: Colors.white,
fontSize: 10.0,
letterSpacing: 1.1,
fontWeight: FontWeight.w600,
height: 1.0,
overflow: TextOverflow.visible,
),
),
const SizedBox(height: 2.0),
const Text(
'Google Play',
textAlign: TextAlign.start,
style: TextStyle(
color: Colors.white,
fontSize: 18.0,
fontWeight: FontWeight.w700,
height: 1.0,
overflow: TextOverflow.visible,
),
),
],
),
),
],
),
),
),
),
),
),
),
);
}
},
),
),
);
}
/// {@template google_play_logo}
/// GooglePlayLogo widget.
/// {@endtemplate}
class GooglePlayLogo extends LeafRenderObjectWidget
implements PreferredSizeWidget {
/// {@macro google_play_logo}
const GooglePlayLogo({super.key});
/// The size of the logo.
static const Size size = Size(31.0, 33.4);
@override
Size get preferredSize => size;
@override
RenderObject createRenderObject(BuildContext context) =>
GooglePlayLogoRenderObject().._targetSize = size;
@override
void updateRenderObject(
BuildContext context,
covariant GooglePlayLogoRenderObject renderObject,
) {
if (renderObject case GooglePlayLogoRenderObject object
when object._targetSize != size) {
renderObject
.._targetSize = size
..markNeedsLayout();
}
}
}
@internal
class GooglePlayLogoRenderObject extends RenderBox {
GooglePlayLogoRenderObject();
Size _targetSize = Size.zero;
double _scale = .0;
@override
bool get isRepaintBoundary => false;
@override
bool get alwaysNeedsCompositing => false;
@override
bool get sizedByParent => false;
@override
Size computeDryLayout(BoxConstraints constraints) =>
constraints.constrain(_targetSize);
@override
void performLayout() {
final size = super.size = computeDryLayout(constraints);
_scale = math.min(
size.width / GooglePlayLogo.size.width,
size.height / GooglePlayLogo.size.height,
);
}
@override
bool hitTestSelf(Offset position) => true;
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) =>
false;
@override
void paint(PaintingContext context, Offset offset) {
final scale = _scale;
final canvas = context.canvas..save();
if (scale < .01) {
// No need to paint if the scale is too small.
return;
} else if (scale < 1.0) {
// Move the logo to the center of the box.
canvas.translate(
offset.dx + (size.width - GooglePlayLogo.size.width * scale) / 2,
offset.dy + (size.height - GooglePlayLogo.size.height * scale) / 2,
);
} else if (scale == 1.0) {
// Move the logo to the center of the box.
canvas.translate(offset.dx, offset.dy);
} else {
// Move the center of the logo to the center of the box.
canvas.translate(
offset.dx + (size.width - GooglePlayLogo.size.width * scale) / 2,
offset.dy + (size.height - GooglePlayLogo.size.height * scale) / 2,
);
}
canvas
// ..clipRect(Offset.zero & size)
..scale(scale, scale)
..drawPicture(_$logoPicture)
..restore();
}
static final ui.Picture _$logoPicture = () {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
const size = GooglePlayLogo.size;
final path_0 = Path();
path_0.moveTo(size.width * 0.03102691, size.height * 0.02351362);
path_0.cubicTo(
size.width * 0.01871931,
size.height * 0.03527088,
size.width * 0.01159669,
size.height * 0.05357618,
size.width * 0.01159669,
size.height * 0.07728206,
);
path_0.lineTo(size.width * 0.01159669, size.height * 0.9228559);
path_0.cubicTo(
size.width * 0.01159669,
size.height * 0.9465618,
size.width * 0.01871931,
size.height * 0.9648676,
size.width * 0.03102691,
size.height * 0.9766235,
);
path_0.lineTo(size.width * 0.03406469, size.height * 0.9792059);
path_0.lineTo(size.width * 0.5532875, size.height * 0.5055647);
path_0.lineTo(size.width * 0.5532875, size.height * 0.4943824);
path_0.lineTo(size.width * 0.03406469, size.height * 0.02074165);
path_0.lineTo(size.width * 0.03102691, size.height * 0.02351362);
path_0.close();
final paint0Fill = Paint()..style = PaintingStyle.fill;
paint0Fill.shader = ui.Gradient.linear(
Offset(size.width * 0.5071875, size.height * 0.9316618),
Offset(size.width * -17.42741, size.height * 0.2701871),
[
const Color(0xff00A0FF),
const Color(0xff00A1FF),
const Color(0xff00BEFF),
const Color(0xff00D2FF),
const Color(0xff00DFFF),
const Color(0xff00E3FF),
],
[0, 0.0066, 0.2601, 0.5122, 0.7604, 1],
);
canvas.drawPath(path_0, paint0Fill);
final path_1 = Path();
path_1.moveTo(size.width * 0.7261062, size.height * 0.6635441);
path_1.lineTo(size.width * 0.5532219, size.height * 0.5055853);
path_1.lineTo(size.width * 0.5532219, size.height * 0.4944029);
path_1.lineTo(size.width * 0.7263156, size.height * 0.3364412);
path_1.lineTo(size.width * 0.7301906, size.height * 0.3384971);
path_1.lineTo(size.width * 0.9351750, size.height * 0.4449353);
path_1.cubicTo(
size.width * 0.9936781,
size.height * 0.4751412,
size.width * 0.9936781,
size.height * 0.5248471,
size.width * 0.9351750,
size.height * 0.5552441,
);
path_1.lineTo(size.width * 0.7301906, size.height * 0.6614912);
path_1.lineTo(size.width * 0.7261062, size.height * 0.6635441);
path_1.close();
final paint1Fill = Paint()..style = PaintingStyle.fill;
paint1Fill.shader = ui.Gradient.linear(
Offset(size.width * 1.011353, size.height * 0.4999412),
Offset(size.width * -0.2451384, size.height * 0.4999412),
[
const Color(0xffFFE000),
const Color(0xffFFBD00),
const Color(0xffFFA500),
const Color(0xffFF9C00),
],
[0, 0.4087, 0.7754, 1],
);
canvas.drawPath(path_1, paint1Fill);
final path_2 = Path();
path_2.moveTo(size.width * 0.7302344, size.height * 0.6614765);
path_2.lineTo(size.width * 0.5532656, size.height * 0.4999824);
path_2.lineTo(size.width * 0.03100587, size.height * 0.9766324);
path_2.cubicTo(
size.width * 0.05043625,
size.height * 0.9952706,
size.width * 0.08212156,
size.height * 0.9975176,
size.width * 0.1181541,
size.height * 0.9788794,
);
path_2.lineTo(size.width * 0.7302344, size.height * 0.6614765);
path_2.close();
final paint2Fill = Paint()..style = PaintingStyle.fill;
paint2Fill.shader = ui.Gradient.linear(
Offset(size.width * 0.6340031, size.height * 0.4121882),
Offset(size.width * -29.01119, size.height * -48.48235),
[const Color(0xffFF3A44), const Color(0xffC31162)],
[0, 1],
);
canvas.drawPath(path_2, paint2Fill);
final path_3 = Path();
path_3.moveTo(size.width * 0.7302344, size.height * 0.3385029);
path_3.lineTo(size.width * 0.1181541, size.height * 0.02110312);
path_3.cubicTo(
size.width * 0.08212156,
size.height * 0.002654603,
size.width * 0.05043625,
size.height * 0.004900853,
size.width * 0.03100587,
size.height * 0.02354056,
);
path_3.lineTo(size.width * 0.5532656, size.height * 0.5000000);
path_3.lineTo(size.width * 0.7302344, size.height * 0.3385029);
path_3.close();
final paint3Fill = Paint()..style = PaintingStyle.fill;
paint3Fill.shader = ui.Gradient.linear(
Offset(size.width * -10.04578, size.height * 1.257968),
Offset(size.width * 0.3122012, size.height * 0.8574147),
[
const Color(0xff32A071),
const Color(0xff2DA771),
const Color(0xff15CF74),
const Color(0xff06E775),
const Color(0xff00F076),
],
[0, 0.0685, 0.4762, 0.8009, 1],
);
canvas.drawPath(path_3, paint3Fill);
return recorder.endRecording();
}();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment