Fp%q4kxL!b1#l^)8dUwJ
zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs
z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0@S>k_hS{T4PF*TDrgdXp+dzsE?
z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw
z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah-
zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw
z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D
zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a
zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD
zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~
z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX
z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR?
zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA
zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c
z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1<
zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-!
zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s
XsPq=LnkwKG00000NkvXXu0mjfhAk5^
literal 0
HcmV?d00001
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22
GIT binary patch
literal 2665
zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p
zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8
zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa
ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ
z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5
z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~
z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n
zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ
zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs
z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0@S>k_hS{T4PF*TDrgdXp+dzsE?
z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw
z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah-
zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw
z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D
zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a
zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD
zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~
z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX
z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR?
zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA
zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c
z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1<
zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-!
zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s
XsPq=LnkwKG00000NkvXXu0mjfhAk5^
literal 0
HcmV?d00001
diff --git a/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/examples/catalog/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680
GIT binary patch
literal 3831
zcmVjJBgitF5mAp-i>4+KS_oR{|13AP->1TD4=w)g|)JHOx|a2Wk1Va
z!k)vP$UcQ#mdj%wNQoaJ!w>jv_6&JPyutpQps?s5dmDQ>`%?Bvj>o<%kYG!YW6H-z
zu`g$@mp`;qDR!51QaS}|ZToSuAGcJ7$2HF0z`ln4t!#Yg46>;vGG9N9{V@9z#}6v*
zfP?}r6b{*-C*)(S>NECI_E~{QYzN5SXRmVnP<=gzP+_Sp(Aza_hKlZ{C1D&l*(7IKXxQC1Z9#6wx}YrGcn~g%;icdw>T0Rf^w0{
z$_wn1J+C0@!jCV<%Go5LA45e{5gY9PvZp8uM$=1}XDI+9m7!A95L>q>>oe0$nC->i
zeexUIvq%Uk<-$>DiDb?!In)lAmtuMWxvWlk`2>4lNuhSsjAf2*2tjT`y;@d}($o)S
zn(+W&hJ1p0xy@oxP%AM15->wPLp{H!k)BdBD$toBpJh+crWdsNV)qsHaqLg2_s|Ih
z`8E9z{E3sA!}5aKu?T!#enD(wLw?IT?k-yWVHZ8Akz4k5(TZJN^zZgm&zM28sfTD2BYJ|Fde3Xzh;;S`
z=GXTnY4Xc)8nYoz6&vF;P7{xRF-{|2Xs5>a5)@BrnQ}I(_x7Cgpx#5&Td^4Q9_FnQ
zX5so*;#8-J8#c$OlA&JyPp$LKUhC~-e~Ij!L%uSMu!-VZG7Hx-L{m2DVR2i=GR(_%
zCVD!4N`I)&Q5S`?P&fQZ=4#Dgt_v2-DzkT}K(9gF0L(owe-Id$Rc2qZVLqI_M_DyO
z9@LC#U28_LU{;wGZ&))}0R2P4MhajKCd^K#D+JJ&JIXZ_p#@+7J9A&P<0kdRujtQ_
zOy>3=C$kgi6$0pW06KaLz!21oOryKM3ZUOWqppndxfH}QpgjEJ`j7Tzn5bk6K&@RA?vl##y
z$?V~1E(!wB5rH`>3nc&@)|#<1dN2cMzzm=PGhQ|Yppne(C-Vlt450IXc`J4R0W@I7
zd1e5uW6juvO%ni(WX7BsKx3MLngO7rHO;^R5I~0^nE^9^E_eYLgiR9&KnJ)pBbfno
zSVnW$0R+&6jOOsZ82}nJ126+
c|%svPo;TeUku<2G7%?$oft
zyaO;tVo}(W)VsTUhq^XmFi#2z%-W9a{7mXn{uzivYQ_d6b7VJG{77naW(vHt-uhnY
zVN#d!JTqVh(7r-lhtXVU6o})aZbDt_;&wJVGl2FKYFBFpU-#9U)z#(A%=IVnqytR$SY-sO(
z($oNE09{D^@OuYPz&w~?9>Fl5`g9u&ecFGhqX=^#fmR=we0CJw+5xna*@oHnkahk+
z9aWeE3v|An+O5%?4fA&$Fgu~H_YmqR!yIU!bFCk4!#pAj%(lI(A5n)n@Id#M)O9Yx
zJU9oKy{sRAIV3=5>(s8n{8ryJ!;ho}%pn6hZKTKbqk=&m=f*UnK$zW3YQP*)pw$O*
zIfLA^!-bmBl6%d_n$#tP8Zd_(XdA*z*WH|E_yILwjtI~;jK#v-6jMl^?<%Y%`gvpwv&cFb$||^v4D&V=aNy?NGo620jL3VZnA%s
zH~I|qPzB~e(;p;b^gJr7Ure#7?8%F0m4vzzPy^^(q4q1OdthF}Fi*RmVZN1OwTsAP
zn9CZP`FazX3^kG(KodIZ=Kty8DLTy--UKfa1$6XugS
zk%6v$Kmxt6U!YMx0JQ)0qX*{CXwZZk$vEROidEc7=J-1;peNat!vS<3P-FT5po>iE
z!l3R+<`#x|+_hw!HjQGV=8!q|76y8L7N8gP3$%0kfush|u0uU^?dKBaeRSBUpOZ0c
z62;D&Mdn2}N}xHRFTRI?zRv=>=AjHgH}`2k4WK=#AHB)UFrR-J87GgX*x5fL^W2#d
z=(%K8-oZfMO=i{aWRDg=FX}UubM4eotRDcn;OR#{3q=*?3mE3_oJ-~prjhxh%PgQT
zyn)Qozaq0@o&|LEgS{Ind4Swsr;b`u185hZPOBLL<`d2%^Yp1?oL)=jnLi;Zo0ZDliTtQ^b5SmfIMe{T==zZkbvn$KTQGlbG8w}s@M3TZnde;1Am46P3juKb
zl9GU&3F=q`>j!`?SyH#r@O59%@aMX^rx}Nxe<>NqpUp5=lX1ojGDIR*-D^SDuvCKF
z?3$xG(gVUsBERef_YjPFl^rU9EtD{pt
z0CXwpN7BN3!8>hajGaTVk-wl=9rxmfWtIhC{mheHgStLi^+Nz12a?4r(fz)?3A%at
zMlvQmL<2-R)-@G1wJ0^zQK%mR=r4d{Y3fHp){nWXUL#|CqXl(+v+qDh>FkF9`eWrW
zfr^D%LNfOcTNvtx0JXR35J0~Jpi2#P3Q&80w+nqNfc}&G0A~*)lGHKv=^FE+b(37|)zL;KLF>oiGfb(?&1
zV3XRu!Sw>@quKiab%g6jun#oZ%!>V#A%+lNc?q>6+VvyAn=kf_6z^(TZUa4Eelh{{
zqFX-#dY(EV@7l$NE&kv9u9BR8&Ojd#ZGJ6l8_BW}^r?DIS_rU2(XaGOK
z225E@kH5Opf+CgD^{y29jD4gHbGf{1MD6ggQ&%>UG4WyPh5q_tb`{@_34B?xfSO*|
zZv8!)q;^o-bz`MuxXk*G^}(6)ACb@=Lfs`Hxoh>`Y0NE8QRQ!*p|SH@{r8=%RKd4p
z+#Ty^-0kb=-H-O`nAA3_6>2z(D=~Tbs(n8LHxD0`R0_ATFqp-SdY3(bZ3;VUM?J=O
zKCNsxsgt@|&nKMC=*+ZqmLHhX1KHbAJs{nGVMs6~TiF%Q)P@>!koa$%oS
zjXa=!5>P`vC-a}ln!uH1ooeI&v?=?v7?1n~P(wZ~0>xWxd_Aw;+}9#eULM7M8&E?Y
zC-ZLhi3RoM92SXUb-5i-Lmt5_rfjE{6y^+24`y$1lywLyHO!)Boa7438K4#iLe?rh
z2O~YGSgFUBH?og*6=r9rme=peP~ah`(8Zt7V)j5!V0KPFf_mebo3z95U8(up$-+EA^9dTRLq>Yl)YMBuch9%=e5B`Vnb>o
zt03=kq;k2TgGe4|lGne&zJa~h(UGutjP_zr?a7~#b)@15XNA>Dj(m=gg2Q5V4-$)D|Q9}R#002ovPDHLkV1o7DH3k3x
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
zcmV-m2cP(fP)x~L`~4d)Rspd&<9kFh{hn*KP1LP0~$;u(LfAu
zp%fx&qLBcRHx$G|3q(bv@+b;o0*D|jwD-Q9uQR(l*ST}s+uPgQ-MeFwZ#GS?b332?
z&Tk$&_miXn3IGq)AmQ)3sisq{raD4(k*bHvpCe-TdWq^NRTEVM)i9xbgQ&ccnUVx*
zEY%vS%gDcSg=!tuIK8$Th2_((_h^+7;R|G{n06&O2#6%LK`a}n?h_fL18btz<@lFG
za}xS}u?#DBMB>
zw^b($1Z)`9G?eP95EKi&$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD?
zmr-s{0wRmxUnbDrYfRvnZ@d
z6johZ2sMX{YkGSKWd}m|@V7`Degt-43=2M?+jR%8{(H$&MLLmS;-|JxnX2pnz;el1jsvqQz}pGSF<`mqEXRQ5sC4#BbwnB_4`
zc5bFE-Gb#JV3tox9fp-vVEN{(tOCpRse`S+@)?%pz+zVJXSooTrNCUg`R6`hxwb{)
zC@{O6MKY8tfZ5@!yy=p5Y|#+myRL=^{tc(6YgAnkg3I(Cd!r5l;|;l-MQ8B`;*SCE
z{u)uP^C$lOPM
z5d~UhKhRRmvv{LIa^|oavk1$QiEApSrP@~Jjbg`<*dW4TO?4qG%a%sTPUFz(QtW5(
zM)lA+5)0TvH~aBaOAs|}?u2FO;yc-CZ1gNM1dAxJ?%m?YsGR`}-xk2*dxC}r5j$d*
zE!#Vtbo69h>V4V`BL%_&$}
z+oJAo@jQ^Tk`;%xw-4G>hhb&)B?##U+(6Fi7nno`C<|#PVA%$Y{}N-?(Gc$1%tr4Pc}}hm~yY#fTOe!@v9s-ik$dX~|ygArPhByaXn8
zpI^FUjNWMsTFKTP3X7m?UK)3m
zp6rI^_zxRYrx6_QmhoWoDR`fp4R7gu6;gdO)!KexaoO2D88F9x#TM1(9Bn7g;|?|o
z)~$n&Lh#hCP6_LOPD>a)NmhW})LADx2kq=X7}7wYRj-0?dXr&bHaRWCfSqvzFa=sn
z-8^gSyn-RmH=BZ{AJZ~!8n5621GbUJV7Qvs%JNv&$%Q17s_X%s-41vAPfIR>;x0Wlqr5?09S>x#%Qkt>?(&XjFRY}*L6BeQ3
z<6XEBh^S7>AbwGm@XP{RkeEKj6@_o%oV?hDuUpUJ+r#JZO?!IUc;r0R?>mi)*ZpQ)
z#((dn=A#i_&EQn|hd)N$#A*fjBFuiHcYvo?@y1
z5|fV=a^a~d!c-%ZbMNqkMKiSzM{Yq=7_c&1H!mXk60Uv32dV;vMg&-kQ)Q{+PFtwc
zj|-uQ;b^gts??J*9VxxOro}W~Q9j4Em|zSRv)(WSO9$F$s=Ydu%Q+5DOid~lwk&we
zY%W(Z@ofdwPHncEZzZgmqS|!gTj3wQq9rxQy+^eNYKr1mj&?tm@wkO*9@UtnRMG>c
aR{jt9+;fr}hV%pg00001^@s67{VYS000c7NklQEG_j
zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{
zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp
zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+
z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR
zcQ#R)(?!~dmtD0+D2!K
zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK
z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b
z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_
z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g
zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R
zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5
zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY
zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxd?!bO>j08`Sr=C-KmT)qU1
z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr
z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at
zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g
ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt
zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly
z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT
zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj
ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e
zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C
z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG
z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$
z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K)
zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn
zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^
zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L
zCRlop0oK4DL@ISz{2_BPlezc;xj2|I
z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze
z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq
zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2
z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y
zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg
z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_
zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj
zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb
cX)(b1KSk$MOV07*qoM6N<$f&6$jw%VRuvdN2+38CZWny1cRtlsl+0_KtW)EU14Ei(F!UtWuj4IK+3{sK@>rh
zs1Z;=(DD&U6+tlyL?UnHVN^&g6QhFi2#HS+*qz;(>63G(`|jRtW|nz$Pv7qTovP!^
zP_jES{mr@O-02w%!^a?^1ZP!_KmQiz0L~jZ=W@Qt`8wzOoclQsAS<5YdH;a(4bGLE
zk8s}1If(PSIgVi!XE!5kA?~z*sobvNyohr;=Q_@h2@$6Flyej3J)D-6YfheRGl`HEcPk|~huT_2-U?PfL=4BPV)f1o!%rQ!NMt_MYw-5bUSwQ9Z&zC>u
zOrl~UJglJNa%f50Ok}?WB{on`Ci`p^Y!xBA?m@rcJXLxtrE0FhRF3d*ir>yzO|BD$
z3V}HpFcCh6bTzY}Nt_(W%QYd3NG)jJ4<`F<1Od)
zfQblTdC&h2lCz`>y?>|9o2CdvC8qZeIZt%jN;B7Hdn2l*k4M4MFEtq`q_#5?}c$b$pf_3y{Y!cRDafZBEj-*OD|gz#PBDeu3QoueOesLzB+O
zxjf2wvf6Wwz>@AiOo2mO4=TkAV+g~%_n&R;)l#!cBxjuoD$aS-`IIJv7cdX%2{WT7
zOm%5rs(wqyPE^k5SIpUZ!&Lq4<~%{*>_Hu$2|~Xa;iX*tz8~G6O3uFOS?+)tWtdi|
zV2b#;zRN!m@H&jd=!$7YY6_}|=!IU@=SjvGDFtL;aCtw06U;-v^0%k0FOyESt
z1Wv$={b_H&8FiRV?MrzoHWd>%v6KTRU;-v^Miiz+@q`(BoT!+<37CKhoKb)|8!+RG
z6BQFU^@fRW;s8!mOf2QViKQGk0TVER6EG1`#;Nm39Do^PoT!+<37AD!%oJe86(=et
zZ~|sLzU>V-qYiU6V8$0GmU7_K8|Fd0B?+9Un1BhKAz#V~Fk^`mJtlCX#{^8^M8!me
z8Yg;8-~>!e<-iG;h*0B1kBKm}hItVGY6WnjVpgnTTAC$rqQ^v)4KvOtpY|sIj@WYg
zyw##ZZ5AC2IKNC;^hwg9BPk0wLStlmBr;E|$5GoAo$&Ui_;S9WY62n3)i49|T%C#i017z3J=$RF|KyZWnci*@lW4
z=AKhNN6+m`Q!V3Ye68|8y@%=am>YD0nG99M)NWc20%)gwO!96j7muR}Fr&54SxKP2
zP30S~lt=a*qDlbu3+Av57=9v&vr<6g0&`!8E2fq>I|EJGKs}t|{h7+KT@)LfIV-3K
zK)r_fr2?}FFyn*MYoLC>oV-J~eavL2ho4a4^r{E-8m2hi>~hA?_vIG4a*KT;2eyl1
zh_hUvUJpNCFwBvRq5BI*srSle>c6%n`#VNsyC|MGa{(P&08p=C9+WUw9Hl<1o9T4M
zdD=_C0F7#o8A_bRR?sFNmU0R6tW`ElnF8p53IdHo#S9(JoZCz}fHwJ6F<&?qrpVqE
zte|m%89JQD+XwaPU#%#lVs-@-OL);|MdfINd6!XwP2h(eyafTUsoRkA%&@fe?9m@jw-v(yTTiV2(*fthQH9}SqmsRPVnwwbV$1E(_lkmo&S
zF-truCU914_$jpqjr(>Ha4HkM4YMT>m~NosUu&UZ>zirfHo%N6PPs9^_o$WqPA0#5
z%tG>qFCL+b*0s?sZ;Sht0nE7Kl>OVXy=gjWxxK;OJ3yGd7-pZf7JYNcZo2*1SF`u6
zHJyRRxGw9mDlOiXqVMsNe#WX`fC`vrtjSQ%KmLcl(lC>ZOQzG^%iql2w-f_K@r?OE
zwCICifM#L-HJyc7Gm>Ern?+Sk3&|Khmu4(~3qa$(m6Ub^U0E5RHq49za|XklN#?kP
zl;EstdW?(_4D>kwjWy2f!LM)y?F94kyU3`W!6+AyId-89v}sXJpuic^NLL7GJItl~
zsiuB98AI-(#Mnm|=A-R6&2fwJ0JVSY#Q>&3$zFh|@;#%0qeF=j5Ajq@4i0tIIW
z&}sk$&fGwoJpe&u-JeGLi^r?dO`m=y(QO{@h
zQqAC7$rvz&5+mo3IqE?h=a~6m>%r5Quapvzq;{y~p
zJpyXOBgD9VrW7@#p6l7O?o3feml(DtSL>D^R)
zZUY%T2b0-vBAFN7VB;M88!~HuOXi4KcI6aRQ&h|XQ0A?m%j2=l1f0cGP}h(oVfJ`N
zz#PpmFC*ieab)zJK<4?^k=g%OjPnkANzbAbmGZHoVRk*mTfm75s_cWVa`l*f$B@xu
z5E*?&@seIo#*Y~1rBm!7sF9~~u6Wrj5oICUOuz}CS)jdNIznfzCA(stJ(7$c^e5wN
z?lt>eYgbA!kvAR7zYSD&*r1$b|(@;9dcZ^67R0
zXAXJKa|5Sdmj!g578Nwt6d$sXuc&MWezA0Whd`94$h{{?1IwXP4)Tx4obDK%xoFZ_Z
zjjHJ_P@R_e5blG@yEjnaJb`l;s%Lb2&=8$&Ct-fV`E^4CUs)=jTk!I}2d&n!f@)bm
z@
z_4Dc86+3l2*p|~;o-Sb~oXb_RuLmoifDU^&Te$*FevycC0*nE3Xws8gsWp|Rj2>SM
zns)qcYj?^2sd8?N!_w~4v+f-HCF|a$TNZDoNl$I1Uq87euoNgKb6&r26TNrfkUa@o
zfdiFA@p{K&mH3b8i!lcoz)V{n8Q@g(vR4ns4r6w;K
z>1~ecQR0-<^J|Ndg5fvVUM9g;lbu-){#ghGw(fg>L
zh)T5Ljb%lWE;V9L!;Cqk>AV1(rULYF07ZBJbGb9qbSoLAd;in9{)95YqX$J43-dY7YU*k~vrM25
zxh5_IqO0LYZW%oxQ5HOzmk4x{atE*vipUk}sh88$b2tn?!ujEHn`tQLe&vo}nMb&{
zio`xzZ&GG6&ZyN3jnaQy#iVqXE9VT(3tWY$n-)uWDQ|tc{`?fq2F`oQ{;d3aWPg4Hp-(iE{ry>MIPWL>
iW8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 00000000000..f3c28516fb3
--- /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 00000000000..81e0505f069
--- /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 00000000000..86070722733
--- /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 00000000000..4a2a29254db
--- /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 00000000000..1435b505940
--- /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 00000000000..9c4f8ffeeac
--- /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 00000000000..efb5f423701
--- /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 00000000000..efe9e8afb90
--- /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