0+tNJRk#?aW=4>iC|!Q
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
literal 0
HcmV?d00001
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d
GIT binary patch
literal 1888
zcmeAS@N?(olHy`uVBq!ia0y~yVDJH94mJh`hU3!%wHX-L)jeGtLn>~)ol~77;wp3e
zd*9(B^+)V|Sb7bYEevxsNN95Ly>R5pG><4!#}uyGs~mG-mhw@#=)jY|P(&$f
z(L{!c<|0R&xLsVAeA3`bV(L)P-SU0k-1Wx0t*`Gaz6}C*cPCHEH}hRrY(4+)xqLb6
z?~|7>axw5eng)r>M#OO6het&Tdk2
zjXO>(+e6iPSaKw;noN1MNY&D7-9
z9Z#(y_{7gW;WTWkedtzxQflox4oCOxJ(tZ>=XklBr<7cJy-_c6<>4pH4?fl?x10LB
zUanWU;8yb0cTa*E9!y=F=<`RhcVRJK?2;*CnExqm)o;lS
z(_0muYo!}TY}Q!gDH0YN&0*T!lU-HKk;?A$3ME+W%-nlh&G(
zYqi#7w)d=_SNG47E1R)ZRCQX3>^`9zM;c0Z98ElV&WL;Ul!LD}@Jx$1`kp6Zy7Z&z
zq35)=UW+`bs<1OLr6kk*if+)p16o@dr#(JUbTYD!d9&7m+nlEj6L!APIo|Me)n*yS
zw9JP08|0_0J)rZpjql(l-Wg8~z02~J>E3O6s+2Iz-%GY_YG}gGXKU6lo&LJAdF_Eu
zlGC0ZND6zGzEfb@(gQJjem=KL*tyO1v4+9!Ij%-iv>R?dT(jrpq_Uig0uOhuYvcIG
zyL@?eNUUJf=`V8Ci}{!y^X{DEn*NdfQ9tARe^P<>OwDBY65Vze?sc$_FEwb{dHfKU
zgZ8fFDQsewcg8i%bU$=TI&Jk_)k9U!Dt5c6&wE?JMFt*Z-FYo0As$ecG9_
zqm#{3o~{hfG@mXTA-MG7*Qmd4_bNWWPjKg!s^@yVi_Ke2@5Q9_>W-pf(Y41GEPbuR
zrXBnJTB=}}o8$cRuWr7SDC<)cH8I}s%J&F!S$;|s5X-lTtY`+C7ri2H`K`R|V*
z-U6n%?^;q{KC-@>0}Nr`<`
zdy7|@ZO+jI+fB+1?c3Nt7=&(Q%qX+gel6W0{>Me~V~<7m;YVJqXEh@tZ!db6|Iuc`
z=1HRM+qU!H3TaPF{i@3GlKqauxmV(|o^841>^Mc9!-`FN^?@%BHZHz@M$s)n)meJ~
z@%~lcG^FbLG`KU>6K=Bg{1-~Tc2VHnttZD9mze0>%;TBClpC$zRes!{?(_W@T&sSY
z7~Y&Y*Qi=%8KZTL@39%Nj3T+b2}>N?r{=sp+94)+c>f>ndtI&-nJ44kDhe#Uw}(Zo
zY<~Q%l%9K;JW(gF$#Hcr$T&RzL#TdJ`7JLG;pq>LF8zJgD)slmn^ku-XR&Rui+KKU
zbp*%CEY3Y&eI;xba|ZnkI=d!JZ8gy^*`Pg~05HAUF3_{)t-pO$Th`yYuVR1~wb^{#6$
zS;cm56aRyiaZ=?|=e=or_JdzKWVxWs`)MB&K5JIYpTnE~vPLwh*7)kJy2QepH*F;i
zj@KU*yZTHfSN+^$J~OUsYn%I>K3rMn$N9S6&s)OQGpyn6l}L4K_P1~BB`!HE3|-%p
zUo|-~!OT+kNWfPS=GuGf-tOyv(_)X+zuy~uCi>%+*^eR~>9s#z_T1;xqn+hj
zUhA1VcV9|noLx0zs^_~oM;k4)S9EqoD9uY+#UM1fU$p4qO3gzd%9D%Ve`s+v-pcQu
z=NK%p!6@2XU+I_7y4oY`tC(8dSR%TEQ&&Fri(YZLpL=1)!;Fm+R$ZBFB&)f1-?F17
zae;Qnx6Mf45!-yvfW@2rR>rEmR#Q(~^7;GtYKd*rsW_9P7jF15m-VV&?zY+as%vI0
zx>fQh`l0;ZMXIL9_quOo&+2*<(N|_a?c
O7(8A5T-G@yGywok=Zg*i
literal 0
HcmV?d00001
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..6a84f41e14e27f4b11f16f9ee39279ac98f8d5ac
GIT binary patch
literal 3294
zcmeAS@N?(olHy`uVBq!ia0y~yV3+~I9Bd2>3=)SF6d4$JmU_B4hE&{oJGXK|$W_r}
z_p`S>na?1)c8S47Eg^9k))p>T1!svy24fbswB|%Z7bU|fQmRRk%!)e#vpQaEP+q{9
z`fov!BC~O#U`lCEC#T}lm5UTQ8aUXrE$-T0K7DfM-*>t1zU+DT^6A&;ch<{aE#A$Z
z^{e#${hH_1&tsRFo_)mBz_4of$?N;uq`IqEwhC?)FO?}}D;4~zc`|6@_baPmJ0&gpI9pBY%-KcrMe0k`
zW3t)!Um8U`3AtBp7C3*wvzyP-9=#FB|Gl8jKS7gs)|-rar`p)G1v3hG9cvX>%d%Sk
zF}$^3acPl*+QHD1%?e8d{Fat=Zq0K(yGg`|bDjLs9;p?7%9Rw7E;LVbP&;TEUL};(
zSt4}c)pJ%ywO&On)$5BC{8gWCs{YjE)0C~D<#En+>amkE+@6~pdf;rjY~PCDDLYpf
zr5J_fgk9tBd04F={{5C)dUr_d6oYwtBV+eZc`(blAn>Whp$d)U)l;G#e@)0)yYP?b3=qAz^&j%293S_3S5c
ztaHL1-j*(n*tp}d-Zc#$@5%2UggSFKjMfgV#;V-~6A+KD!9
zWItr^@vVW+hDcHN!vWvIII{X%9v=RGbWV=rqs*HkkF<-}w3T;oHD}xCmD~-i4((oL
zFV=SIaN>`XyK@T^MarFbi$&(F;<>@~Vo&!(E%zO(`fq4|n`3Vy(k8DQ^StTflg_ro
zs%7T+)7(YgPi}t7AHLT6LD#OwKF$UulU()LZfp-!w31YMuI02^<`lzfUj9jGmP1lW?95W=^N>=6+1>KcvdAc+nZx2EiDX89|Nn
zjKmq2Ei2x-;$aL+gjUS*mAXO+Q+9F~tUu&BvzsCK(#~hwR=kYa$I^34iP5b7_C(&OOH>k*cTlp3yBjkHg@;#r2p|F|Qchawjq<
z?^v;iT_RPFsb^02sp>JG*$!
zoO7`aE4Q4D+|#H1w$P&c{$rb0bIw}Hcd7UNUwm!H>KGs89bBhln2y!QbUOasxb4XI
zRK1wuBX(Cm@vF}*G2l5XzDV|7{C&n{S}|{`{VY>-DyH_XdbobM%>CobJ_lakn(fXI
ztXQo2@YkJ&>{p4u$_b*ceJ*w7o}04M#wtU1+hoyXk%uBNSLIqxi%#$AVx8$#aJ522?>M8jSc+Df
zp8lbyTnE;iO9`{O{_*@uT`k5Lp?+ygi}w~=PGiWHiq0w0d(Ry9V$PW^&f1p$zZY{{
zUu)fLu)=c2o&w(jO@@elZ!d8@_c~On$;Mp9`cRDJjoBXAU(XXhEh-W{__X#wQR|tm
zGgG;)pY3(j$mBZoR>s6yAy`oFI@7dVwar&{>J{%0SYa)oS2q77--#y<_tyx-tYgy&
zR$IMkkFML~f~g(_GabWn4O3QGO;=~|exCC6IM@AWccvDnHu>Cp;a*TFvmxZfG^Lc(
z-_8d9P1<1^F}=^uP327qWtsk+(K~m#_P@J)=~22(+395kkvH{1B^PP2&ftmMbv!I}
zU6R#I@$Gkhuo*Cl7O(yD@RVM$=;4!pE{WOlM%bmYg?lm%pd|q7q
zlgVK9?WFJtyf^PwwmjT^=Bf07H%;2XYOB9(EY{?WSaTwGWA+Ir&K;}xSQ8)JR=j^`
zsY=2lp5psQGY)Nh)8yUu=Gxq~1}t;ee6wx1EB;bAe=1APzxZ1Zc9};d7Hr=5_L$xQ
zHia0~vxzVE8?z|K-{8(&IH54c?yql|SDl7Uu)yw1ccvM9xqXkT-0UX%^@dnQjhn)a
zujT4ww@mDJe!2c$!|H_{`HI0t&7G+{$|>LX99GQU#=mYKe;#Y@8KD<}`x&=*@_zWM
znt$l(27ATeZ#qG1lz+!cxVEohj;VLpA?CcGO!mbJIpg@3JTv?2WEWj3-oQ6us|cr&
zVYh?)eip`RDaPu5QVCaA3$EKL
z$@uz$p6I!heJncZCsdxlvQ3uExG%-tc!_nv^uJ7B~_J-019j&t`XBnmFv0d-I&-~ZA*g?M1o@txFm%{xlwU3OL4a&qKH=Aj1
zxRhpeRPEnBwl$|8=%=r%YI(gZ=mL9qv)K-@FAu*r+5HkMS+CB!L8x!*#BAqx6F!z%
zddVHrdXTb@eT{Gmr#kncxw|c5ekZv_|9Mt$f!&WWH$XH)xvne0Hg(IPmmF8Vgnw$%
z%H18JFk_^zUx9qxA0`f=?s^jLoDAA{oMNSCOo+a+LQN&v>=nBX8@yq8&lTH#Jy|B$;)bGEPo7Dm-6JG>7-l-Tr-D
zd7GrF&fQAVsS{6bU*!DkVA)BAY}GYB3WvGe9vOTyw=P&1@29gvHeqI}k^cFU5((3P
z)c!gXW_;q-B96%9q=mALYwkU{wY2I}wjij31(Q%>`osIuamS#C7bVh12=A3&nFJr%s6>x@82kfZxYOJ
z^orL1Q7kr^yz>XsPKy~vY!UibJc~B|Y2Kgw_R`#9qe6lC3w~v+`EYLaAN`CMv$#{<
z@@@An&)eTuG;Lemu
zbk0f5=arr0hJ7pkU$R-_aN^i{ADwMxC(}aLF|N&F<%!wBd#>)A>!TeqNj^5&0;SCj
ztF8Cno#dnt+BZS>Muo(yus5xe+TRa;5q}#jn-zArZpNQ0dKnr2rcYjLq_o|A&)c*=
zi8c~`j`ru}qd)z*^6?1E%{@2fGVHf?h}5Y+v1sFt$3+5Vg75yH^YJ!Vs=D^f4q4}h
zK+zX*rbfX=DTj`J6G#cEJoHuX@rvf^nLBPc$X#1h;^sM3IbrFtcd1M_i>wweIrZ?&
z#H(#R1t0fcsdV@)^lJVUt(CiGUHWkQ@``(VY+pq!n-r5Hpyjf5lGfc$<8*>e@^UZ)%r<0GQ60LdNM^hKz_yr-=)j4=eqR@3%4ygkjfO9ulc9hM0nvRyNFK(bKeT@*|%Wx
zQ5%KVYIo&(t&)lwN|YX~Wsvt}e7$}}(Pq=_+(wquH~uybtT%pnpJn#$g5v-F08V87Im}fnEd^osu}V>QlsLp
zUlqMlcSY&S+m+cbS7fJdlvbYb>8o@=aKQNmvkv^|-}j~Z3?*VdZx|SOAA7nuhE&{od$)Rm$W>AH
z2k#QggMYcNN{Ulkcq)XYc}2*|BTD-m91|L26B83Ja%mZ+E(%O`ICUt)QN(e|3c&z}
z3!el%MI8;fT$J*kCWd(E2rq8>#L?+5aAWS>nJ*_7FaQ1SUT)g7$`b$k@BYnQ_4rJ{
zCTrurXLmlo^HP%M@NtDjEa{cr=!^Fxz)LTPQhhArW6Z5x1BS+VBQLu
zXk!r%*(uA4e#@NuZE^qf=LMJj(&lWL(74=2?9+`1%f8!r=k6DOE0(X94OEMnx~Z>t
z>t}`34)&R^9x*C^;XQ4cr+M8_bmP3ufpYcRTNfmTX}Z|C!8{Iw|8v{hx3VR=W$()@K>#
z(pc4)3ioJf>8=0Bx`OHR@^x~|jT%eDr)s}e@o&^JSRYy^nV{4a`z%#-?^WRiTxSfu
zD&6WkHMaBrjf`}7`s4g``)59jl9X)LEO=Cu;d$F9Wk0K`S%AOV?;Oz_QN=y)9QQx4
z39Zsznzim=NZ*ITuKVXAeb))MaWz^>8VK68m6&c`@H95^T(xDb8k<7P{lCj2o}IVW
z3wUKQ@p0;xnH^ku@7G&}4D-s>{CfTl$yW
z@-n`jR;|2hS-<!c1xl}OtzO<#ym7UoYsJPm
zOZVF2;*wIIvnFMkt7QH-maw@lc#>Y+ZKuok@)jS~+`THhA$Xqno<*0p3B~`^Dy^9&
z!#&}+LAjNId#>N59oHwBm>rPWf3&Xg&j0n>`ldNgK6iYl&8f;ww~G4?OnPVU)94(-
zuXU6=nd9)D@AGrtUJcsprIJ3EQ|nyim7i18TEq?bUvU~QR`)5*j9*pfecUhP+>7ut
zH>(bZ{C)e(#__=$N7?zDPWvSvE{*xSPyX(FuXFoeMZRm0Oq~(7BBATY&I6U(U-Jt#
ztYkjt&U!BOK4Ds1t!tuy}-
zgI|b`&eH>(OJyt=g$){o4Fm&^t^DjM(;%4)(c>C^MkL{wsJa%&LYsYWc`X+66`Lg-
z6IY*B^x%s0GsiuuoO4!yj8=?4#c7b-DQv4&u;@4o$Ym}qQtDMc4_JJBo*v+w(Gl?2
zrN7f)OIxMD|18#mMM+_6lA>m*7A!jMGL_f(oW}fbYXAFg6#d$Bb&+$fm@JRQexJxM
zo|hIWYjRsW-ZdqC&VChxJKHw*9?SQ<6ncBom&t1nKD~PT&GYZa%%fjAbT54?l+Up2
zzX8AT8Bgn|OpX6%E}l!y;ZQjGKKIFe`>N@ynXwne`%{P|7AI6V<4^kZQ2s)XB&FIo#*?t@#@ZmWv25lF&)&L
zKly*5v);6w6K8+5sEGTsT2$7+GkeBNfgE|3D9fV@ek|Yn@(p9i^!Xd|#7o<5wslE9
zn-^^7e*Ud@ub*vO{kgAOr{qNTui;rM|ot=&AIJk
z*qH7iuHc48|Iewe{>#O*+8cUMOSVg=meF~r5PFq!S^bC7R1HZ##
z4&yBrcO8zVC$+8lzVNt?z_R(a8CSMOaeVf|
z#MiSkADJEK(e>MDxc~c#sek>{_E(>A`qZv3x3K%#){DCN-M^`V`HG27uep3io)PI<5|@2h)=LATgba5Gth6r0m;iA;T##(XO{k6xvb7g()rz@BRAY7o-KZB
zakjAg6T{X9VTZqrw_YDTENkF?$Maa~WevkLy_vnAC7&^|rn6tjVqiUY$6>ZD^DV|=
z`Kwp_RFfaPosv<}apoe^@*vBpuWs^{*i5T<@MUGww_lqZG6T=2e-AqQAo`?)L93AR
z^9h!(9HpPn_*q$4#(IUxk3l!R>-?b_*8_K)O_}qR9v_;m8F+kh<=-u^6n_TKR^vxtQ)Q`TXX
zS0B%`ht&QHO_^ieDAb&%Tzq0K-?^J+=hzyH=QS5QPPh2%|55TmPqxTzeyzlBHC5uF
z-*Z(SpJDTgsS%zz{a)5%nPV2OSE84_qB
zbADsQn=J@WnI%NE)y5i5UV?C1$G+c-VSSh7HFHWNbS>%v#N1>FsI{G
zpI@u{af)bq
z`8l!hOxKD%N4RCKhU@U@WLx;}DB_g-`uXKOQR`Bh8`CaF&C^R%n{zsJ(|UI8`3-#4
z7STJIeCsB56f^ZO7H3?V9KF(X>arfWc%E5he{!?SPySz7l-G0k(!0x>-^u(@eSL3f
zv{?Q!PygJd@%B4aH;8Dc7su@{5Z+tg+d`NdUAOcG6f
z7R@^~pDT#2STip;z*bOHuEpg{*`HG5JBmjq{q4K>d&RsCuPu-IHvcc0_vD9hZ;ayA
zPpPu2)E71FeHL1|I!$hYKeNUnrqe%HH6@%n>#y!Q^X}p)@hpcsCh!)JBu{`%`OXy8AKi`o>9FNe(&WT
zhB?caz7#UJRxlLrG`?J-ExD}v|K*+kT8s`ZW_K0g{%ms6NhW}gN8JJs}97Oj5h
zo+oLm7${~lb2>-Kq$2hW{9PwRU#*;e`D(ERkNu&a4~u4JYCNxd+LtiXeaH5$BX((v
z@92HvO`E5=Z}(+)fvUg-C2JSy7k|^p-o1in!QCmZ!;9as1}#weJ>|4?k>yI0fUW$0
z*`DXT`r+l;DdHS#qWsuN(@w!Me9f{v>BhFzUEdUb|DJAl_Tww3e5rR_Gf!^R>f%?_
zePE;fAuIC4h9`Ia^%-B?fAOf8Gh4*g<1@}}{?)nu+?-RDU0s`$oc$#|gx`hp-M8y6
zmU^D{d}c5j8FMoDx>qp_I{~5E@l-G4OczH80Ffe$!`njxgN@xNA2Bge|
literal 0
HcmV?d00001
diff --git a/examples/catalog/ios/Runner/Base.lproj/LaunchScreen.storyboard b/examples/catalog/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000000..ebf48f6039
--- /dev/null
+++ b/examples/catalog/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/catalog/ios/Runner/Base.lproj/Main.storyboard b/examples/catalog/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000000..f3c28516fb
--- /dev/null
+++ b/examples/catalog/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/catalog/ios/Runner/Info.plist b/examples/catalog/ios/Runner/Info.plist
new file mode 100644
index 0000000000..81e0505f06
--- /dev/null
+++ b/examples/catalog/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ animated_list
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/examples/catalog/ios/Runner/main.m b/examples/catalog/ios/Runner/main.m
new file mode 100644
index 0000000000..8607072273
--- /dev/null
+++ b/examples/catalog/ios/Runner/main.m
@@ -0,0 +1,14 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import
+#import
+#import "AppDelegate.h"
+
+int main(int argc, char * argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil,
+ NSStringFromClass([AppDelegate class]));
+ }
+}
diff --git a/examples/catalog/lib/animated_list.dart b/examples/catalog/lib/animated_list.dart
new file mode 100644
index 0000000000..4a2a29254d
--- /dev/null
+++ b/examples/catalog/lib/animated_list.dart
@@ -0,0 +1,227 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+class AnimatedListSample extends StatefulWidget {
+ @override
+ _AnimatedListSampleState createState() => _AnimatedListSampleState();
+}
+
+class _AnimatedListSampleState extends State {
+ final GlobalKey _listKey = GlobalKey();
+ ListModel _list;
+ int _selectedItem;
+ int _nextItem; // The next item inserted when the user presses the '+' button.
+
+ @override
+ void initState() {
+ super.initState();
+ _list = ListModel(
+ listKey: _listKey,
+ initialItems: [0, 1, 2],
+ removedItemBuilder: _buildRemovedItem,
+ );
+ _nextItem = 3;
+ }
+
+ // Used to build list items that haven't been removed.
+ Widget _buildItem(BuildContext context, int index, Animation animation) {
+ return CardItem(
+ animation: animation,
+ item: _list[index],
+ selected: _selectedItem == _list[index],
+ onTap: () {
+ setState(() {
+ _selectedItem = _selectedItem == _list[index] ? null : _list[index];
+ });
+ },
+ );
+ }
+
+ // Used to build an item after it has been removed from the list. This method is
+ // needed because a removed item remains visible until its animation has
+ // completed (even though it's gone as far this ListModel is concerned).
+ // The widget will be used by the [AnimatedListState.removeItem] method's
+ // [AnimatedListRemovedItemBuilder] parameter.
+ Widget _buildRemovedItem(int item, BuildContext context, Animation animation) {
+ return CardItem(
+ animation: animation,
+ item: item,
+ selected: false,
+ // No gesture detector here: we don't want removed items to be interactive.
+ );
+ }
+
+ // Insert the "next item" into the list model.
+ void _insert() {
+ final int index = _selectedItem == null ? _list.length : _list.indexOf(_selectedItem);
+ _list.insert(index, _nextItem++);
+ }
+
+ // Remove the selected item from the list model.
+ void _remove() {
+ if (_selectedItem != null) {
+ _list.removeAt(_list.indexOf(_selectedItem));
+ setState(() {
+ _selectedItem = null;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('AnimatedList'),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.add_circle),
+ onPressed: _insert,
+ tooltip: 'insert a new item',
+ ),
+ IconButton(
+ icon: const Icon(Icons.remove_circle),
+ onPressed: _remove,
+ tooltip: 'remove the selected item',
+ ),
+ ],
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: AnimatedList(
+ key: _listKey,
+ initialItemCount: _list.length,
+ itemBuilder: _buildItem,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+/// Keeps a Dart List in sync with an AnimatedList.
+///
+/// The [insert] and [removeAt] methods apply to both the internal list and the
+/// animated list that belongs to [listKey].
+///
+/// This class only exposes as much of the Dart List API as is needed by the
+/// sample app. More list methods are easily added, however methods that mutate the
+/// list must make the same changes to the animated list in terms of
+/// [AnimatedListState.insertItem] and [AnimatedList.removeItem].
+class ListModel {
+ ListModel({
+ @required this.listKey,
+ @required this.removedItemBuilder,
+ Iterable initialItems,
+ }) : assert(listKey != null),
+ assert(removedItemBuilder != null),
+ _items = initialItems?.toList() ?? [];
+
+ final GlobalKey listKey;
+ final Widget Function(E item, BuildContext context, Animation animation) removedItemBuilder;
+ final List _items;
+
+ AnimatedListState get _animatedList => listKey.currentState;
+
+ void insert(int index, E item) {
+ _items.insert(index, item);
+ _animatedList.insertItem(index);
+ }
+
+ E removeAt(int index) {
+ final E removedItem = _items.removeAt(index);
+ if (removedItem != null) {
+ _animatedList.removeItem(index, (BuildContext context, Animation animation) {
+ return removedItemBuilder(removedItem, context, animation);
+ });
+ }
+ return removedItem;
+ }
+
+ int get length => _items.length;
+ E operator [](int index) => _items[index];
+ int indexOf(E item) => _items.indexOf(item);
+}
+
+/// Displays its integer item as 'item N' on a Card whose color is based on
+/// the item's value. The text is displayed in bright green if selected is true.
+/// This widget's height is based on the animation parameter, it varies
+/// from 0 to 128 as the animation varies from 0.0 to 1.0.
+class CardItem extends StatelessWidget {
+ const CardItem({
+ Key key,
+ @required this.animation,
+ this.onTap,
+ @required this.item,
+ this.selected = false,
+ }) : assert(animation != null),
+ assert(item != null && item >= 0),
+ assert(selected != null),
+ super(key: key);
+
+ final Animation animation;
+ final VoidCallback onTap;
+ final int item;
+ final bool selected;
+
+ @override
+ Widget build(BuildContext context) {
+ TextStyle textStyle = Theme.of(context).textTheme.headline4;
+ if (selected)
+ textStyle = textStyle.copyWith(color: Colors.lightGreenAccent[400]);
+ return Padding(
+ padding: const EdgeInsets.all(2.0),
+ child: SizeTransition(
+ axis: Axis.vertical,
+ sizeFactor: animation,
+ child: GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: onTap,
+ child: SizedBox(
+ height: 128.0,
+ child: Card(
+ color: Colors.primaries[item % Colors.primaries.length],
+ child: Center(
+ child: Text('Item $item', style: textStyle),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(AnimatedListSample());
+}
+
+/*
+Sample Catalog
+
+Title: AnimatedList
+
+Summary: An AnimatedList for displaying a list of cards that stay
+in sync with an app-specific ListModel. When an item is added to or removed
+from the model, the corresponding card animates in or out of view.
+
+Description:
+Tap an item to select it, tap it again to unselect. Tap '+' to insert at the
+selected item, '-' to remove the selected item. The tap handlers add or
+remove items from a `ListModel`, a simple encapsulation of `List`
+that keeps the AnimatedList in sync. The list model has a GlobalKey for
+its animated list. It uses the key to call the insertItem and removeItem
+methods defined by AnimatedListState.
+
+Classes: AnimatedList, AnimatedListState
+
+Sample: AnimatedListSample
+
+See also:
+ - The "Components-Lists: Controls" section of the material design specification:
+
+*/
diff --git a/examples/catalog/lib/app_bar_bottom.dart b/examples/catalog/lib/app_bar_bottom.dart
new file mode 100644
index 0000000000..1435b50594
--- /dev/null
+++ b/examples/catalog/lib/app_bar_bottom.dart
@@ -0,0 +1,145 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+class AppBarBottomSample extends StatefulWidget {
+ @override
+ _AppBarBottomSampleState createState() => _AppBarBottomSampleState();
+}
+
+class _AppBarBottomSampleState extends State with SingleTickerProviderStateMixin {
+ TabController _tabController;
+
+ @override
+ void initState() {
+ super.initState();
+ _tabController = TabController(vsync: this, length: choices.length);
+ }
+
+ @override
+ void dispose() {
+ _tabController.dispose();
+ super.dispose();
+ }
+
+ void _nextPage(int delta) {
+ final int newIndex = _tabController.index + delta;
+ if (newIndex < 0 || newIndex >= _tabController.length)
+ return;
+ _tabController.animateTo(newIndex);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('AppBar Bottom Widget'),
+ leading: IconButton(
+ tooltip: 'Previous choice',
+ icon: const Icon(Icons.arrow_back),
+ onPressed: () { _nextPage(-1); },
+ ),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.arrow_forward),
+ tooltip: 'Next choice',
+ onPressed: () { _nextPage(1); },
+ ),
+ ],
+ bottom: PreferredSize(
+ preferredSize: const Size.fromHeight(48.0),
+ child: Theme(
+ data: Theme.of(context).copyWith(accentColor: Colors.white),
+ child: Container(
+ height: 48.0,
+ alignment: Alignment.center,
+ child: TabPageSelector(controller: _tabController),
+ ),
+ ),
+ ),
+ ),
+ body: TabBarView(
+ controller: _tabController,
+ children: choices.map((Choice choice) {
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: ChoiceCard(choice: choice),
+ );
+ }).toList(),
+ ),
+ ),
+ );
+ }
+}
+
+class Choice {
+ const Choice({ this.title, this.icon });
+ final String title;
+ final IconData icon;
+}
+
+const List choices = [
+ Choice(title: 'CAR', icon: Icons.directions_car),
+ Choice(title: 'BICYCLE', icon: Icons.directions_bike),
+ Choice(title: 'BOAT', icon: Icons.directions_boat),
+ Choice(title: 'BUS', icon: Icons.directions_bus),
+ Choice(title: 'TRAIN', icon: Icons.directions_railway),
+ Choice(title: 'WALK', icon: Icons.directions_walk),
+];
+
+class ChoiceCard extends StatelessWidget {
+ const ChoiceCard({ Key key, this.choice }) : super(key: key);
+
+ final Choice choice;
+
+ @override
+ Widget build(BuildContext context) {
+ final TextStyle textStyle = Theme.of(context).textTheme.headline4;
+ return Card(
+ color: Colors.white,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(choice.icon, size: 128.0, color: textStyle.color),
+ Text(choice.title, style: textStyle),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(AppBarBottomSample());
+}
+
+/*
+Sample Catalog
+
+Title: AppBar with a custom bottom widget.
+
+Summary: An AppBar that includes a bottom widget. Any widget
+with a PreferredSize can appear at the bottom of an AppBar.
+
+Summary: Any widget with a PreferredSize can appear at the bottom of an AppBar.
+
+Description:
+Typically an AppBar's bottom widget is a TabBar however any widget with a
+PreferredSize can be used. In this app, the app bar's bottom widget is a
+TabPageSelector that displays the relative position of the selected page
+in the app's TabBarView. The arrow buttons in the toolbar part of the app
+bar and they select the previous or the next page.
+
+Classes: AppBar, PreferredSize, TabBarView, TabController
+
+Sample: AppBarBottomSample
+
+See also:
+ - The "Components-Tabs" section of the material design specification:
+
+*/
diff --git a/examples/catalog/lib/basic_app_bar.dart b/examples/catalog/lib/basic_app_bar.dart
new file mode 100644
index 0000000000..9c4f8ffeea
--- /dev/null
+++ b/examples/catalog/lib/basic_app_bar.dart
@@ -0,0 +1,121 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+// This app is a stateful, it tracks the user's current choice.
+class BasicAppBarSample extends StatefulWidget {
+ @override
+ _BasicAppBarSampleState createState() => _BasicAppBarSampleState();
+}
+
+class _BasicAppBarSampleState extends State {
+ Choice _selectedChoice = choices[0]; // The app's "state".
+
+ void _select(Choice choice) {
+ setState(() { // Causes the app to rebuild with the new _selectedChoice.
+ _selectedChoice = choice;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Basic AppBar'),
+ actions: [
+ IconButton( // action button
+ icon: Icon(choices[0].icon),
+ onPressed: () { _select(choices[0]); },
+ ),
+ IconButton( // action button
+ icon: Icon(choices[1].icon),
+ onPressed: () { _select(choices[1]); },
+ ),
+ PopupMenuButton( // overflow menu
+ onSelected: _select,
+ itemBuilder: (BuildContext context) {
+ return choices.skip(2).map>((Choice choice) {
+ return PopupMenuItem(
+ value: choice,
+ child: Text(choice.title),
+ );
+ }).toList();
+ },
+ ),
+ ],
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: ChoiceCard(choice: _selectedChoice),
+ ),
+ ),
+ );
+ }
+}
+
+class Choice {
+ const Choice({ this.title, this.icon });
+ final String title;
+ final IconData icon;
+}
+
+const List choices = [
+ Choice(title: 'Car', icon: Icons.directions_car),
+ Choice(title: 'Bicycle', icon: Icons.directions_bike),
+ Choice(title: 'Boat', icon: Icons.directions_boat),
+ Choice(title: 'Bus', icon: Icons.directions_bus),
+ Choice(title: 'Train', icon: Icons.directions_railway),
+ Choice(title: 'Walk', icon: Icons.directions_walk),
+];
+
+class ChoiceCard extends StatelessWidget {
+ const ChoiceCard({ Key key, this.choice }) : super(key: key);
+
+ final Choice choice;
+
+ @override
+ Widget build(BuildContext context) {
+ final TextStyle textStyle = Theme.of(context).textTheme.headline4;
+ return Card(
+ color: Colors.white,
+ child: Center(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Icon(choice.icon, size: 128.0, color: textStyle.color),
+ Text(choice.title, style: textStyle),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(BasicAppBarSample());
+}
+
+/*
+Sample Catalog
+
+Title: AppBar Basics
+
+Summary: A basic AppBar with a title, actions, and an overflow dropdown menu.
+
+Description:
+An app that displays one of a half dozen choices with an icon and a title.
+The two most common choices are available as action buttons and the remaining
+choices are included in the overflow dropdown menu.
+
+Classes: AppBar, IconButton, PopupMenuButton, Scaffold
+
+Sample: BasicAppBarSample
+
+See also:
+ - The "Layout-Structure" section of the material design specification:
+
+*/
diff --git a/examples/catalog/lib/custom_a11y_traversal.dart b/examples/catalog/lib/custom_a11y_traversal.dart
new file mode 100644
index 0000000000..efb5f42370
--- /dev/null
+++ b/examples/catalog/lib/custom_a11y_traversal.dart
@@ -0,0 +1,320 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+import 'package:flutter/semantics.dart';
+
+/// This example shows a set of widgets for changing data fields arranged in a
+/// column of rows but, in accessibility mode, are traversed in a custom order.
+///
+/// This demonstrates how Flutter's accessibility system describes custom
+/// traversal orders using sort keys.
+///
+/// The example app here has three fields that have a title and up/down spinner
+/// buttons above and below. The traversal order should allow the user to focus
+/// on each title, the input field next, the up spinner next, and the down
+/// spinner last before moving to the next input title.
+///
+/// Users that do not use a screen reader (e.g. TalkBack on Android and
+/// VoiceOver on iOS) will just see a regular app with controls.
+///
+/// The example's [RowColumnTraversal] widget sets up two [Semantics] objects
+/// that wrap the given [Widget] child, providing the traversal order they
+/// should have in the "row" direction, and then the traversal order they should
+/// have in the "column" direction.
+///
+/// Since widgets are globally sorted by their sort key, the order does not have
+/// to conform to the widget hierarchy. Indeed, in this example, we traverse
+/// vertically first, but the widget hierarchy is a column of rows.
+///
+/// See also:
+///
+/// * [Semantics] for an object that annotates widgets with accessibility semantics
+/// (including traversal order).
+/// * [SemanticSortKey] for the base class of all semantic sort keys.
+/// * [OrdinalSortKey] for a concrete sort key that sorts based on the given ordinal.
+class RowColumnTraversal extends StatelessWidget {
+ const RowColumnTraversal({this.rowOrder, this.columnOrder, this.child});
+
+ final int rowOrder;
+ final int columnOrder;
+ final Widget child;
+
+ /// Builds a widget hierarchy that wraps [child].
+ ///
+ /// This function expresses the sort keys as a hierarchy.
+ @override
+ Widget build(BuildContext context) {
+ return Semantics(
+ sortKey: OrdinalSortKey(columnOrder.toDouble()),
+ child: Semantics(
+ sortKey: OrdinalSortKey(rowOrder.toDouble()),
+ child: child,
+ ),
+ );
+ }
+}
+
+// --------------- Component widgets ---------------------
+
+/// A Button class that wraps an [IconButton] with a [RowColumnTraversal] to
+/// set its traversal order.
+class SpinnerButton extends StatelessWidget {
+ const SpinnerButton({
+ Key key,
+ this.onPressed,
+ this.icon,
+ this.rowOrder,
+ this.columnOrder,
+ this.field,
+ this.increment,
+ }) : super(key: key);
+
+ final VoidCallback onPressed;
+ final IconData icon;
+ final int rowOrder;
+ final int columnOrder;
+ final Field field;
+ final bool increment;
+
+ @override
+ Widget build(BuildContext context) {
+ final String label = '${increment ? 'Increment' : 'Decrement'} ${_fieldToName(field)}';
+
+ return RowColumnTraversal(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ child: Center(
+ child: IconButton(
+ icon: Icon(icon),
+ onPressed: onPressed,
+ tooltip: label,
+ ),
+ ),
+ );
+ }
+}
+
+/// A text entry field that wraps a [TextField] with a [RowColumnTraversal] to
+/// set its traversal order.
+class FieldWidget extends StatelessWidget {
+ const FieldWidget({
+ Key key,
+ this.rowOrder,
+ this.columnOrder,
+ this.onIncrease,
+ this.onDecrease,
+ this.value,
+ this.field,
+ }) : super(key: key);
+
+ final int rowOrder;
+ final int columnOrder;
+ final VoidCallback onDecrease;
+ final VoidCallback onIncrease;
+ final int value;
+ final Field field;
+
+ @override
+ Widget build(BuildContext context) {
+ final String stringValue = '${_fieldToName(field)} $value';
+ final String increasedValue = '${_fieldToName(field)} ${value + 1}';
+ final String decreasedValue = '${_fieldToName(field)} ${value - 1}';
+
+ return RowColumnTraversal(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ child: Center(
+ child: Semantics(
+ onDecrease: onDecrease,
+ onIncrease: onIncrease,
+ value: stringValue,
+ increasedValue: increasedValue,
+ decreasedValue: decreasedValue,
+ child: ExcludeSemantics(child: Text(value.toString())),
+ ),
+ ),
+ );
+ }
+}
+
+// --------------- Field manipulation functions ---------------------
+
+/// An enum that describes which column we're referring to.
+enum Field { DOGS, CATS, FISH }
+
+String _fieldToName(Field field) {
+ switch (field) {
+ case Field.DOGS:
+ return 'Dogs';
+ case Field.CATS:
+ return 'Cats';
+ case Field.FISH:
+ return 'Fish';
+ }
+ return null;
+}
+
+// --------------- Main app ---------------------
+
+/// The top-level example widget that serves as the body of the app.
+class CustomTraversalExample extends StatefulWidget {
+ @override
+ CustomTraversalExampleState createState() => CustomTraversalExampleState();
+}
+
+/// The state object for the top level example widget.
+class CustomTraversalExampleState extends State {
+ /// The fields that we are manipulating. List indices correspond to
+ /// the entries in the [Field] enum.
+ List fields = [0, 0, 0];
+
+ void _addToField(Field field, int delta) {
+ setState(() {
+ fields[field.index] += delta;
+ });
+ }
+
+ Widget _makeFieldHeader(int rowOrder, int columnOrder, Field field) {
+ return RowColumnTraversal(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ child: Text(_fieldToName(field)),
+ );
+ }
+
+ Widget _makeSpinnerButton(int rowOrder, int columnOrder, Field field, {bool increment = true}) {
+ return SpinnerButton(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ icon: increment ? Icons.arrow_upward : Icons.arrow_downward,
+ onPressed: () => _addToField(field, increment ? 1 : -1),
+ field: field,
+ increment: increment,
+ );
+ }
+
+ Widget _makeEntryField(int rowOrder, int columnOrder, Field field) {
+ return FieldWidget(
+ rowOrder: rowOrder,
+ columnOrder: columnOrder,
+ onIncrease: () => _addToField(field, 1),
+ onDecrease: () => _addToField(field, -1),
+ value: fields[field.index],
+ field: field,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Pet Inventory'),
+ ),
+ body: Builder(
+ builder: (BuildContext context) {
+ return DefaultTextStyle(
+ style: DefaultTextStyle.of(context).style.copyWith(fontSize: 21.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Semantics(
+ // Since this is the only sort key that the text has, it
+ // will be compared with the 'column' OrdinalSortKeys of all the
+ // fields, because the column sort keys are first in the other fields.
+ //
+ // An ordinal of "0.0" means that it will be traversed before column 1.
+ sortKey: const OrdinalSortKey(0.0),
+ child: const Text(
+ 'How many pets do you own?',
+ ),
+ ),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 10.0)),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _makeFieldHeader(1, 0, Field.DOGS),
+ _makeFieldHeader(1, 1, Field.CATS),
+ _makeFieldHeader(1, 2, Field.FISH),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _makeSpinnerButton(3, 0, Field.DOGS, increment: true),
+ _makeSpinnerButton(3, 1, Field.CATS, increment: true),
+ _makeSpinnerButton(3, 2, Field.FISH, increment: true),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _makeEntryField(2, 0, Field.DOGS),
+ _makeEntryField(2, 1, Field.CATS),
+ _makeEntryField(2, 2, Field.FISH),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _makeSpinnerButton(4, 0, Field.DOGS, increment: false),
+ _makeSpinnerButton(4, 1, Field.CATS, increment: false),
+ _makeSpinnerButton(4, 2, Field.FISH, increment: false),
+ ],
+ ),
+ const Padding(padding: EdgeInsets.symmetric(vertical: 10.0)),
+ Semantics(
+ // Since this is the only sort key that the reset button has, it
+ // will be compared with the 'column' OrdinalSortKeys of all the
+ // fields, because the column sort keys are first in the other fields.
+ //
+ // an ordinal of "5.0" means that it will be traversed after column 4.
+ sortKey: const OrdinalSortKey(5.0),
+ child: MaterialButton(
+ child: const Text('RESET'),
+ textTheme: ButtonTextTheme.normal,
+ textColor: Colors.blue,
+ onPressed: () {
+ setState(() {
+ fields = [0, 0, 0];
+ });
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(CustomTraversalExample());
+}
+
+/*
+Sample Catalog
+
+Title: CustomTraversalExample
+
+Summary: An app that demonstrates a custom semantics traversal order.
+
+Description:
+This app presents a value selection interface where the fields can be
+incremented or decremented using spinner arrows. In accessibility mode, the
+widgets are traversed in a custom order from one column to the next, starting
+with the column title, moving to the input field, then to the "up" increment
+button, and lastly to the "down" decrement button.
+
+When not in accessibility mode, the app works as one would expect.
+
+Classes: Semantics
+
+Sample: CustomTraversalExample
+*/
diff --git a/examples/catalog/lib/custom_semantics.dart b/examples/catalog/lib/custom_semantics.dart
new file mode 100644
index 0000000000..efe9e8afb9
--- /dev/null
+++ b/examples/catalog/lib/custom_semantics.dart
@@ -0,0 +1,152 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+/// A [ListTile] containing a dropdown menu that exposes itself as an
+/// "Adjustable" to screen readers (e.g. TalkBack on Android and VoiceOver on
+/// iOS).
+///
+/// This allows screen reader users to swipe up/down (on iOS) or use the volume
+/// keys (on Android) to switch between the values in the dropdown menu.
+/// Depending on what the values in the dropdown menu are this can be a more
+/// intuitive way of switching values compared to exposing the content of the
+/// drop down menu as a screen overlay from which the user can select.
+///
+/// Users that do not use a screen reader will just see a regular dropdown menu.
+class AdjustableDropdownListTile extends StatelessWidget {
+ const AdjustableDropdownListTile({
+ this.label,
+ this.value,
+ this.items,
+ this.onChanged,
+ });
+
+ final String label;
+ final String value;
+ final List items;
+ final ValueChanged onChanged;
+
+ @override
+ Widget build(BuildContext context) {
+ final int indexOfValue = items.indexOf(value);
+ assert(indexOfValue != -1);
+
+ final bool canIncrease = indexOfValue < items.length - 1;
+ final bool canDecrease = indexOfValue > 0;
+
+ return Semantics(
+ container: true,
+ label: label,
+ value: value,
+ increasedValue: canIncrease ? _increasedValue : null,
+ decreasedValue: canDecrease ? _decreasedValue : null,
+ onIncrease: canIncrease ? _performIncrease : null,
+ onDecrease: canDecrease ? _performDecrease : null,
+ child: ExcludeSemantics(
+ child: ListTile(
+ title: Text(label),
+ trailing: DropdownButton(
+ value: value,
+ onChanged: onChanged,
+ items: items.map>((String item) {
+ return DropdownMenuItem(
+ value: item,
+ child: Text(item),
+ );
+ }).toList(),
+ ),
+ ),
+ ),
+ );
+ }
+
+ String get _increasedValue {
+ final int indexOfValue = items.indexOf(value);
+ assert(indexOfValue < items.length - 1);
+ return items[indexOfValue + 1];
+ }
+
+ String get _decreasedValue {
+ final int indexOfValue = items.indexOf(value);
+ assert(indexOfValue > 0);
+ return items[indexOfValue - 1];
+ }
+
+ void _performIncrease() => onChanged(_increasedValue);
+
+ void _performDecrease() => onChanged(_decreasedValue);
+}
+
+class AdjustableDropdownExample extends StatefulWidget {
+ @override
+ AdjustableDropdownExampleState createState() => AdjustableDropdownExampleState();
+}
+
+class AdjustableDropdownExampleState extends State {
+
+ final List items = [
+ '1 second',
+ '5 seconds',
+ '15 seconds',
+ '30 seconds',
+ '1 minute',
+ ];
+ String timeout;
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Adjustable DropDown'),
+ ),
+ body: ListView(
+ children: [
+ AdjustableDropdownListTile(
+ label: 'Timeout',
+ value: timeout ?? items[2],
+ items: items,
+ onChanged: (String value) {
+ setState(() {
+ timeout = value;
+ });
+ },
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+void main() {
+ runApp(AdjustableDropdownExample());
+}
+
+/*
+Sample Catalog
+
+Title: AdjustableDropdownListTile
+
+Summary: A dropdown menu that exposes itself as an "Adjustable" to screen
+readers.
+
+Description:
+This app presents a dropdown menu to the user that exposes itself as an
+"Adjustable" to screen readers (e.g. TalkBack on Android and VoiceOver on iOS).
+This allows users of screen readers to cycle through the values of the dropdown
+menu by swiping up or down on the screen with one finger (on iOS) or by using
+the volume keys (on Android). Depending on the values in the dropdown this
+behavior may be more intuitive to screen reader users compared to showing the
+classical dropdown overlay on screen to choose a value.
+
+When the screen reader is turned off, the dropdown menu behaves like any
+dropdown menu would.
+
+Classes: Semantics
+
+Sample: AdjustableDropdownListTile
+
+*/
diff --git a/examples/catalog/lib/expansion_tile_sample.dart b/examples/catalog/lib/expansion_tile_sample.dart
new file mode 100644
index 0000000000..96c024be59
--- /dev/null
+++ b/examples/catalog/lib/expansion_tile_sample.dart
@@ -0,0 +1,121 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+class ExpansionTileSample extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('ExpansionTile'),
+ ),
+ body: ListView.builder(
+ itemBuilder: (BuildContext context, int index) => EntryItem(data[index]),
+ itemCount: data.length,
+ ),
+ ),
+ );
+ }
+}
+
+// One entry in the multilevel list displayed by this app.
+class Entry {
+ Entry(this.title, [this.children = const []]);
+ final String title;
+ final List children;
+}
+
+// The entire multilevel list displayed by this app.
+final List data = [
+ Entry('Chapter A',
+ [
+ Entry('Section A0',
+