InkDecoration do not paint if it's part of the tree doesn't get painted (#61216)

diff --git a/packages/flutter/lib/src/rendering/stack.dart b/packages/flutter/lib/src/rendering/stack.dart
index 32d4a25..0aaee2b 100644
--- a/packages/flutter/lib/src/rendering/stack.dart
+++ b/packages/flutter/lib/src/rendering/stack.dart
@@ -8,6 +8,7 @@
 import 'dart:ui' show lerpDouble, hashValues;
 
 import 'package:flutter/foundation.dart';
+import 'package:vector_math/vector_math_64.dart' show Matrix4;
 
 import 'box.dart';
 import 'object.dart';
@@ -708,6 +709,22 @@
   }
 
   @override
+  void applyPaintTransform(RenderObject child, Matrix4 transform) {
+    if (firstChild == null || index == null)
+      return;
+    final RenderBox childAtIndex = _childAtIndex();
+    if (child != childAtIndex)
+      // It is possible that the offstage widgets want to paint themselves.
+      // For example, the Material widget tries to paint all
+      // InkFeatures under its subtree as long as they are not disposed. In
+      // such case, we give it a zero transform to prevent them from painting.
+      // https://github.com/flutter/flutter/issues/59963
+      transform.setZero();
+    else
+      super.applyPaintTransform(child, transform);
+  }
+
+  @override
   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     super.debugFillProperties(properties);
     properties.add(IntProperty('index', index));
diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart
index 14dfbe8..0db33fd 100644
--- a/packages/flutter/test/material/dropdown_test.dart
+++ b/packages/flutter/test/material/dropdown_test.dart
@@ -341,11 +341,19 @@
 
   testWidgets('Dropdown button control test', (WidgetTester tester) async {
     String value = 'one';
+    StateSetter setState;
     void didChangeValue(String newValue) {
-      value = newValue;
+      setState(() {
+        value = newValue;
+      });
     }
 
-    Widget build() => buildFrame(value: value, onChanged: didChangeValue);
+    Widget build() {
+      return StatefulBuilder(builder: (BuildContext context, StateSetter setter) {
+        setState = setter;
+        return buildFrame(value: value, onChanged: didChangeValue);
+      },);
+    }
 
     await tester.pumpWidget(build());
 
@@ -380,27 +388,33 @@
 
   testWidgets('Dropdown button with no app', (WidgetTester tester) async {
     String value = 'one';
+    StateSetter setState;
     void didChangeValue(String newValue) {
-      value = newValue;
+      setState(() {
+        value = newValue;
+      });
     }
 
     Widget build() {
-      return Directionality(
-        textDirection: TextDirection.ltr,
-        child: Navigator(
-          initialRoute: '/',
-          onGenerateRoute: (RouteSettings settings) {
-            return MaterialPageRoute<void>(
-              settings: settings,
-              builder: (BuildContext context) {
-                return Material(
-                  child: buildFrame(value: 'one', onChanged: didChangeValue),
-                );
-              },
-            );
-          },
-        ),
-      );
+      return StatefulBuilder(builder: (BuildContext context, StateSetter setter) {
+        setState = setter;
+        return Directionality(
+          textDirection: TextDirection.ltr,
+          child: Navigator(
+            initialRoute: '/',
+            onGenerateRoute: (RouteSettings settings) {
+              return MaterialPageRoute<void>(
+                settings: settings,
+                builder: (BuildContext context) {
+                  return Material(
+                    child: buildFrame(value: value, onChanged: didChangeValue),
+                  );
+                },
+              );
+            },
+          ),
+        );
+      },);
     }
 
     await tester.pumpWidget(build());
@@ -2486,15 +2500,22 @@
   testWidgets('DropdownButton onTap callback is called when defined', (WidgetTester tester) async {
     int dropdownButtonTapCounter = 0;
     String value = 'one';
+    StateSetter setState;
 
-    void onChanged(String newValue) { value = newValue; }
+    void onChanged(String newValue) {
+      setState(() {
+        value = newValue;
+      });
+    }
     void onTap() { dropdownButtonTapCounter += 1; }
 
-    Widget build() => buildFrame(
-      value: value,
-      onChanged: onChanged,
-      onTap: onTap,
-    );
+    Widget build() {
+      return StatefulBuilder(builder: (BuildContext context, StateSetter setter) {
+        setState = setter;
+        return buildFrame(value: value, onChanged: onChanged, onTap: onTap,);
+      },);
+    }
+
     await tester.pumpWidget(build());
 
     expect(dropdownButtonTapCounter, 0);
@@ -2530,8 +2551,15 @@
 
   testWidgets('DropdownMenuItem onTap callback is called when defined', (WidgetTester tester) async {
     String value = 'one';
+    int currentIndex = -1;
+    StateSetter setState;
+    void onChanged(String newValue) {
+      setState(() {
+        currentIndex = -1;
+        value = newValue;
+      });
+    }
     final List<int> menuItemTapCounters = <int>[0, 0, 0, 0];
-    void onChanged(String newValue) { value = newValue; }
 
     final List<VoidCallback> onTapCallbacks = <VoidCallback>[
       () { menuItemTapCounters[0] += 1; },
@@ -2540,28 +2568,32 @@
       () { menuItemTapCounters[3] += 1; },
     ];
 
-    int currentIndex = -1;
-    await tester.pumpWidget(
-      TestApp(
-        textDirection: TextDirection.ltr,
-        child: Material(
-          child: RepaintBoundary(
-            child: DropdownButton<String>(
-              value: value,
-              onChanged: onChanged,
-              items: menuItems.map<DropdownMenuItem<String>>((String item) {
-                currentIndex += 1;
-                return DropdownMenuItem<String>(
-                  value: item,
-                  onTap: onTapCallbacks[currentIndex],
-                  child: Text(item),
-                );
-              }).toList(),
+    Widget build() {
+      return StatefulBuilder(builder: (BuildContext context, StateSetter setter) {
+        setState = setter;
+        return TestApp(
+          textDirection: TextDirection.ltr,
+          child: Material(
+            child: RepaintBoundary(
+              child: DropdownButton<String>(
+                value: value,
+                onChanged: onChanged,
+                items: menuItems.map<DropdownMenuItem<String>>((String item) {
+                  currentIndex += 1;
+                  return DropdownMenuItem<String>(
+                    value: item,
+                    onTap: onTapCallbacks[currentIndex],
+                    child: Text(item),
+                  );
+                }).toList(),
+              ),
             ),
           ),
-        ),
-      ),
-    );
+        );
+      });
+    }
+
+    await tester.pumpWidget(build());
 
     // Tap dropdown button.
     await tester.tap(find.text('one'));
diff --git a/packages/flutter/test/material/ink_paint_test.dart b/packages/flutter/test/material/ink_paint_test.dart
index 127be46..ca2afc0 100644
--- a/packages/flutter/test/material/ink_paint_test.dart
+++ b/packages/flutter/test/material/ink_paint_test.dart
@@ -434,4 +434,36 @@
       throw 'Expected: paint.color.alpha == 0, found: ${paint.color.alpha}';
     }));
   });
+
+  testWidgets('Does the Ink widget render anything if it have ancestor IndexedStack', (WidgetTester tester) async {
+    // Regressing test for https://github.com/flutter/flutter/issues/59963
+    int index = 0;
+    Widget build() => Directionality(
+      textDirection: TextDirection.ltr,
+      child: Material(
+        child: IndexedStack(
+          index: index,
+          children: <Widget>[
+            Ink(width: 100, height: 100, decoration: const BoxDecoration(color: Colors.black)),
+            Ink(width: 50, height: 50, decoration: const BoxDecoration(color: Colors.red)),
+          ],
+        ),
+      ),
+    );
+
+    await tester.pumpWidget(build());
+
+    final RenderBox box = Material.of(tester.element(find.byType(IndexedStack))) as RenderBox;
+
+    expect(box, paints..rect(rect: const Rect.fromLTRB(0.0, 0.0, 100.0, 100.0), color: Color(Colors.black.value)));
+
+    // update index, child do not at index should not be painted by have a zero
+    // transform.
+    index = 1;
+    await tester.pumpWidget(build());
+
+    expect(box, paints..transform(
+      matrix4: equals(<dynamic>[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
+    ));
+  });
 }