From fb8d6cb538511d2e9745b764c91f111861035a1d Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Mon, 2 Nov 2015 17:40:41 -0800 Subject: [PATCH] contact selection reeemix 1) RecyclerView-based, with better long scroller and more material-inspired look. 2) Add badge for Signal users to contact selection list. // FREEBIE --- build.gradle | 12 +- res/drawable-hdpi/ic_badge_24dp.png | Bin 0 -> 1816 bytes res/drawable-hdpi/ic_signal_grey_24dp.png | Bin 0 -> 875 bytes res/drawable-hdpi/ic_signal_white_48dp.png | Bin 0 -> 1357 bytes res/drawable-mdpi/ic_badge_24dp.png | Bin 0 -> 1075 bytes res/drawable-mdpi/ic_signal_grey_24dp.png | Bin 0 -> 596 bytes res/drawable-mdpi/ic_signal_white_48dp.png | Bin 0 -> 900 bytes .../recycler_view_fast_scroller_bubble.xml | 9 + res/drawable-xhdpi/ic_badge_24dp.png | Bin 0 -> 2553 bytes res/drawable-xhdpi/ic_signal_grey_24dp.png | Bin 0 -> 1128 bytes res/drawable-xhdpi/ic_signal_white_48dp.png | Bin 0 -> 1747 bytes res/drawable-xxhdpi/ic_badge_24dp.png | Bin 0 -> 4120 bytes res/drawable-xxhdpi/ic_signal_grey_24dp.png | Bin 0 -> 1749 bytes res/drawable-xxhdpi/ic_signal_white_48dp.png | Bin 0 -> 2659 bytes res/drawable-xxxhdpi/ic_badge_24dp.png | Bin 0 -> 3299 bytes res/drawable-xxxhdpi/ic_signal_grey_24dp.png | Bin 0 -> 1425 bytes res/drawable-xxxhdpi/ic_signal_white_48dp.png | Bin 0 -> 1905 bytes res/drawable/badge_drawable.xml | 6 + .../recycler_view_fast_scroller_bubble.xml | 9 + .../recycler_view_fast_scroller_handle.xml | 17 ++ .../contact_selection_list_fragment.xml | 15 +- res/layout/contact_selection_list_item.xml | 115 ++++----- .../contact_selection_recyclerview_header.xml | 9 + res/layout/recycler_view_fast_scroller.xml | 24 ++ res/values/attrs.xml | 3 +- .../ContactSelectionListFragment.java | 224 ++++++++++++++++-- .../securesms/components/AvatarImageView.java | 39 ++- .../components/RecyclerViewFastScroller.java | 206 ++++++++++++++++ .../contacts/ContactSelectionListAdapter.java | 163 +++++++++---- .../contacts/ContactSelectionListItem.java | 8 +- .../thoughtcrime/securesms/util/ViewUtil.java | 38 +++ 31 files changed, 761 insertions(+), 136 deletions(-) create mode 100644 res/drawable-hdpi/ic_badge_24dp.png create mode 100644 res/drawable-hdpi/ic_signal_grey_24dp.png create mode 100644 res/drawable-hdpi/ic_signal_white_48dp.png create mode 100644 res/drawable-mdpi/ic_badge_24dp.png create mode 100644 res/drawable-mdpi/ic_signal_grey_24dp.png create mode 100644 res/drawable-mdpi/ic_signal_white_48dp.png create mode 100644 res/drawable-v12/recycler_view_fast_scroller_bubble.xml create mode 100644 res/drawable-xhdpi/ic_badge_24dp.png create mode 100644 res/drawable-xhdpi/ic_signal_grey_24dp.png create mode 100644 res/drawable-xhdpi/ic_signal_white_48dp.png create mode 100644 res/drawable-xxhdpi/ic_badge_24dp.png create mode 100644 res/drawable-xxhdpi/ic_signal_grey_24dp.png create mode 100644 res/drawable-xxhdpi/ic_signal_white_48dp.png create mode 100644 res/drawable-xxxhdpi/ic_badge_24dp.png create mode 100644 res/drawable-xxxhdpi/ic_signal_grey_24dp.png create mode 100644 res/drawable-xxxhdpi/ic_signal_white_48dp.png create mode 100644 res/drawable/badge_drawable.xml create mode 100644 res/drawable/recycler_view_fast_scroller_bubble.xml create mode 100644 res/drawable/recycler_view_fast_scroller_handle.xml create mode 100644 res/layout/contact_selection_recyclerview_header.xml create mode 100644 res/layout/recycler_view_fast_scroller.xml create mode 100644 src/org/thoughtcrime/securesms/components/RecyclerViewFastScroller.java diff --git a/build.gradle b/build.gradle index 43d71c338f..4c00f12fa7 100644 --- a/build.gradle +++ b/build.gradle @@ -59,8 +59,8 @@ dependencies { compile 'pl.tajchert:waitingdots:0.1.0' compile 'com.soundcloud.android:android-crop:0.9.10@aar' - compile 'com.android.support:appcompat-v7:22.1.1' - compile 'com.android.support:recyclerview-v7:21.0.3' + compile 'com.android.support:appcompat-v7:22.2.1' + compile 'com.android.support:recyclerview-v7:22.2.1' compile 'com.melnykov:floatingactionbutton:1.3.0' compile 'com.google.zxing:android-integration:3.1.0' compile ('com.android.support:support-v4-preferencefragment:1.0.0@aar'){ @@ -119,8 +119,8 @@ dependencyVerification { 'com.afollestad:material-dialogs:624dffff240533ca69414464f416c81c88c5c29689bb169093b9a333104f2471', 'pl.tajchert:waitingdots:2835d49e0787dbcb606c5a60021ced66578503b1e9fddcd7a5ef0cd5f095ba2c', 'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177', - 'com.android.support:appcompat-v7:9a2355537c2f01cf0b95523605c18606b8d824017e6e94a05c77b0cfc8f21c96', - 'com.android.support:recyclerview-v7:e525ad3f33c84bb12b73d2dc975b55364a53f0f2d0697e043efba59ba73e22d2', + 'com.android.support:appcompat-v7:4b5ccba8c4557ef04f99aa0a80f8aa7d50f05f926a709010a54afd5c878d3618', + 'com.android.support:recyclerview-v7:b0f530a5b14334d56ce0de85527ffe93ac419bc928e2884287ce1dddfedfb505', 'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263', 'com.google.zxing:android-integration:89e56aadf1164bd71e57949163c53abf90af368b51669c0d4a47a163335f95c4', 'com.android.support:support-v4-preferencefragment:5470f5872514a6226fa1fc6f4e000991f38805691c534cf0bd2778911fc773ad', @@ -149,8 +149,8 @@ dependencyVerification { 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94', 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0', 'org.whispersystems:curve25519-java:9ccef8f5aba05d9942336f023c589d6278b4f9135bdc34a7bade1f4e7ad65fa3', - 'com.android.support:support-v4:1e2e4d35ac7fd30db5ce3bc177b92e4d5af86acef2ef93e9221599d733346f56', - 'com.android.support:support-annotations:7bc07519aa613b186001160403bcfd68260fa82c61cc7e83adeedc9b862b94ae', + 'com.android.support:support-v4:c62f0d025dafa86f423f48df9185b0d89496adbc5f6a9be5a7c394d84cf91423', + 'com.android.support:support-annotations:104f353b53d5dd8d64b2f77eece4b37f6b961de9732eb6b706395e91033ec70a', ] } diff --git a/res/drawable-hdpi/ic_badge_24dp.png b/res/drawable-hdpi/ic_badge_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf4fba850c2aa412fc55cc8d41649ff7a6e9985 GIT binary patch literal 1816 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 BqND%- literal 0 HcmV?d00001 diff --git a/res/drawable-hdpi/ic_signal_white_48dp.png b/res/drawable-hdpi/ic_signal_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..28ede6b2a59f0a42077321bfb2b6151525ff873b GIT binary patch literal 1357 zcmV-T1+w~yP)ogg`qh^K`yYhL;hG zmywYI0I#v8XembmH`8$=09gT)VptgfH!(}l)TM|Dw&DlCefTaOCNeS40x1q+Zj3y665tgkjv$%XnS?ihMAUX89z@d{Oe%&m z!<`zrr6IL!{=pH<&sn?0cftsld&qJM@dOW@}8O#9*SFtGzumBrz0eyfo z7|rnmlLa!`8f-~Ht>ec5U$F8ZElcty;M8VIf#t&LZA1QGLxedTKe6b*Od8a~+VYKJ^I%!{=gs^%FI;L2i#t^g%NE1X{Zji}V7L#cu1+FZv_L~5y>DTozt*8r+%t)WQ zv?7)B^5R-HKmCT^=kRfIWJ10JoQa`bihITe9|6u#KAwwrta(9SHe@Gqp+HBWTYD12 zue&MlhNL6nRxtEb@FA^ES7mD%dzsT?Et~};1`oQh^Of0He4#X&L!|%0(>kI<29u>}Qim^UCt!qTxX`SFXwm`HAv%3V02mHbXTtfd88D zs13dQF*U!$^}3=RqFoF5i8wm2(j6iXWWdYRReP}^pFC6H9+Ah9;aJkL%Qy>}gr$c{ z_F+%8?!#^&i3@xM-t!bAQa7HEaTL&BDkLvS_bXNT!#~|L2DJpw@w8@vuqlaVRX&iRqaS(B2WTY}OVI&T7 zxPxg*h9f*R<*#`-jJx<)WhywGnmzAWm=a-BmSQDN;9@S}3NGf+97t{|K4&ATk5vkF zn(-2WH}ak_1K>fX{PTU`^=}XwVKiybqQQcIm*xU`I6xS!Ky(RozC8WG1I#Yb`Oipj z+j()a_qeBqo9Y>w8-UB1Ox{sQwG1u9>ny7WQ@JoYBg=oOS)_W?{mCx P00000NkvXXu0mjfk6DZI literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_badge_24dp.png b/res/drawable-mdpi/ic_badge_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..49b7c74eaff239a519aefbe26cfedbf68b7f55c5 GIT binary patch literal 1075 zcmV-31kC%1P)yx3g*0AlEUy?|w0K?@VmqIE!!3Uid zzik&6#{tRahEWVJNYNO_i#d1f1>aLqlQQRl~sOC5=b zPJgp~4zXef%$&D!9{J8xVb)K4-*d}d8l}KXTx4aLOCnu*NVvx{>QMKI@B0kQLG<>| zx`&Qn!)qBCECcL+7{rs`_uYKw8eRB;40uUIxx~dOm@av7CR}rmXVjrCZD30$5NQK< z-T#X#av4N_uDY1f&|s+%Py!-q{4SL_KnD1wgItHYw4p6A&@2jhByUAo5g9b-`H;sO z2~%cNMQ{SodtRgqoFC~z7eqSGxslIg#0hO^dkAVmEMPidZ;ApZ$11+>(GiPZXj9H} z!veU~eebpU($}HB@m;7#KW24)?2)3P6ACbhH6SLir?cAk2)@fd=)|R&p)a1#08v}r z`bMi&8)~-}YO@k*Tn}~6yN%9!F6lAUEo2cA%MlaUZp!a@kbkPfKfy%KRkfLA5qKJ`snhto#YNr~4o>0$thV<~VnT(!`#-ymuB0n@n zkzt&1T$*il>PuEzC~yOE&D*5M5|BFM75PK9>8ZX=7aMBR#V^V@o(o(*@QG3F1gh$35Xkp-_z&y(bj?>)mrcb*U5vv~$jZQ!2Zu_izIQqAl>q}u|N?~LQ z_7a}u@4tfg!Qb?m*5^##z`alF4R{{ii*2J;!q#ijrN002ovPDHLkV1k^;5y1ce literal 0 HcmV?d00001 diff --git a/res/drawable-mdpi/ic_signal_grey_24dp.png b/res/drawable-mdpi/ic_signal_grey_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2399fe616e2753f1415ecdc3564507ae08923bde GIT binary patch literal 596 zcmV-a0;~OrP)}T6Gw%%+R?^aJ zbIqI9ByCvQ?6f&)W77Pzq9*28znbgt%;%1FZ@WmrV|R{Ql9`_+J_Axit_p~#dWCy^(@;GG~@3mx`sBUs5x%gX+@U=)x>atO_#e$a>8c#SM;m1C{ULcvHTzA?=D z1j&7o2^XIB0Ku%cS_L)*JAGZwq?=+MUg8ZlQkduwq2TKDEV@J1me+kP_MO%G@B8qX!DfUdie?KmQLSgZ0>nAeGSSm!(+B=dC6-&t`H) zkb)@1kV8^R^R!P<9 i>HnAY%WbaC&++dm@8GzAXDrYF0000B)Y{d9!!!otL-9GyAOd zum9hiK6&UL-%YTIZS2r}+twx~G+T54{aVik*v<_;H5(#k`_v6~G%&X8E%fbvjB&be z0aVKRRss0VImWkIcM|)7eg&06B?ySm^|$bcL+X%vPH%!oAW{cSgeU`#nV3yggMJ-9 z*5m|18i88mH|s}@o zqQE}ULln4F(4{>pm4)_+?RPRqrP3Zv3k0@KEAQ10s0~#+aGC;BP}4lgc+jBA|dd#y0!VY=ndZ=9wf~ z);Akag;~Zl9i%M>B~EOWEmrJ^WDN-I1jd<*km*Hp?ARi2(c8+*L@!D@80<$fIW6#f z6nGW|83%KXYmVeM$V?Png6mpIYNg4I^*8AW??;21Bl&hgk)2_V>(i$m1t(=^tpcwt zIaM5$bx>lD=wMHj*_QM2bF-c#+9=a)CR%=5=pb5N9VAIWC6N8T4yj z>^;H10zS50=j2V9{w=B2&D%;8E;6{ozY_A5@gd%@K_WFvFX0I%FRswC5E!y7Vvp!; a?Ee9}xPs2h7qTP(0000 + + + + + \ No newline at end of file diff --git a/res/drawable-xhdpi/ic_badge_24dp.png b/res/drawable-xhdpi/ic_badge_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..15fde7bffdc87ece56e089ac46de1f19360978ef GIT binary patch literal 2553 zcmVH-V2W^Rj1~4{gm6#Y}G&Mm}LZcET8jXR(_!lt(t(JdOjD#*-`fCeF zp~a{z#cr#FD3X*4gl$5AXjjvPZK-h;w`IHU&V9?f~6e+94}xEJ_6@J}EF%0LVZ0B3+d0Y3va0;@A*&P(AS z-g6^xJ8%%_0VIyWl{KGJ5b@ifo1BDUpqR$ z<^0jH;)13&oc8zUTPPercs#Kz_uukJR1c7`s zX)^ySl;aJD3sSc=kR=%WDkZerkpp%+(yZ~0@ZYfw-?2Sqs4U8aY>wef3jCuI2nfmp z?L&7eH#&G$x#w7MX>YN7CXO9>_Lt7SQhfd1ezj?u1j#xwe0GXtgR7J33t1|YvUQA8 z7;8!il$c?;`0I|*&(0VwM<(I}H1bKa5emE-_S)FO8S`h@5vc{95(>)Z7*{`<_m!YI z;gM88scBmBc4uXW$dW&YD09PJC7;avmAf3_&?i4fB@~p+F&1DfjLESDg=+v#Hh?rw zaZc^9XmDEFpgs4Q61v=axWJ`dN%FppXOODuT}2<^7%Qzg>DUwCU;~MIqSAu5%Z*M6 zfA)XqX5X34t~W>MTz2N&Y-NxjQqD~HBr}v}TelJ%8yrZh?80kO4tlG=hlN0wwou2_ z`n?azF@!Ker0lgKc|U|M=RRP=s|}mWNseuZEIDqhC8`K&a1jO-6uS|rR-f?A=j%32 zh@9qx%D$vpygyiuS0fTE*4gKPnGS209kY!3Bah^fE`+k>n2|$&GjjN3AT6&Mx%oFB z17&RZbxKSp58&A3pj^O7#IAx(Ke~VgN`47&o~lLM`jDrLCbQoPVly}_9S?wsfP5%l z{9P#T_85tZIT&nSdhSY=fdL+Q&H4 zoCdn8nBq&#*^x?H@>%hlaA6aD zLV4+(P)0sTWQ>ukx%V2a@h{I3K_C}`QX$JZps^6BS}hUojV$0)1q_W z;K5CV=3{4|n%rg)2(_OHWmpv;VN2dBz?A^^?W zqCgeMN70m4t-5(V93XaM1$KD}TDA``TpsB#>bNu)V^@H{a}KLgfFGPa7sxe_X)3M# z&5S!bIFJiCxlN5+ZKge<3g~97FX`Dh0y0O(29hb9pp(-F81ujPT!@I1lUy)mJ{B|` z84Bd_*WoX8Vn4!VPY}3}6Sz4AP&nU_DiC`L$Sf7`QwEyFZ{IQuoqahdq2vRj&oHf= zoa_#`M>N5SIr}u9>J|9qGztvRgzO4rn^3kRe-!PTu;c#WA4WzBIXZI=3Jl}}IrX1F zZg?WpbFnx3;ap8TP z+KDI6=6O}%`P#!MVcQw*2~8l@{%*i%l4tY@f}9kf#QFn9Qwc9KCFZh`{Y4enSaT}jhf=$l-LyaC z%!e9AmArN0G5ExH{+MK@D?A0NPbCU|Dp9hz&_k-gDxjZ!03|Y#xtaF@%v;mSj zgRIohfw3yssWs)c(S?1hJJ4*Ze4yFyA83{Wz4k~+SKyJ7$GU2clmPdEyj+J^|7rNa zC-;3A>+o*vlOV2bYd=!@XMr!IKdCeWq8iTjP`}h0d@+>JPFNP1?YoF01>n<DrTo19 zUql=xY$VT!Qc9A{(XGlY%Kp60J1I6e(lvIkQ)22`37Gc_4h)te-xZXeR!(1ew#o|* zIFVE0$HFqX$+6fgCaHAAv@GL_Y1#BwOfq%FL}RyRBXT+@@2{(+)a#uT@5zSl8ZE_x zE;`w8zya+gpSVlDHC0^lt*Ly;H#zH!fJw?M2Tsv!YA?4PPR!})j*-tnXb0e~Sxesa zsDtE+GhVZ@jej+(GOe7l%>BgBCB~pqs0-p!A07rmMAOJ=^vl#>Oz`uYOfCqtFfh*?vCpZ5GvU4xvqlADX P00000NkvXXu0mjf_T}8c literal 0 HcmV?d00001 diff --git a/res/drawable-xhdpi/ic_signal_grey_24dp.png b/res/drawable-xhdpi/ic_signal_grey_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..9088e211df652690ebdc6733a69d569af4a13ec1 GIT binary patch literal 1128 zcmV-u1eg1XP)1*rHl~13w^opv7s~i5kqC3#t=RS9xZfF>N?pybaJIQS_ zd_^rHLVB4hk4TRo0g=2a`^sEZlY5f;jr_A7M4$+Ll!s7{40#Zr`FdWTuCDe|q@Ohg z62-`?JVq!1kK8Q_^UliXWg!g)iXoDc+CY4qBJ!E6uXk4BLKc>HlF3bxNUw%8%0?Bq zkU8Wnfei9So7@(mgsfFQo?G&&M}^>(l=w^*7;)~%9=tHDhMvd;b^8c0$zsyPmc_|e zFA)g*mV8Lf`mYd>9yK8v7I;r)v?E$hx+JpW@dgq^LJygiN@WW@T$WaWlbF8|PmZi! zk;xr3J@`mouHn(>VeC)9>Y>^@kE!(f`kuL;QgW|tH6B}NQh@DUPG7CHz^v>jNh?49K{U> zp}_r=ox5-r&EP-sC=G}d8b=0Spk1jOi3YUcSoM&x9YiWHm|eY4U`GEr1q=r&q7oly z_H;mrCLDk%wgdQ(0&ApFH`L`j_>4_9BN9x+jTl4&ssBK~2IhFC8V6t65yc)nUPK#P z+@f*qi>crl+kqjF_OB*65O~g`eHhT6=)naTgL;Z4Ita3BF{0e?wk*hh`!9uP%MyP53~!GgN)XE zht~e5qKhp~UW@pkJyW{aE#W%_$qpvrS#;x1J_Da)8kY*|G2>DJcQV(aB8g{&At{VD zZ;21kk5;AcL?iY{c#i2n8QdmZi;DJxT?BSS1dhZBIF2|T+tGnf+7I3~-&Xxx@-ug| z1P@a*rpXHZ$=a-m0O2|y|HYH34h2T;9 zt)f1DtFSz*F9QI%N|k zQISNh-BAVfj8%5FU*Ij7Lu4R+OEpA{)+!FDn4T^+=Zs=l6d;w=TG9{7{>jZvl9*YJ zlpj6REkI}5Y@)2Jy^T~elGUKukvikRA%}VJF5&m9H_K>UPDfvP^S9A;F zS+BTB=Bb+A5aE!S6f??$N_jh~vA3baL ul>LW?+$DF*V)0!27lvEX{tuANkllU0qLrt?*O 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 literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/ic_signal_grey_24dp.png b/res/drawable-xxxhdpi/ic_signal_grey_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7df7b11d8055050dc45f967e41bb9d8fdd5426ae GIT binary patch literal 1425 zcmV;C1#bF@P)9`wtcX+ZEx=Vl03DUowaS-Hfr0pZQH#@jc0TB?D>jGnn~VCW)}B<*!F&X zf5TurVSfJqF&Y>*8Mhd>8+RKihBEFo?lf*QZZ@tliWwfevvDa^N!_BJQj^qFm8KZB zMWMg#qwZCgDUV&*vIuITF@|FuXt86MhA(gpJo4mAT#0Vj3dYrJJQTt^1}Kasm;?&j ziq5#m3BU~)58Bj0yo2(|1k^wm906@@HQFZ&&|Dw$b~gak6AdVhZeRl2jE0E>oQVlw zf;)t#P%<$9KQh6jG|Wtp0|Gb*CavKpB>~9Cmp8&h5CHt*@#T9#0Ju^xzO4Rs07_vx z=vW=4fD#xEI@kgDWdH+oxQ!?qKj1>_10An>ynw=YXOz79> znjVZD@F3`-)}c&nfYMkEx~d0b1Kba~tkozV3t%?rx>BtJE=Il!0e!6l-UMA(1{zoc z6vjNzo&p8gM`&&ha5?sa(tPw%(f3zgrr5(6Y7Nj@zTFjD^CJ%B49?_acI7K<#8f$8 zwW?_0!(EI5CHd*&dtAmG=Kfti_izO7qfbmwcUS?;lx=+6bWEcuvXA>VxHhBso}B{cm{Ks7O^~jNv{OJ;2qHb zQ)PhJrv>ElTPfhSXn@Hwz?3L}kEDRx6ADo5ahh`=gWLHCQ{4*yQ}`;=m>zNaPh(^H z-3;)v5l1mU?D&~X=No$ayA%MX(%`Z%gbe1eI~y|Kb^z$(6P&}$Q|QN zq*o4@Y2_{Dbm?WZ9oI0+1o+Voz^_&SFG+7@k8?XSOn`w@3`ha@S^?Z4y#~F;ET)?P zQyGu~_Npta0IFcU4DdB`!@Qkb$`;Z)!Cb4KjrNjVM7xFoWN{rkNiU>dTLpM!Z)zs+ z5OYr{pV@p)b|F22Ypnt*V6EgX@F9Qa&!&E(A-fkZLGh>n-5av&(7p7Ulb!)jgqzg0 zUvZn-jarrfy6!Fm`~**IfQwwU5BA|~%K+V1E&{v{Pwaq-*aEtuO{f?XK-U7tTmfho z|MNlvK*t+~!tnztVk_uqnWz{415btI4_pd6gvJGP#0AiV);QuA;1@l9JOEAj0(7L& zC~eP!ucaM%_-r=H>+LTB6f99P>5pIL;{rXt44?_$CiOu-zaC!>&`Wj1p(KqD)A%xg zUaldwIxxNzpa}(w*1)I)j~~$E+Y8Y9p%fm#4lp5YvoO8{pck)$E;tO@++@_VFun+& z52a9-qKB=BktnOj7Xz##y4rGDAH$C*7326CAVt)|gP0`R7YEToAD%G-garMcUIHKb zV|pClF3iP^dVCo`&Iok{?t~vdU^1p)DyD@B6@00000NkvXXu0mjfAH9NG literal 0 HcmV?d00001 diff --git a/res/drawable-xxxhdpi/ic_signal_white_48dp.png b/res/drawable-xxxhdpi/ic_signal_white_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..c17e9ae4d5d5b6481eb624fddffb3abedd3b4ffe GIT binary patch literal 1905 zcmV-%2afoOP)G0000L#Nklr4)giDPf|b5y6mr9r*blqJS#6S-)~7{ zNroI!Vn9(aSTb@-2F$X+5l(Q9^PJ}#Cpf|)vkWQ8$*>qrtgFx+6-%r#VjMs4WX$prrpdRMBRiyE zC^(N4mr!8(9R7gUA`!69@-V^=xfB~^HjJfq3Aq7rE*^02-N-8{` zEWXaaMq1=C#b1yjWi0Vmq{<+M_=lv-Oz82?kUEDoepL|Qrb`)*I;z#+rOPLmIj5a zU+qWR5G8Wx%^D2%p=`xYPry9Nc9!+%tsj?SPN3|}h3P!;BZ)pe#Ceq6xianXuXBJ? zD0_3Uu7A?KRmmdzqa2k!ep9R-Kc{y*jo}WIcDxRw3s6~{9>Aq_Fo)rDDDAlqlTriH z9We`xhqBUb19}VNp^PxHM-YljeG67^_@%ua<4j<||A~)r3)O}hdKK^E_dG^OT#iZa z8HmUD6}RtN=_)@AN`g&o3Hl%}-M89X_&YUW;8J??z2^6AUyb93Jebg@ZdeH-{-2Fi z^H+mVa3Y;k(BF?;_xGuh<`tMUZo@oykc~ZmMIM{NCHOI3bfC&tc?@Y>3U7h^;=r|k z6=|yYKfqCQi^uZeF=UzZ7qE;hfAXsSC&+RP6Z>n}ANH#MrO0x%zkp{T%k^IM&r&1F zTYLprMo3coK6B6g$YSH(1=NQ;;kQx0v_TfVfS<%Iz(p_MhsfgkF5p*DKOI3Hy@0y5G&ykb(dQ!6^8C`_bQh{3-xy{!Z5F z0QyP7p-Ah^fPD`pxSpDjC?vm)D{y~|Dlh@LuYmUu4)?dJ`N5O~7;c~;j&mXYL`XPa z0cQw@ZwcQ66SyU5SR@=i*m4mX4(nK70W*ZdOIy|a8p7dq{sIcZ;i^_O-9JU ziPklLfiPGn>tY5t!r=9-YknJHa2=)#0ipa=;q9?J4b-rN>7oT0jv-IYvDP)8rB31! z+$b0T#PpPCAOc)h0vz)EHJ0~c8lH{qN`U1Kr1@Dy@5MA%fUX1>=8$H*ry{el}ZCp3#2)>puqywxX2H_@9b`qijSuF{St$0U00R|0ws7O-(?f zpyK~1gXzWJ2xxZ2$~mlu0IxRmY?KksGky;RIZ7oE|Asa+q~EMyLc?AXi;`GMuABKXG{%|wdhVdVaf{K3d z9$XWD2w-}pV6Da4M^HgY>J%H}4^ctEnl3$M7392!G#PG;KV$`t>k_@`-UtX?LB=tS zUsi$Ta8mJGkNAZY7z#e3qhD4*#%W?_=pVmqD;g3rm3;jE^T2U|I4M2i4@!Y$z_*Bt z&yjn?ADjY1#uD*xH!j%tJ?25qIbz^qvY_MlmjC@2{)!z}YGu*5P; ztZ<4E2jM;Av>w&4O-dss=NXL8NwRHYUpbHj$IXn(P+Lz|A}kr#F%GNUZn#v1|f;1YIb zy?Llm;};Nc0;l0i33xuGj{WHgxR}k%lWF);0?v_RG=3QYucOk&BPU=^8;_KL8#sy6 r#v=qq!xyLD;cNVgMz1J}GA927d7;sJ*=x}L00000NkvXXu0mjfl9i4X literal 0 HcmV?d00001 diff --git a/res/drawable/badge_drawable.xml b/res/drawable/badge_drawable.xml new file mode 100644 index 0000000000..5a87e3c781 --- /dev/null +++ b/res/drawable/badge_drawable.xml @@ -0,0 +1,6 @@ + + + diff --git a/res/drawable/recycler_view_fast_scroller_bubble.xml b/res/drawable/recycler_view_fast_scroller_bubble.xml new file mode 100644 index 0000000000..417f6e4bc6 --- /dev/null +++ b/res/drawable/recycler_view_fast_scroller_bubble.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/recycler_view_fast_scroller_handle.xml b/res/drawable/recycler_view_fast_scroller_handle.xml new file mode 100644 index 0000000000..fa2ca8a58a --- /dev/null +++ b/res/drawable/recycler_view_fast_scroller_handle.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/contact_selection_list_fragment.xml b/res/layout/contact_selection_list_fragment.xml index f54bcb98c8..a0c8e9f44a 100644 --- a/res/layout/contact_selection_list_fragment.xml +++ b/res/layout/contact_selection_list_fragment.xml @@ -1,5 +1,5 @@ - @@ -9,8 +9,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - @@ -23,4 +23,11 @@ android:textSize="20sp" /> - + + + + diff --git a/res/layout/contact_selection_list_item.xml b/res/layout/contact_selection_list_item.xml index 149350a7bd..ab4ab40aaa 100644 --- a/res/layout/contact_selection_list_item.xml +++ b/res/layout/contact_selection_list_item.xml @@ -1,72 +1,79 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="?android:attr/listPreferredItemHeight" + android:orientation="horizontal" + android:gravity="center_vertical" + android:background="@drawable/conversation_list_item_background" + android:paddingLeft="48dp" + android:paddingRight="20dp"> - - - + android:layout_weight="1" + android:orientation="vertical"> + + + + + + + + + + - - - + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:focusable="false" + android:clickable="false" /> diff --git a/res/layout/contact_selection_recyclerview_header.xml b/res/layout/contact_selection_recyclerview_header.xml new file mode 100644 index 0000000000..82cdc469e0 --- /dev/null +++ b/res/layout/contact_selection_recyclerview_header.xml @@ -0,0 +1,9 @@ + + diff --git a/res/layout/recycler_view_fast_scroller.xml b/res/layout/recycler_view_fast_scroller.xml new file mode 100644 index 0000000000..2edd24c130 --- /dev/null +++ b/res/layout/recycler_view_fast_scroller.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index a36cc0a386..7657dc2565 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -129,7 +129,8 @@ - + + diff --git a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java index bec8d3a138..bcb7c909a9 100644 --- a/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java +++ b/src/org/thoughtcrime/securesms/ContactSelectionListFragment.java @@ -18,30 +18,36 @@ package org.thoughtcrime.securesms; import android.database.Cursor; +import android.graphics.Canvas; +import android.graphics.Rect; import android.os.Build; +import android.os.Build.VERSION; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; -import android.support.v4.widget.CursorAdapter; +import android.support.v4.view.ViewCompat; import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; import android.widget.TextView; +import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader; +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; +import org.thoughtcrime.securesms.util.ViewUtil; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; -import se.emilsjolander.stickylistheaders.StickyListHeadersListView; - /** * Fragment for selecting a one or more contacts from a list. * @@ -65,9 +71,10 @@ public class ContactSelectionListFragment extends Fragment private Map selectedContacts; private OnContactSelectedListener onContactSelectedListener; - private StickyListHeadersListView listView; private SwipeRefreshLayout swipeRefresh; private String cursorFilter; + private RecyclerView recyclerView; + private RecyclerViewFastScroller fastScroller; @Override public void onActivityCreated(Bundle icicle) { @@ -89,13 +96,12 @@ public class ContactSelectionListFragment extends Fragment public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false); - emptyText = (TextView) view.findViewById(android.R.id.empty); - swipeRefresh = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh); - listView = (StickyListHeadersListView) view.findViewById(android.R.id.list); - listView.setFocusable(true); - listView.setFastScrollEnabled(true); - listView.setDrawingListUnderStickyHeader(false); - listView.setOnItemClickListener(new ListClickListener()); + emptyText = ViewUtil.findById(view, android.R.id.empty); + recyclerView = ViewUtil.findById(view, R.id.recycler_view); + swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh); + fastScroller = ViewUtil.findById(view, R.id.fast_scroller); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + fastScroller.setRecyclerView(recyclerView); swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN); @@ -117,9 +123,13 @@ public class ContactSelectionListFragment extends Fragment } private void initializeCursor() { - ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(), null, isMulti()); + ContactSelectionListAdapter adapter = new ContactSelectionListAdapter(getActivity(), + null, + new ListClickListener(), + isMulti()); selectedContacts = adapter.getSelectedContacts(); - listView.setAdapter(adapter); + recyclerView.setAdapter(adapter); + recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, true)); this.getLoaderManager().initLoader(0, null, this); } @@ -147,19 +157,18 @@ public class ContactSelectionListFragment extends Fragment @Override public void onLoadFinished(Loader loader, Cursor data) { - ((CursorAdapter) listView.getAdapter()).changeCursor(data); + ((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(data); emptyText.setText(R.string.contact_selection_group_activity__no_contacts); } @Override public void onLoaderReset(Loader loader) { - ((CursorAdapter) listView.getAdapter()).changeCursor(null); + ((CursorRecyclerViewAdapter) recyclerView.getAdapter()).changeCursor(null); } - private class ListClickListener implements AdapterView.OnItemClickListener { + private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener { @Override - public void onItemClick(AdapterView l, View v, int position, long id) { - ContactSelectionListItem contact = (ContactSelectionListItem)v; + public void onItemClick(ContactSelectionListItem contact) { if (!isMulti() || !selectedContacts.containsKey(contact.getContactId())) { selectedContacts.put(contact.getContactId(), contact.getNumber()); @@ -185,4 +194,181 @@ public class ContactSelectionListFragment extends Fragment void onContactSelected(String number); void onContactDeselected(String number); } + + /** + * A sticky header decoration for android's RecyclerView. + */ + public static class StickyHeaderDecoration extends RecyclerView.ItemDecoration { + + private Map mHeaderCache; + + private StickyHeaderAdapter mAdapter; + + private boolean mRenderInline; + + /** + * @param adapter the sticky header adapter to use + */ + public StickyHeaderDecoration(StickyHeaderAdapter adapter, boolean renderInline) { + mAdapter = adapter; + mHeaderCache = new HashMap<>(); + mRenderInline = renderInline; + } + + /** + * {@inheritDoc} + */ + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) + { + int position = parent.getChildAdapterPosition(view); + + int headerHeight = 0; + if (position != RecyclerView.NO_POSITION && hasHeader(position)) { + View header = getHeader(parent, position).itemView; + headerHeight = getHeaderHeightForLayout(header); + } + + outRect.set(0, headerHeight, 0, 0); + } + + private boolean hasHeader(int position) { + if (position == 0) { + return true; + } + + int previous = position - 1; + return mAdapter.getHeaderId(position) != mAdapter.getHeaderId(previous); + } + + private RecyclerView.ViewHolder getHeader(RecyclerView parent, int position) { + final long key = mAdapter.getHeaderId(position); + + if (mHeaderCache.containsKey(key)) { + return mHeaderCache.get(key); + } else { + final RecyclerView.ViewHolder holder = mAdapter.onCreateHeaderViewHolder(parent); + final View header = holder.itemView; + + //noinspection unchecked + mAdapter.onBindHeaderViewHolder(holder, position); + + int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY); + int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED); + + int childWidth = ViewGroup.getChildMeasureSpec(widthSpec, + parent.getPaddingLeft() + parent.getPaddingRight(), header.getLayoutParams().width); + int childHeight = ViewGroup.getChildMeasureSpec(heightSpec, + parent.getPaddingTop() + parent.getPaddingBottom(), header.getLayoutParams().height); + + header.measure(childWidth, childHeight); + header.layout(0, 0, header.getMeasuredWidth(), header.getMeasuredHeight()); + + mHeaderCache.put(key, holder); + + return holder; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) { + final int count = parent.getChildCount(); + + for (int layoutPos = 0; layoutPos < count; layoutPos++) { + final View child = parent.getChildAt(layoutPos); + + final int adapterPos = parent.getChildAdapterPosition(child); + + if (adapterPos != RecyclerView.NO_POSITION && (layoutPos == 0 || hasHeader(adapterPos))) { + View header = getHeader(parent, adapterPos).itemView; + c.save(); + final int left = child.getLeft(); + final int top = getHeaderTop(parent, child, header, adapterPos, layoutPos); + c.translate(left, top); + header.draw(c); + c.restore(); + } + } + } + + private int getHeaderTop(RecyclerView parent, View child, View header, int adapterPos, + int layoutPos) + { + int headerHeight = getHeaderHeightForLayout(header); + int top = getChildY(parent, child) - headerHeight; + if (layoutPos == 0) { + final int count = parent.getChildCount(); + final long currentId = mAdapter.getHeaderId(adapterPos); + // find next view with header and compute the offscreen push if needed + for (int i = 1; i < count; i++) { + int adapterPosHere = parent.getChildAdapterPosition(parent.getChildAt(i)); + if (adapterPosHere != RecyclerView.NO_POSITION) { + long nextId = mAdapter.getHeaderId(adapterPosHere); + if (nextId != currentId) { + final View next = parent.getChildAt(i); + final int offset = getChildY(parent, next) - (headerHeight + getHeader(parent, adapterPosHere).itemView.getHeight()); + if (offset < 0) { + return offset; + } else { + break; + } + } + } + } + + top = Math.max(0, top); + } + + return top; + } + + private int getChildY(RecyclerView parent, View child) { + if (VERSION.SDK_INT < 11) { + Rect rect = new Rect(); + parent.getChildVisibleRect(child, rect, null); + return rect.top; + } else { + return (int)ViewCompat.getY(child); + } + } + + private int getHeaderHeightForLayout(View header) { + return mRenderInline ? 0 : header.getHeight(); + } + } + + /** + * The adapter to assist the {@link StickyHeaderDecoration} in creating and binding the header views. + * + * @param the header view holder + */ + public interface StickyHeaderAdapter { + + /** + * Returns the header id for the item at the given position. + * + * @param position the item position + * @return the header id + */ + long getHeaderId(int position); + + /** + * Creates a new header ViewHolder. + * + * @param parent the header's view parent + * @return a view holder for the created view + */ + T onCreateHeaderViewHolder(ViewGroup parent); + + /** + * Updates the header view to reflect the header data for the given position + * @param viewHolder the header view holder + * @param position the header's item position + */ + void onBindHeaderViewHolder(T viewHolder, int position); + } } diff --git a/src/org/thoughtcrime/securesms/components/AvatarImageView.java b/src/org/thoughtcrime/securesms/components/AvatarImageView.java index b97edadb59..6f7f462141 100644 --- a/src/org/thoughtcrime/securesms/components/AvatarImageView.java +++ b/src/org/thoughtcrime/securesms/components/AvatarImageView.java @@ -3,8 +3,13 @@ package org.thoughtcrime.securesms.components; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.os.AsyncTask; import android.provider.ContactsContract; import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v4.util.Pair; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; @@ -16,10 +21,13 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoFactory; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; +import org.thoughtcrime.securesms.util.DirectoryHelper; +import org.thoughtcrime.securesms.util.DirectoryHelper.UserCapabilities.Capability; public class AvatarImageView extends ImageView { private boolean inverted; + private boolean showBadge; public AvatarImageView(Context context) { super(context); @@ -33,18 +41,22 @@ public class AvatarImageView extends ImageView { if (attrs != null) { TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AvatarImageView, 0, 0); inverted = typedArray.getBoolean(0, false); + showBadge = typedArray.getBoolean(1, false); typedArray.recycle(); } } - public void setAvatar(@Nullable Recipients recipients, boolean quickContactEnabled) { + public void setAvatar(final @Nullable Recipients recipients, boolean quickContactEnabled) { if (recipients != null) { MaterialColor backgroundColor = recipients.getColor(); setImageDrawable(recipients.getContactPhoto().asDrawable(getContext(), backgroundColor.toConversationColor(getContext()), inverted)); setAvatarClickHandler(recipients, quickContactEnabled); + setTag(recipients); + if (showBadge) new BadgeResolutionTask(getContext()).execute(recipients); } else { setImageDrawable(ContactPhotoFactory.getDefaultContactPhoto(null).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted)); setOnClickListener(null); + setTag(null); } } @@ -74,4 +86,29 @@ public class AvatarImageView extends ImageView { } } + private class BadgeResolutionTask extends AsyncTask> { + private final Context context; + + public BadgeResolutionTask(Context context) { + this.context = context; + } + + @Override + protected Pair doInBackground(Recipients... recipients) { + Capability textCapability = DirectoryHelper.getUserCapabilities(context, recipients[0]).getTextCapability(); + return new Pair<>(recipients[0], textCapability == Capability.SUPPORTED); + } + + @Override + protected void onPostExecute(Pair result) { + if (getTag() == result.first && result.second) { + final Drawable badged = new LayerDrawable(new Drawable[] { + getDrawable(), + ContextCompat.getDrawable(context, R.drawable.badge_drawable) + }); + + setImageDrawable(badged); + } + } + } } diff --git a/src/org/thoughtcrime/securesms/components/RecyclerViewFastScroller.java b/src/org/thoughtcrime/securesms/components/RecyclerViewFastScroller.java new file mode 100644 index 0000000000..70c7336223 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/RecyclerViewFastScroller.java @@ -0,0 +1,206 @@ +/** + * Modified version of + * https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller + * + * Their license: + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thoughtcrime.securesms.components; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build.VERSION; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.ViewUtil; + +public class RecyclerViewFastScroller extends LinearLayout { + private static final int BUBBLE_ANIMATION_DURATION = 100; + private static final int TRACK_SNAP_RANGE = 5; + + private TextView bubble; + private View handle; + private RecyclerView recyclerView; + private int height; + private ObjectAnimator currentAnimator; + + private final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) { + if (bubble == null || handle.isSelected()) + return; + final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset(); + final int verticalScrollRange = recyclerView.computeVerticalScrollRange(); + float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height); + setBubbleAndHandlePosition(height * proportion); + } + }; + + public interface FastScrollAdapter { + CharSequence getBubbleText(int pos); + } + + public RecyclerViewFastScroller(final Context context) { + this(context, null); + } + + public RecyclerViewFastScroller(final Context context, final AttributeSet attrs) { + super(context, attrs); + setOrientation(HORIZONTAL); + setClipChildren(false); + inflate(context, R.layout.recycler_view_fast_scroller, this); + bubble = ViewUtil.findById(this, R.id.fastscroller_bubble); + handle = ViewUtil.findById(this, R.id.fastscroller_handle); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + height = h; + } + + @Override + @TargetApi(11) + public boolean onTouchEvent(@NonNull MotionEvent event) { + final int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + if (event.getX() < ViewUtil.getX(handle) - handle.getPaddingLeft() || + event.getY() < ViewUtil.getY(handle) - handle.getPaddingTop() || + event.getY() > ViewUtil.getY(handle) + handle.getHeight() + handle.getPaddingBottom()) + { + return false; + } + if (currentAnimator != null) { + currentAnimator.cancel(); + } + if (bubble.getVisibility() != VISIBLE) { + showBubble(); + } + handle.setSelected(true); + case MotionEvent.ACTION_MOVE: + final float y = event.getY(); + setBubbleAndHandlePosition(y); + setRecyclerViewPosition(y); + return true; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + handle.setSelected(false); + hideBubble(); + return true; + } + return super.onTouchEvent(event); + } + + public void setRecyclerView(final RecyclerView recyclerView) { + this.recyclerView = recyclerView; + recyclerView.addOnScrollListener(onScrollListener); + recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + recyclerView.getViewTreeObserver().removeOnPreDrawListener(this); + if (bubble == null || handle.isSelected()) + return true; + final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset(); + final int verticalScrollRange = recyclerView.computeVerticalScrollRange(); + float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height); + setBubbleAndHandlePosition(height * proportion); + return true; + } + }); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (recyclerView != null) + recyclerView.removeOnScrollListener(onScrollListener); + } + + private void setRecyclerViewPosition(float y) { + if (recyclerView != null) { + final int itemCount = recyclerView.getAdapter().getItemCount(); + float proportion; + if (ViewUtil.getY(handle) == 0) + proportion = 0f; + else if (ViewUtil.getY(handle) + handle.getHeight() >= height - TRACK_SNAP_RANGE) + proportion = 1f; + else + proportion = y / (float) height; + final int targetPos = Util.clamp((int)(proportion * (float)itemCount), 0, itemCount - 1); + ((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(targetPos, 0); + final CharSequence bubbleText = ((FastScrollAdapter) recyclerView.getAdapter()).getBubbleText(targetPos); + if (bubble != null) + bubble.setText(bubbleText); + } + } + + private void setBubbleAndHandlePosition(float y) { + final int handleHeight = handle.getHeight(); + final int bubbleHeight = bubble.getHeight(); + ViewUtil.setY(handle, Util.clamp((int)(y - handleHeight / 2), 0, height - handleHeight)); + ViewUtil.setY(bubble, Util.clamp((int)(y - bubbleHeight), 0, height - bubbleHeight - handleHeight / 2)); + } + + @TargetApi(11) + private void showBubble() { + bubble.setVisibility(VISIBLE); + if (VERSION.SDK_INT >= 11) { + if (currentAnimator != null) currentAnimator.cancel(); + currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).setDuration(BUBBLE_ANIMATION_DURATION); + currentAnimator.start(); + } + } + + @TargetApi(11) + private void hideBubble() { + if (VERSION.SDK_INT >= 11) { + if (currentAnimator != null) currentAnimator.cancel(); + currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).setDuration(BUBBLE_ANIMATION_DURATION); + currentAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + bubble.setVisibility(INVISIBLE); + currentAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + bubble.setVisibility(INVISIBLE); + currentAnimator = null; + } + }); + currentAnimator.start(); + } else { + bubble.setVisibility(INVISIBLE); + } + } +} diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java index 87ae41d951..d882f02ac1 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListAdapter.java @@ -20,101 +20,174 @@ import android.content.Context; import android.content.res.TypedArray; import android.database.Cursor; import android.provider.ContactsContract; -import android.support.v4.widget.CursorAdapter; -import android.util.Log; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.RecyclerView; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.ImageSpan; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.TextView; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.RecyclerViewFastScroller.FastScrollAdapter; +import org.thoughtcrime.securesms.ContactSelectionListFragment.StickyHeaderAdapter; +import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.HeaderViewHolder; +import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolder; +import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import java.util.HashMap; import java.util.Map; -import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; - /** * List adapter to display all contacts and their related information * * @author Jake McGinty */ -public class ContactSelectionListAdapter extends CursorAdapter - implements StickyListHeadersAdapter +public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter + implements FastScrollAdapter, + StickyHeaderAdapter { private final static String TAG = ContactSelectionListAdapter.class.getSimpleName(); private final static int STYLE_ATTRIBUTES[] = new int[]{R.attr.contact_selection_push_user, R.attr.contact_selection_lay_user}; - private final boolean multiSelect; - private final LayoutInflater li; - private final TypedArray drawables; + private final boolean multiSelect; + private final LayoutInflater li; + private final TypedArray drawables; + private final ItemClickListener clickListener; private final HashMap selectedContacts = new HashMap<>(); - public ContactSelectionListAdapter(Context context, Cursor cursor, boolean multiSelect) { - super(context, cursor, 0); - this.li = LayoutInflater.from(context); - this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES); - this.multiSelect = multiSelect; + public static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(@NonNull final View itemView, + @Nullable final ItemClickListener clickListener) + { + super(itemView); + itemView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (clickListener != null) clickListener.onItemClick(getView()); + } + }); + } + + public ContactSelectionListItem getView() { + return (ContactSelectionListItem) itemView; + } + } + + public static class HeaderViewHolder extends RecyclerView.ViewHolder { + public HeaderViewHolder(View itemView) { + super(itemView); + } + } + + public ContactSelectionListAdapter(@NonNull Context context, + @Nullable Cursor cursor, + @Nullable ItemClickListener clickListener, + boolean multiSelect) + { + super(context, cursor); + this.li = LayoutInflater.from(context); + this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES); + this.multiSelect = multiSelect; + this.clickListener = clickListener; } @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return li.inflate(R.layout.contact_selection_list_item, parent, false); + public long getHeaderId(int i) { + return getHeaderString(i).hashCode(); } @Override - public void bindView(View view, Context context, Cursor cursor) { + public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder(li.inflate(R.layout.contact_selection_list_item, parent, false), clickListener); + } + + @Override + public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(ContactsDatabase.ID_COLUMN)); int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)); String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN)); String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_COLUMN)); int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.NUMBER_TYPE_COLUMN)); String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.LABEL_COLUMN)); - String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(context.getResources(), + String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(), numberType, label).toString(); int color = (contactType == ContactsDatabase.PUSH_TYPE) ? drawables.getColor(0, 0xa0000000) : - drawables.getColor(1, 0xff000000); + drawables.getColor(1, 0xff000000); - - ((ContactSelectionListItem)view).unbind(); - ((ContactSelectionListItem)view).set(id, contactType, name, number, labelText, color, multiSelect); - ((ContactSelectionListItem)view).setChecked(selectedContacts.containsKey(id)); + viewHolder.getView().unbind(); + viewHolder.getView().set(id, contactType, name, number, labelText, color, multiSelect); + viewHolder.getView().setChecked(selectedContacts.containsKey(id)); } @Override - public View getHeaderView(int i, View convertView, ViewGroup viewGroup) { - Cursor cursor = getCursor(); - - final TextView text; - if (convertView == null) { - text = (TextView)li.inflate(R.layout.contact_selection_list_header, viewGroup, false); - } else { - text = (TextView)convertView; - } - - cursor.moveToPosition(i); - - int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)); - - if (contactType == ContactsDatabase.PUSH_TYPE) text.setText(R.string.contact_selection_list__header_signal_users); - else text.setText(R.string.contact_selection_list__header_other); - - return text; + public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent) { + return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false)); } @Override - public long getHeaderId(int i) { - Cursor cursor = getCursor(); - cursor.moveToPosition(i); + public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) { + ((TextView)viewHolder.itemView).setText(getSpannedHeaderString(position, R.drawable.ic_signal_grey_24dp)); + } - return cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)); + @Override + public CharSequence getBubbleText(int position) { + return getSpannedHeaderString(position, R.drawable.ic_signal_white_48dp); } public Map getSelectedContacts() { return selectedContacts; } + + private CharSequence getSpannedHeaderString(int position, @DrawableRes int drawable) { + Cursor cursor = getCursorAtPositionOrThrow(position); + + if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)) == ContactsDatabase.PUSH_TYPE) { + SpannableString spannable = new SpannableString(" "); + spannable.setSpan(new ImageSpan(getContext(), drawable, ImageSpan.ALIGN_BOTTOM), 0, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return spannable; + } else { + return getHeaderString(position); + } + } + + private String getHeaderString(int position) { + Cursor cursor = getCursorAtPositionOrThrow(position); + + if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactsDatabase.CONTACT_TYPE_COLUMN)) == ContactsDatabase.PUSH_TYPE) { + return getContext().getString(R.string.app_name); + } else { + String letter = cursor.getString(cursor.getColumnIndexOrThrow(ContactsDatabase.NAME_COLUMN)) + .trim() + .substring(0,1) + .toUpperCase(); + if (Character.isLetterOrDigit(letter.codePointAt(0))) { + return letter; + } else { + return "#"; + } + } + } + + private Cursor getCursorAtPositionOrThrow(int position) { + Cursor cursor = getCursor(); + if (cursor == null) { + throw new IllegalStateException("Cursor should not be null here."); + } + if (!cursor.moveToPosition(position)); + return cursor; + } + + public interface ItemClickListener { + void onItemClick(ContactSelectionListItem item); + } } diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java index c7b16a6582..70a1870893 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java @@ -5,7 +5,7 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.CheckBox; -import android.widget.RelativeLayout; +import android.widget.LinearLayout; import android.widget.TextView; import org.thoughtcrime.securesms.R; @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.Recipients; -public class ContactSelectionListItem extends RelativeLayout implements Recipients.RecipientsModifiedListener { +public class ContactSelectionListItem extends LinearLayout implements Recipients.RecipientsModifiedListener { private AvatarImageView contactPhotoImage; private TextView numberView; @@ -34,10 +34,6 @@ public class ContactSelectionListItem extends RelativeLayout implements Recipien super(context, attrs); } - public ContactSelectionListItem(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - @Override protected void onFinishInflate() { super.onFinishInflate(); diff --git a/src/org/thoughtcrime/securesms/util/ViewUtil.java b/src/org/thoughtcrime/securesms/util/ViewUtil.java index 9cd4af625e..fdfbfa943d 100644 --- a/src/org/thoughtcrime/securesms/util/ViewUtil.java +++ b/src/org/thoughtcrime/securesms/util/ViewUtil.java @@ -24,6 +24,7 @@ import android.support.annotation.IdRes; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.v4.view.ViewCompat; import android.support.v4.view.animation.FastOutSlowInInterpolator; import android.text.TextUtils; import android.text.TextUtils.TruncateAt; @@ -33,6 +34,7 @@ import android.view.ViewGroup; import android.view.ViewStub; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; +import android.widget.LinearLayout.LayoutParams; import android.widget.TextView; public class ViewUtil { @@ -45,6 +47,42 @@ public class ViewUtil { } } + public static void setY(final @NonNull View v, final int y) { + if (VERSION.SDK_INT >= 11) { + ViewCompat.setY(v, y); + } else { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)v.getLayoutParams(); + params.topMargin = y; + v.setLayoutParams(params); + } + } + + public static float getY(final @NonNull View v) { + if (VERSION.SDK_INT >= 11) { + return ViewCompat.getY(v); + } else { + return ((ViewGroup.MarginLayoutParams)v.getLayoutParams()).topMargin; + } + } + + public static void setX(final @NonNull View v, final int x) { + if (VERSION.SDK_INT >= 11) { + ViewCompat.setX(v, x); + } else { + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)v.getLayoutParams(); + params.leftMargin = x; + v.setLayoutParams(params); + } + } + + public static float getX(final @NonNull View v) { + if (VERSION.SDK_INT >= 11) { + return ViewCompat.getX(v); + } else { + return ((LayoutParams)v.getLayoutParams()).leftMargin; + } + } + public static void swapChildInPlace(ViewGroup parent, View toRemove, View toAdd, int defaultIndex) { int childIndex = parent.indexOfChild(toRemove); if (childIndex > -1) parent.removeView(toRemove);