├── .gitignore ├── .idea └── encodings.xml ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── site │ │ └── hanschen │ │ └── pretty │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── ic_launcher_round-web.png │ ├── java │ │ └── site │ │ │ └── hanschen │ │ │ └── pretty │ │ │ ├── application │ │ │ └── PrettyApplication.java │ │ │ ├── base │ │ │ ├── BaseActivity.java │ │ │ └── HttpClient.java │ │ │ ├── config │ │ │ └── PrettyGlideModule.java │ │ │ ├── db │ │ │ ├── bean │ │ │ │ ├── Picture.java │ │ │ │ └── Question.java │ │ │ ├── gen │ │ │ │ ├── DaoMaster.java │ │ │ │ ├── DaoSession.java │ │ │ │ ├── PictureDao.java │ │ │ │ └── QuestionDao.java │ │ │ └── repository │ │ │ │ ├── PrettyRepository.java │ │ │ │ └── PrettyRepositoryImpl.java │ │ │ ├── eventbus │ │ │ ├── EditModeChangedEvent.java │ │ │ ├── NewPictureEvent.java │ │ │ ├── NewQuestionEvent.java │ │ │ └── ShareFromZhihuEvent.java │ │ │ ├── service │ │ │ ├── Dispatcher.java │ │ │ ├── TaskManager.java │ │ │ ├── TaskObservable.java │ │ │ ├── TaskObserver.java │ │ │ ├── TaskService.java │ │ │ └── UrlHunter.java │ │ │ ├── ui │ │ │ ├── picture │ │ │ │ ├── GalleryActivity.java │ │ │ │ ├── GalleryPagerAdapter.java │ │ │ │ ├── PictureAdapter.java │ │ │ │ └── PictureListActivity.java │ │ │ ├── question │ │ │ │ ├── QuestionActivity.java │ │ │ │ ├── QuestionCategory.java │ │ │ │ ├── QuestionFragment.java │ │ │ │ ├── QuestionListAdapter.java │ │ │ │ └── QuestionPagerAdapter.java │ │ │ └── splash │ │ │ │ └── SplashActivity.java │ │ │ ├── utils │ │ │ ├── ColorUtils.java │ │ │ ├── CommonUtils.java │ │ │ └── JsonUtils.java │ │ │ ├── widget │ │ │ ├── BackHandlerHelper.java │ │ │ ├── DepthPageTransformer.java │ │ │ ├── FragmentBackHandler.java │ │ │ ├── ScrollBehavior.java │ │ │ ├── ScrollViewPager.java │ │ │ └── ViewPagerCatchException.java │ │ │ └── zhihu │ │ │ ├── ZhiHuApi.java │ │ │ ├── ZhiHuApiApiImpl.java │ │ │ └── bean │ │ │ ├── AnswerList.java │ │ │ └── RequestAnswerParams.java │ └── res │ │ ├── drawable-v21 │ │ └── bg_ripple_rectangle.xml │ │ ├── drawable-xhdpi │ │ ├── ic_action_back.png │ │ ├── icon_back_normal.png │ │ ├── icon_back_press.png │ │ ├── icon_menu_delete.png │ │ ├── icon_share.png │ │ ├── icon_share_press.png │ │ └── profile_cover.jpg │ │ ├── drawable │ │ ├── back_picture_selector.xml │ │ ├── bg_gallery_select_mark.xml │ │ ├── bg_ripple_rectangle.xml │ │ ├── ic_add_black_24dp.xml │ │ ├── ic_close_black_24dp.xml │ │ ├── ic_delete_black_24dp.xml │ │ ├── ic_refresh_black_24dp.xml │ │ ├── ic_select_all_black_24dp.xml │ │ ├── ic_tab_unselected_black_24dp.xml │ │ ├── icon_share_selector.xml │ │ └── shape_circle_primary_color.xml │ │ ├── layout │ │ ├── activity_gallery.xml │ │ ├── activity_picture_list.xml │ │ ├── activity_question.xml │ │ ├── fragment_question_list.xml │ │ ├── item_gallery_pager.xml │ │ ├── item_gallery_recycle_view.xml │ │ ├── item_picture.xml │ │ ├── list_item_two_line_with_icon.xml │ │ └── list_item_two_line_with_icon_and_check.xml │ │ ├── menu │ │ ├── menu_picture_list.xml │ │ └── menu_question_fragment.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-v19 │ │ └── styles.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── site │ └── hanschen │ └── pretty │ └── ExampleUnitTest.java ├── build.gradle ├── download └── Pretty_v1.0.apk ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── image ├── 1.jpg ├── 2.jpg ├── 3.jpg └── 4.jpg └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/* 5 | !/.idea/encodings.xml 6 | .DS_Store 7 | /gradle.properties 8 | /build 9 | /captures 10 | .externalNativeBuild 11 | *.apk 12 | release.keystore -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | jdk: oraclejdk8 4 | 5 | android: 6 | components: 7 | - tools 8 | - build-tools-26.0.2 9 | - android-26 10 | - extra-android-m2repository 11 | - extra-android-support 12 | 13 | licenses: 14 | - 'android-sdk-preview-license-.+' 15 | - 'android-sdk-license-.+' 16 | - 'google-gdk-license-.+' 17 | 18 | script: 19 | - "./gradlew assembleTravis --stacktrace" 20 | 21 | before_cache: 22 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 23 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 24 | 25 | cache: 26 | directories: 27 | - $HOME/.gradle/caches/ 28 | - $HOME/.gradle/wrapper/ 29 | - $HOME/.android/build-cache -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pretty-Zhihu 2 | 3 | [![Build Status](https://travis-ci.org/shensky711/Pretty-Zhihu.svg?branch=master)](https://travis-ci.org/shensky711/Pretty-Zhihu) 4 | 5 | ## 应用安装 6 | [安装地址](https://github.com/shensky711/Pretty-Zhihu/blob/master/download/Pretty_v1.0.apk) 7 | 8 | ![](https://github.com/shensky711/Pretty-Zhihu/blob/master/image/1.jpg) 9 | ![](https://github.com/shensky711/Pretty-Zhihu/blob/master/image/2.jpg) 10 | ![](https://github.com/shensky711/Pretty-Zhihu/blob/master/image/3.jpg) 11 | ![](https://github.com/shensky711/Pretty-Zhihu/blob/master/image/4.jpg) 12 | 13 | 知乎上的看图神器,你懂的。 14 | 15 | 抓取知乎某一话题下所有回答者的照片,方便各位看(美女)图。 16 | 17 | # 功能需求 18 | - [x] 话题分类 19 | - [x] 最近浏览记录 20 | - [x] 收藏 21 | - [x] 热门话题 22 | - [x] 话题管理 23 | - [x] 增加 24 | - [x] 删除 25 | - [x] 分类 26 | - [x] 图片管理 27 | - [x] 缓存照片列表 28 | - [x] 图片硬盘缓存、内存缓存 29 | - [x] 扩展功能 30 | - [x] 查看照片对应的知乎用户的资料 31 | - [x] 分享照片 32 | - [x] 保存照片 33 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'org.greenrobot.greendao' 3 | 4 | android { 5 | compileSdkVersion 26 6 | defaultConfig { 7 | applicationId "site.hanschen.pretty" 8 | minSdkVersion 15 9 | targetSdkVersion 22 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | 15 | signingConfigs { 16 | release { 17 | storeFile file('../keystore/release.keystore') 18 | storePassword getProperty('store.password', 'STORE_PASSWORD') 19 | keyAlias getProperty('store.key.alias', 'KEY_ALIAS') 20 | keyPassword getProperty('store.key.password', 'KEY_PASSWORD') 21 | } 22 | } 23 | 24 | buildTypes { 25 | release { 26 | signingConfig signingConfigs.release 27 | minifyEnabled true 28 | //Zipalign优化 29 | zipAlignEnabled true 30 | // 移除无用的resource文件 31 | shrinkResources true 32 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 33 | } 34 | 35 | travis { 36 | initWith release 37 | signingConfig signingConfigs.debug 38 | } 39 | } 40 | 41 | applicationVariants.all { variant -> 42 | variant.outputs.all { output -> 43 | outputFileName = String.format('%s_%s_%s_%s.apk', 'Pretty', getVersionName(), getDate(), variant.buildType.name) 44 | } 45 | } 46 | } 47 | 48 | def getDate() { 49 | return new Date().format('yyyyMMdd', TimeZone.getTimeZone('GMT+8')) 50 | } 51 | 52 | def getVersionName() { 53 | return 'v' + android.defaultConfig.versionName 54 | } 55 | 56 | def getProperty(String propertyName, String envName) { 57 | try { 58 | def property = project.hasProperty(propertyName) ? project.property(propertyName) : System.getenv(envName) 59 | if (property == null) { 60 | throw new NullPointerException(); 61 | } 62 | return property 63 | } catch (Throwable ignored) { 64 | def message = String.format('********************************************************************************\n' + 65 | 'You need define %s in gradle.properties or set environment variable: %s\n' + 66 | '********************************************************************************\n', propertyName, envName) 67 | throw new RuntimeException(message) 68 | } 69 | } 70 | 71 | greendao { 72 | schemaVersion 1 73 | daoPackage 'site.hanschen.pretty.db.gen' 74 | targetGenDir 'src/main/java' 75 | } 76 | 77 | dependencies { 78 | implementation fileTree(dir: 'libs', include: ['*.jar']) 79 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 80 | exclude group: 'com.android.support', module: 'support-annotations' 81 | }) 82 | testImplementation 'junit:junit:4.12' 83 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4' 84 | releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' 85 | travisImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' 86 | testImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4' 87 | 88 | implementation 'com.android.support:design:26.1.0' 89 | implementation 'com.android.support:recyclerview-v7:26.1.0' 90 | implementation 'com.android.support:support-v13:26.1.0' 91 | 92 | implementation 'site.hanschen:common:1.0.9' 93 | 94 | implementation 'com.jakewharton:butterknife:8.8.1' 95 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' 96 | 97 | implementation 'com.squareup.okhttp3:okhttp:3.9.1' 98 | implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.0' 99 | implementation 'io.reactivex.rxjava2:rxjava:2.1.8' 100 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' 101 | implementation 'com.github.bumptech.glide:glide:3.7.0' 102 | implementation 'com.github.chrisbanes:PhotoView:2.1.3' 103 | implementation 'com.pnikosis:materialish-progress:1.7' 104 | implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' 105 | implementation 'org.greenrobot:eventbus:3.1.1' 106 | implementation 'org.greenrobot:greendao:3.2.2' 107 | } 108 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # If your project uses WebView with JS, uncomment the following 2 | # and specify the fully qualified class name to the JavaScript interface 3 | # class: 4 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 5 | # public *; 6 | #} 7 | 8 | # This is a configuration file for ProGuard. 9 | # http://proguard.sourceforge.net/index.html#manual/usage.html 10 | 11 | # 不使用大小写混合类名,注意,windows用户必须为ProGuard指定该选项 12 | -dontusemixedcaseclassnames 13 | # 不忽略library里面非public修饰的类 14 | -dontskipnonpubliclibraryclasses 15 | # 把所有信息都输出,而不仅仅是输出出错信息 16 | -verbose 17 | 18 | # 不对dex进行优化和预检 19 | -dontoptimize 20 | -dontpreverify 21 | -dontshrink 22 | 23 | # 保留Annotation不混淆 24 | -keepattributes *Annotation* 25 | -keep public class com.google.vending.licensing.ILicensingService 26 | -keep public class com.android.vending.licensing.ILicensingService 27 | 28 | # 保留含有native方法的类不混淆 29 | -keepclasseswithmembernames class * { 30 | native ; 31 | } 32 | 33 | # 保留继承于View的类的get和set方法不被混淆 34 | -keepclassmembers public class * extends android.view.View { 35 | void set*(***); 36 | *** get*(); 37 | } 38 | 39 | # 保留Activity中定义的onClick事件 40 | -keepclassmembers class * extends android.app.Activity { 41 | public void *(android.view.View); 42 | } 43 | 44 | # 保留枚举的方法 45 | -keepclassmembers enum * { 46 | public static **[] values(); 47 | public static ** valueOf(java.lang.String); 48 | } 49 | 50 | # 保留Parcelable的CREATOR成员 51 | -keepclassmembers class * implements android.os.Parcelable { 52 | public static final android.os.Parcelable$Creator CREATOR; 53 | } 54 | 55 | # 保留R资源类内部类的静态成员变量不被混淆 56 | -keepclassmembers class **.R$* { 57 | public static ; 58 | } 59 | 60 | -keep class android.support.** 61 | -dontwarn android.support.** 62 | 63 | # Keep注解的支持 64 | -keep @android.support.annotation.Keep class * {*;} 65 | -keep class android.support.annotation.Keep 66 | -keepclasseswithmembers class * { 67 | @android.support.annotation.Keep ; 68 | } 69 | -keepclasseswithmembers class * { 70 | @android.support.annotation.Keep ; 71 | } 72 | -keepclasseswithmembers class * { 73 | @android.support.annotation.Keep (...); 74 | } 75 | 76 | # --------------------------------------------------------- 77 | # 保留四大组件,自定义的Application等不被混淆,实际测试中发现manifests中注册的组件会自动保留 78 | #-keep public class * extends android.app.Activity 79 | #-keep public class * extends android.app.Application 80 | #-keep public class * extends android.app.Service 81 | #-keep public class * extends android.content.BroadcastReceiver 82 | #-keep public class * extends android.content.ContentProvider 83 | #-keep public class * extends android.app.backup.BackupAgentHelper 84 | #-keep public class * extends android.preference.Preference 85 | #-keep public class * extends android.view.View 86 | 87 | -keep class org.** { *; } 88 | -dontwarn org.** 89 | 90 | -keep class io.** { *; } 91 | -dontwarn io.** 92 | 93 | -keep class javax.** { *; } 94 | -dontwarn javax.** 95 | 96 | -keep class org.greenrobot.greendao.** { *; } 97 | -dontwarn org.greenrobot.greendao.** 98 | -keepclassmembers class * extends org.greenrobot.greendao.AbstractDao { 99 | public static java.lang.String TABLENAME; 100 | } 101 | -keep class **$Properties { *; } 102 | 103 | -keep class butterknife.** { *; } 104 | -dontwarn butterknife.** 105 | 106 | -keep class com.afollestad.materialdialogs.** { *; } 107 | -dontwarn com.afollestad.materialdialogs.** 108 | 109 | -keep class com.roughike.bottombar.** { *; } 110 | -dontwarn com.roughike.bottombar.** 111 | 112 | -keep class com.squareup.** { *; } 113 | -dontwarn com.squareup.** 114 | 115 | -keep class com.tbruyelle.** { *; } 116 | -dontwarn com.tbruyelle.** 117 | 118 | -keep class com.wang.avi.** { *; } 119 | -dontwarn com.wang.avi.** 120 | 121 | -keep class com.wdullaer.materialdatetimepicker.** { *; } 122 | -dontwarn com.wdullaer.materialdatetimepicker.** 123 | 124 | -keep class de.hdodenhof.circleimageview.** { *; } 125 | -dontwarn de.hdodenhof.circleimageview.** 126 | 127 | -keep class dagger.** { *; } 128 | -dontwarn dagger.** 129 | 130 | -keep class io.reactivex.** { *; } 131 | -dontwarn io.reactivex.** 132 | 133 | -keep class me.zhanghai.android.materialprogressbar.** { *; } 134 | -dontwarn me.zhanghai.android.materialprogressbar.** 135 | 136 | -keep class org.reactivestreams.** { *; } 137 | -dontwarn org.reactivestreams.** 138 | 139 | -keep class com.amap.** { *; } 140 | -dontwarn com.amap.** 141 | 142 | -keep class com.autonavi.** { *; } 143 | -dontwarn com.autonavi.** 144 | 145 | -keep class com.bumptech.glide.** { *; } 146 | -dontwarn com.bumptech.glide.** 147 | 148 | -keep class com.google.** { *; } 149 | -dontwarn com.google.** 150 | 151 | -keep class com.maploc.** { *; } 152 | -dontwarn com.maploc.** 153 | 154 | -keep class com.nineoldandroids.** { *; } 155 | -dontwarn com.nineoldandroids.** 156 | 157 | -keep class com.rengwuxian.** { *; } 158 | -dontwarn com.rengwuxian.** 159 | 160 | -keep class com.tbruyelle.** { *; } 161 | -dontwarn com.tbruyelle.** 162 | 163 | -keep class com.vansuita.** { *; } 164 | -dontwarn com.vansuita.** 165 | 166 | -keep class okio.** { *; } 167 | -dontwarn okio.** 168 | 169 | -keep public class * extends com.google.protobuf.** { *; } 170 | 171 | -keep public class * implements com.bumptech.glide.module.GlideModule 172 | -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { 173 | **[] $VALUES; 174 | public *; 175 | } 176 | 177 | -keep class com.amulyakhare.textdrawable.** { *; } 178 | -dontwarn com.amulyakhare.textdrawable.** 179 | 180 | -keep class com.github.chrisbanes.photoview.** { *; } 181 | -dontwarn com.github.chrisbanes.photoview.** 182 | 183 | -keep class com.google.** { *; } 184 | -dontwarn com.google.** 185 | 186 | -keep class com.pnikosis.materialishprogress.** { *; } 187 | -dontwarn com.pnikosis.materialishprogress.** 188 | 189 | -keep class okhttp3.** { *; } 190 | -dontwarn okhttp3.** -------------------------------------------------------------------------------- /app/src/androidTest/java/site/hanschen/pretty/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("site.hanschen.pretty", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 23 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/ic_launcher_round-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/ic_launcher_round-web.png -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/application/PrettyApplication.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.application; 2 | 3 | import android.app.Application; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.ServiceConnection; 7 | import android.database.sqlite.SQLiteDatabase; 8 | import android.os.IBinder; 9 | 10 | import com.squareup.leakcanary.LeakCanary; 11 | 12 | import site.hanschen.pretty.db.gen.DaoMaster; 13 | import site.hanschen.pretty.db.gen.DaoSession; 14 | import site.hanschen.pretty.db.repository.PrettyRepository; 15 | import site.hanschen.pretty.db.repository.PrettyRepositoryImpl; 16 | import site.hanschen.pretty.service.TaskManager; 17 | import site.hanschen.pretty.service.TaskService; 18 | import site.hanschen.pretty.zhihu.ZhiHuApi; 19 | import site.hanschen.pretty.zhihu.ZhiHuApiApiImpl; 20 | 21 | /** 22 | * @author HansChen 23 | */ 24 | public class PrettyApplication extends Application { 25 | 26 | private static PrettyApplication sInstance; 27 | 28 | public static PrettyApplication getInstance() { 29 | return sInstance; 30 | } 31 | 32 | private PrettyRepository mPrettyRepository; 33 | private ZhiHuApi mZhiHuApi; 34 | private TaskManager mTaskManager; 35 | 36 | @Override 37 | protected void attachBaseContext(Context base) { 38 | super.attachBaseContext(base); 39 | sInstance = this; 40 | } 41 | 42 | @Override 43 | public void onCreate() { 44 | super.onCreate(); 45 | if (LeakCanary.isInAnalyzerProcess(this)) { 46 | return; 47 | } 48 | LeakCanary.install(this); 49 | bindTaskService(); 50 | } 51 | 52 | public PrettyRepository getPrettyRepository() { 53 | if (mPrettyRepository == null) { 54 | synchronized (this) { 55 | if (mPrettyRepository == null) { 56 | DaoMaster.OpenHelper helper = new DaoMaster.DevOpenHelper(this, "pretty-db", null); 57 | SQLiteDatabase db = helper.getWritableDatabase(); 58 | DaoSession daoSession = new DaoMaster(db).newSession(); 59 | mPrettyRepository = new PrettyRepositoryImpl(daoSession.getPictureDao(), daoSession.getQuestionDao()); 60 | } 61 | } 62 | } 63 | return mPrettyRepository; 64 | } 65 | 66 | public ZhiHuApi getApi() { 67 | if (mZhiHuApi == null) { 68 | synchronized (this) { 69 | if (mZhiHuApi == null) { 70 | mZhiHuApi = new ZhiHuApiApiImpl(); 71 | } 72 | } 73 | } 74 | return mZhiHuApi; 75 | } 76 | 77 | private final ServiceConnection mConn = new ServiceConnection() { 78 | @Override 79 | public void onServiceConnected(ComponentName name, IBinder service) { 80 | if (service instanceof TaskService.TaskBinder) { 81 | mTaskManager = ((TaskService.TaskBinder) service).getPrettyManager(); 82 | } 83 | } 84 | 85 | @Override 86 | public void onServiceDisconnected(ComponentName name) { 87 | mTaskManager = null; 88 | } 89 | }; 90 | 91 | private void bindTaskService() { 92 | TaskService.bind(getApplicationContext(), mConn); 93 | } 94 | 95 | private void unbindTaskService() { 96 | TaskService.unbind(getApplicationContext(), mConn); 97 | mTaskManager = null; 98 | } 99 | 100 | public TaskManager getTaskManager() { 101 | if (mTaskManager == null) { 102 | throw new IllegalStateException("mTaskManager is null now "); 103 | } 104 | return mTaskManager; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/base/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.base; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v7.app.AppCompatActivity; 6 | 7 | import com.afollestad.materialdialogs.MaterialDialog; 8 | 9 | import org.greenrobot.eventbus.EventBus; 10 | 11 | /** 12 | * @author HansChen 13 | */ 14 | public class BaseActivity extends AppCompatActivity { 15 | 16 | private MaterialDialog mWaitingDialog; 17 | 18 | @Override 19 | protected void onCreate(@Nullable Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | } 22 | 23 | @Override 24 | protected void onDestroy() { 25 | super.onDestroy(); 26 | EventBus.getDefault().unregister(this); 27 | } 28 | 29 | protected void showWaitingDialog(String title, String message) { 30 | dismissDialog(); 31 | mWaitingDialog = new MaterialDialog.Builder(this).title(title) 32 | .cancelable(false) 33 | .canceledOnTouchOutside(false) 34 | .progress(true, 0) 35 | .progressIndeterminateStyle(true) 36 | .content(message) 37 | .build(); 38 | mWaitingDialog.show(); 39 | } 40 | 41 | protected void dismissDialog() { 42 | if (mWaitingDialog != null && mWaitingDialog.isShowing()) { 43 | mWaitingDialog.dismiss(); 44 | mWaitingDialog = null; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/base/HttpClient.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.base; 2 | 3 | import java.io.IOException; 4 | import java.util.Map; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | import okhttp3.OkHttpClient; 8 | import okhttp3.Request; 9 | import okhttp3.RequestBody; 10 | import okhttp3.Response; 11 | 12 | public class HttpClient { 13 | 14 | private static final long DEFAULT_CONNECT_TIMEOUT = 5; 15 | private static final long DEFAULT_RESPONSE_TIMEOUT = 10; 16 | 17 | private final OkHttpClient mClient; 18 | 19 | public HttpClient() { 20 | this(DEFAULT_CONNECT_TIMEOUT, DEFAULT_RESPONSE_TIMEOUT, TimeUnit.SECONDS); 21 | } 22 | 23 | public HttpClient(long connectTimeout, long responseTimeout, TimeUnit unit) { 24 | OkHttpClient.Builder builder = new OkHttpClient.Builder(); 25 | builder.connectTimeout(connectTimeout, unit); 26 | builder.readTimeout(responseTimeout, unit); 27 | mClient = builder.build(); 28 | } 29 | 30 | public String httpGet(String url) throws IOException { 31 | Request request = new Request.Builder().url(url).build(); 32 | Response response = mClient.newCall(request).execute(); 33 | return response.body().string(); 34 | } 35 | 36 | public String httpPost(String url, Map header, RequestBody body) throws IOException { 37 | Request.Builder builder = new Request.Builder().url(url).post(body); 38 | for (Map.Entry entry : header.entrySet()) { 39 | builder.addHeader(entry.getKey(), entry.getValue()); 40 | } 41 | Request request = builder.build(); 42 | Response response = mClient.newCall(request).execute(); 43 | return response.body().string(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/config/PrettyGlideModule.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.config; 2 | 3 | import android.content.Context; 4 | 5 | import com.bumptech.glide.Glide; 6 | import com.bumptech.glide.GlideBuilder; 7 | import com.bumptech.glide.module.GlideModule; 8 | 9 | /** 10 | * @author HansChen 11 | */ 12 | public class PrettyGlideModule implements GlideModule { 13 | 14 | @Override 15 | public void applyOptions(Context context, GlideBuilder builder) { 16 | } 17 | 18 | @Override 19 | public void registerComponents(Context context, Glide glide) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/db/bean/Picture.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.db.bean; 2 | 3 | import org.greenrobot.greendao.annotation.Entity; 4 | import org.greenrobot.greendao.annotation.Generated; 5 | import org.greenrobot.greendao.annotation.Id; 6 | import org.greenrobot.greendao.annotation.NotNull; 7 | 8 | /** 9 | * @author HansChen 10 | */ 11 | @Entity 12 | public class Picture { 13 | 14 | @Id 15 | private Long id; 16 | 17 | private int questionId; 18 | 19 | @NotNull 20 | private String url; 21 | 22 | @Generated(hash = 1760715477) 23 | public Picture(Long id, int questionId, @NotNull String url) { 24 | this.id = id; 25 | this.questionId = questionId; 26 | this.url = url; 27 | } 28 | 29 | @Generated(hash = 1602548376) 30 | public Picture() { 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | if (this == o) { 36 | return true; 37 | } 38 | if (o == null || getClass() != o.getClass()) { 39 | return false; 40 | } 41 | 42 | Picture picture = (Picture) o; 43 | 44 | if (questionId != picture.questionId) { 45 | return false; 46 | } 47 | return url != null ? url.equals(picture.url) : picture.url == null; 48 | 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | int result = questionId; 54 | result = 31 * result + (url != null ? url.hashCode() : 0); 55 | return result; 56 | } 57 | 58 | public Long getId() { 59 | return this.id; 60 | } 61 | 62 | public void setId(Long id) { 63 | this.id = id; 64 | } 65 | 66 | public int getQuestionId() { 67 | return this.questionId; 68 | } 69 | 70 | public void setQuestionId(int questionId) { 71 | this.questionId = questionId; 72 | } 73 | 74 | public String getUrl() { 75 | return this.url; 76 | } 77 | 78 | public void setUrl(String url) { 79 | this.url = url; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/db/bean/Question.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.db.bean; 2 | 3 | import org.greenrobot.greendao.DaoException; 4 | import org.greenrobot.greendao.annotation.Entity; 5 | import org.greenrobot.greendao.annotation.Generated; 6 | import org.greenrobot.greendao.annotation.Id; 7 | import org.greenrobot.greendao.annotation.JoinProperty; 8 | import org.greenrobot.greendao.annotation.NotNull; 9 | import org.greenrobot.greendao.annotation.ToMany; 10 | import org.greenrobot.greendao.annotation.Unique; 11 | 12 | import java.util.List; 13 | 14 | import site.hanschen.pretty.db.gen.DaoSession; 15 | import site.hanschen.pretty.db.gen.PictureDao; 16 | import site.hanschen.pretty.db.gen.QuestionDao; 17 | 18 | /** 19 | * @author HansChen 20 | */ 21 | @Entity 22 | public class Question { 23 | 24 | @Id 25 | private Long id; 26 | 27 | @NotNull 28 | @Unique 29 | private int questionId; 30 | 31 | @NotNull 32 | private String title; 33 | 34 | private int answerCount; 35 | 36 | @ToMany(joinProperties = { 37 | @JoinProperty(name = "questionId", referencedName = "questionId")}) 38 | private List pictures; 39 | 40 | @Override 41 | public boolean equals(Object o) { 42 | if (this == o) { 43 | return true; 44 | } 45 | if (o == null || getClass() != o.getClass()) { 46 | return false; 47 | } 48 | 49 | Question question = (Question) o; 50 | 51 | if (questionId != question.questionId) { 52 | return false; 53 | } 54 | if (answerCount != question.answerCount) { 55 | return false; 56 | } 57 | return title != null ? title.equals(question.title) : question.title == null; 58 | 59 | } 60 | 61 | @Override 62 | public int hashCode() { 63 | int result = questionId; 64 | result = 31 * result + (title != null ? title.hashCode() : 0); 65 | result = 31 * result + answerCount; 66 | return result; 67 | } 68 | 69 | /** 70 | * Used to resolve relations 71 | */ 72 | @Generated(hash = 2040040024) 73 | private transient DaoSession daoSession; 74 | 75 | /** 76 | * Used for active entity operations. 77 | */ 78 | @Generated(hash = 891254763) 79 | private transient QuestionDao myDao; 80 | 81 | @Generated(hash = 1172449823) 82 | public Question(Long id, int questionId, @NotNull String title, int answerCount) { 83 | this.id = id; 84 | this.questionId = questionId; 85 | this.title = title; 86 | this.answerCount = answerCount; 87 | } 88 | 89 | @Generated(hash = 1868476517) 90 | public Question() { 91 | } 92 | 93 | public Long getId() { 94 | return this.id; 95 | } 96 | 97 | public void setId(Long id) { 98 | this.id = id; 99 | } 100 | 101 | public int getQuestionId() { 102 | return this.questionId; 103 | } 104 | 105 | public void setQuestionId(int questionId) { 106 | this.questionId = questionId; 107 | } 108 | 109 | public String getTitle() { 110 | return this.title; 111 | } 112 | 113 | public void setTitle(String title) { 114 | this.title = title; 115 | } 116 | 117 | /** 118 | * To-many relationship, resolved on first access (and after reset). 119 | * Changes to to-many relations are not persisted, make changes to the target entity. 120 | */ 121 | @Generated(hash = 1043365398) 122 | public List getPictures() { 123 | if (pictures == null) { 124 | final DaoSession daoSession = this.daoSession; 125 | if (daoSession == null) { 126 | throw new DaoException("Entity is detached from DAO context"); 127 | } 128 | PictureDao targetDao = daoSession.getPictureDao(); 129 | List picturesNew = targetDao._queryQuestion_Pictures(questionId); 130 | synchronized (this) { 131 | if (pictures == null) { 132 | pictures = picturesNew; 133 | } 134 | } 135 | } 136 | return pictures; 137 | } 138 | 139 | /** 140 | * Resets a to-many relationship, making the next get call to query for a fresh result. 141 | */ 142 | @Generated(hash = 1035739203) 143 | public synchronized void resetPictures() { 144 | pictures = null; 145 | } 146 | 147 | /** 148 | * Convenient call for {@link org.greenrobot.greendao.AbstractDao#delete(Object)}. 149 | * Entity must attached to an entity context. 150 | */ 151 | @Generated(hash = 128553479) 152 | public void delete() { 153 | if (myDao == null) { 154 | throw new DaoException("Entity is detached from DAO context"); 155 | } 156 | myDao.delete(this); 157 | } 158 | 159 | /** 160 | * Convenient call for {@link org.greenrobot.greendao.AbstractDao#refresh(Object)}. 161 | * Entity must attached to an entity context. 162 | */ 163 | @Generated(hash = 1942392019) 164 | public void refresh() { 165 | if (myDao == null) { 166 | throw new DaoException("Entity is detached from DAO context"); 167 | } 168 | myDao.refresh(this); 169 | } 170 | 171 | /** 172 | * Convenient call for {@link org.greenrobot.greendao.AbstractDao#update(Object)}. 173 | * Entity must attached to an entity context. 174 | */ 175 | @Generated(hash = 713229351) 176 | public void update() { 177 | if (myDao == null) { 178 | throw new DaoException("Entity is detached from DAO context"); 179 | } 180 | myDao.update(this); 181 | } 182 | 183 | /** 184 | * called by internal mechanisms, do not call yourself. 185 | */ 186 | @Generated(hash = 754833738) 187 | public void __setDaoSession(DaoSession daoSession) { 188 | this.daoSession = daoSession; 189 | myDao = daoSession != null ? daoSession.getQuestionDao() : null; 190 | } 191 | 192 | public int getAnswerCount() { 193 | return this.answerCount; 194 | } 195 | 196 | public void setAnswerCount(int answerCount) { 197 | this.answerCount = answerCount; 198 | } 199 | 200 | 201 | } 202 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/db/gen/DaoMaster.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.db.gen; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteDatabase.CursorFactory; 6 | import android.util.Log; 7 | 8 | import org.greenrobot.greendao.AbstractDaoMaster; 9 | import org.greenrobot.greendao.database.StandardDatabase; 10 | import org.greenrobot.greendao.database.Database; 11 | import org.greenrobot.greendao.database.DatabaseOpenHelper; 12 | import org.greenrobot.greendao.identityscope.IdentityScopeType; 13 | 14 | 15 | // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. 16 | /** 17 | * Master of DAO (schema version 1): knows all DAOs. 18 | */ 19 | public class DaoMaster extends AbstractDaoMaster { 20 | public static final int SCHEMA_VERSION = 1; 21 | 22 | /** Creates underlying database table using DAOs. */ 23 | public static void createAllTables(Database db, boolean ifNotExists) { 24 | PictureDao.createTable(db, ifNotExists); 25 | QuestionDao.createTable(db, ifNotExists); 26 | } 27 | 28 | /** Drops underlying database table using DAOs. */ 29 | public static void dropAllTables(Database db, boolean ifExists) { 30 | PictureDao.dropTable(db, ifExists); 31 | QuestionDao.dropTable(db, ifExists); 32 | } 33 | 34 | /** 35 | * WARNING: Drops all table on Upgrade! Use only during development. 36 | * Convenience method using a {@link DevOpenHelper}. 37 | */ 38 | public static DaoSession newDevSession(Context context, String name) { 39 | Database db = new DevOpenHelper(context, name).getWritableDb(); 40 | DaoMaster daoMaster = new DaoMaster(db); 41 | return daoMaster.newSession(); 42 | } 43 | 44 | public DaoMaster(SQLiteDatabase db) { 45 | this(new StandardDatabase(db)); 46 | } 47 | 48 | public DaoMaster(Database db) { 49 | super(db, SCHEMA_VERSION); 50 | registerDaoClass(PictureDao.class); 51 | registerDaoClass(QuestionDao.class); 52 | } 53 | 54 | public DaoSession newSession() { 55 | return new DaoSession(db, IdentityScopeType.Session, daoConfigMap); 56 | } 57 | 58 | public DaoSession newSession(IdentityScopeType type) { 59 | return new DaoSession(db, type, daoConfigMap); 60 | } 61 | 62 | /** 63 | * Calls {@link #createAllTables(Database, boolean)} in {@link #onCreate(Database)} - 64 | */ 65 | public static abstract class OpenHelper extends DatabaseOpenHelper { 66 | public OpenHelper(Context context, String name) { 67 | super(context, name, SCHEMA_VERSION); 68 | } 69 | 70 | public OpenHelper(Context context, String name, CursorFactory factory) { 71 | super(context, name, factory, SCHEMA_VERSION); 72 | } 73 | 74 | @Override 75 | public void onCreate(Database db) { 76 | Log.i("greenDAO", "Creating tables for schema version " + SCHEMA_VERSION); 77 | createAllTables(db, false); 78 | } 79 | } 80 | 81 | /** WARNING: Drops all table on Upgrade! Use only during development. */ 82 | public static class DevOpenHelper extends OpenHelper { 83 | public DevOpenHelper(Context context, String name) { 84 | super(context, name); 85 | } 86 | 87 | public DevOpenHelper(Context context, String name, CursorFactory factory) { 88 | super(context, name, factory); 89 | } 90 | 91 | @Override 92 | public void onUpgrade(Database db, int oldVersion, int newVersion) { 93 | Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables"); 94 | dropAllTables(db, true); 95 | onCreate(db); 96 | } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/db/gen/DaoSession.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.db.gen; 2 | 3 | import java.util.Map; 4 | 5 | import org.greenrobot.greendao.AbstractDao; 6 | import org.greenrobot.greendao.AbstractDaoSession; 7 | import org.greenrobot.greendao.database.Database; 8 | import org.greenrobot.greendao.identityscope.IdentityScopeType; 9 | import org.greenrobot.greendao.internal.DaoConfig; 10 | 11 | import site.hanschen.pretty.db.bean.Picture; 12 | import site.hanschen.pretty.db.bean.Question; 13 | 14 | import site.hanschen.pretty.db.gen.PictureDao; 15 | import site.hanschen.pretty.db.gen.QuestionDao; 16 | 17 | // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. 18 | 19 | /** 20 | * {@inheritDoc} 21 | * 22 | * @see org.greenrobot.greendao.AbstractDaoSession 23 | */ 24 | public class DaoSession extends AbstractDaoSession { 25 | 26 | private final DaoConfig pictureDaoConfig; 27 | private final DaoConfig questionDaoConfig; 28 | 29 | private final PictureDao pictureDao; 30 | private final QuestionDao questionDao; 31 | 32 | public DaoSession(Database db, IdentityScopeType type, Map>, DaoConfig> 33 | daoConfigMap) { 34 | super(db); 35 | 36 | pictureDaoConfig = daoConfigMap.get(PictureDao.class).clone(); 37 | pictureDaoConfig.initIdentityScope(type); 38 | 39 | questionDaoConfig = daoConfigMap.get(QuestionDao.class).clone(); 40 | questionDaoConfig.initIdentityScope(type); 41 | 42 | pictureDao = new PictureDao(pictureDaoConfig, this); 43 | questionDao = new QuestionDao(questionDaoConfig, this); 44 | 45 | registerDao(Picture.class, pictureDao); 46 | registerDao(Question.class, questionDao); 47 | } 48 | 49 | public void clear() { 50 | pictureDaoConfig.clearIdentityScope(); 51 | questionDaoConfig.clearIdentityScope(); 52 | } 53 | 54 | public PictureDao getPictureDao() { 55 | return pictureDao; 56 | } 57 | 58 | public QuestionDao getQuestionDao() { 59 | return questionDao; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/db/gen/PictureDao.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.db.gen; 2 | 3 | import java.util.List; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteStatement; 6 | 7 | import org.greenrobot.greendao.AbstractDao; 8 | import org.greenrobot.greendao.Property; 9 | import org.greenrobot.greendao.internal.DaoConfig; 10 | import org.greenrobot.greendao.database.Database; 11 | import org.greenrobot.greendao.database.DatabaseStatement; 12 | import org.greenrobot.greendao.query.Query; 13 | import org.greenrobot.greendao.query.QueryBuilder; 14 | 15 | import site.hanschen.pretty.db.bean.Picture; 16 | 17 | // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. 18 | /** 19 | * DAO for table "PICTURE". 20 | */ 21 | public class PictureDao extends AbstractDao { 22 | 23 | public static final String TABLENAME = "PICTURE"; 24 | 25 | /** 26 | * Properties of entity Picture.
27 | * Can be used for QueryBuilder and for referencing column names. 28 | */ 29 | public static class Properties { 30 | public final static Property Id = new Property(0, Long.class, "id", true, "_id"); 31 | public final static Property QuestionId = new Property(1, int.class, "questionId", false, "QUESTION_ID"); 32 | public final static Property Url = new Property(2, String.class, "url", false, "URL"); 33 | } 34 | 35 | private Query question_PicturesQuery; 36 | 37 | public PictureDao(DaoConfig config) { 38 | super(config); 39 | } 40 | 41 | public PictureDao(DaoConfig config, DaoSession daoSession) { 42 | super(config, daoSession); 43 | } 44 | 45 | /** Creates the underlying database table. */ 46 | public static void createTable(Database db, boolean ifNotExists) { 47 | String constraint = ifNotExists? "IF NOT EXISTS ": ""; 48 | db.execSQL("CREATE TABLE " + constraint + "\"PICTURE\" (" + // 49 | "\"_id\" INTEGER PRIMARY KEY ," + // 0: id 50 | "\"QUESTION_ID\" INTEGER NOT NULL ," + // 1: questionId 51 | "\"URL\" TEXT NOT NULL );"); // 2: url 52 | } 53 | 54 | /** Drops the underlying database table. */ 55 | public static void dropTable(Database db, boolean ifExists) { 56 | String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"PICTURE\""; 57 | db.execSQL(sql); 58 | } 59 | 60 | @Override 61 | protected final void bindValues(DatabaseStatement stmt, Picture entity) { 62 | stmt.clearBindings(); 63 | 64 | Long id = entity.getId(); 65 | if (id != null) { 66 | stmt.bindLong(1, id); 67 | } 68 | stmt.bindLong(2, entity.getQuestionId()); 69 | stmt.bindString(3, entity.getUrl()); 70 | } 71 | 72 | @Override 73 | protected final void bindValues(SQLiteStatement stmt, Picture entity) { 74 | stmt.clearBindings(); 75 | 76 | Long id = entity.getId(); 77 | if (id != null) { 78 | stmt.bindLong(1, id); 79 | } 80 | stmt.bindLong(2, entity.getQuestionId()); 81 | stmt.bindString(3, entity.getUrl()); 82 | } 83 | 84 | @Override 85 | public Long readKey(Cursor cursor, int offset) { 86 | return cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0); 87 | } 88 | 89 | @Override 90 | public Picture readEntity(Cursor cursor, int offset) { 91 | Picture entity = new Picture( // 92 | cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id 93 | cursor.getInt(offset + 1), // questionId 94 | cursor.getString(offset + 2) // url 95 | ); 96 | return entity; 97 | } 98 | 99 | @Override 100 | public void readEntity(Cursor cursor, Picture entity, int offset) { 101 | entity.setId(cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0)); 102 | entity.setQuestionId(cursor.getInt(offset + 1)); 103 | entity.setUrl(cursor.getString(offset + 2)); 104 | } 105 | 106 | @Override 107 | protected final Long updateKeyAfterInsert(Picture entity, long rowId) { 108 | entity.setId(rowId); 109 | return rowId; 110 | } 111 | 112 | @Override 113 | public Long getKey(Picture entity) { 114 | if(entity != null) { 115 | return entity.getId(); 116 | } else { 117 | return null; 118 | } 119 | } 120 | 121 | @Override 122 | public boolean hasKey(Picture entity) { 123 | return entity.getId() != null; 124 | } 125 | 126 | @Override 127 | protected final boolean isEntityUpdateable() { 128 | return true; 129 | } 130 | 131 | /** Internal query to resolve the "pictures" to-many relationship of Question. */ 132 | public List _queryQuestion_Pictures(int questionId) { 133 | synchronized (this) { 134 | if (question_PicturesQuery == null) { 135 | QueryBuilder queryBuilder = queryBuilder(); 136 | queryBuilder.where(Properties.QuestionId.eq(null)); 137 | question_PicturesQuery = queryBuilder.build(); 138 | } 139 | } 140 | Query query = question_PicturesQuery.forCurrentThread(); 141 | query.setParameter(0, questionId); 142 | return query.list(); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/db/gen/QuestionDao.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.db.gen; 2 | 3 | import android.database.Cursor; 4 | import android.database.sqlite.SQLiteStatement; 5 | 6 | import org.greenrobot.greendao.AbstractDao; 7 | import org.greenrobot.greendao.Property; 8 | import org.greenrobot.greendao.internal.DaoConfig; 9 | import org.greenrobot.greendao.database.Database; 10 | import org.greenrobot.greendao.database.DatabaseStatement; 11 | 12 | import site.hanschen.pretty.db.bean.Question; 13 | 14 | // THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT. 15 | /** 16 | * DAO for table "QUESTION". 17 | */ 18 | public class QuestionDao extends AbstractDao { 19 | 20 | public static final String TABLENAME = "QUESTION"; 21 | 22 | /** 23 | * Properties of entity Question.
24 | * Can be used for QueryBuilder and for referencing column names. 25 | */ 26 | public static class Properties { 27 | public final static Property Id = new Property(0, Long.class, "id", true, "_id"); 28 | public final static Property QuestionId = new Property(1, int.class, "questionId", false, "QUESTION_ID"); 29 | public final static Property Title = new Property(2, String.class, "title", false, "TITLE"); 30 | public final static Property AnswerCount = new Property(3, int.class, "answerCount", false, "ANSWER_COUNT"); 31 | } 32 | 33 | private DaoSession daoSession; 34 | 35 | 36 | public QuestionDao(DaoConfig config) { 37 | super(config); 38 | } 39 | 40 | public QuestionDao(DaoConfig config, DaoSession daoSession) { 41 | super(config, daoSession); 42 | this.daoSession = daoSession; 43 | } 44 | 45 | /** Creates the underlying database table. */ 46 | public static void createTable(Database db, boolean ifNotExists) { 47 | String constraint = ifNotExists? "IF NOT EXISTS ": ""; 48 | db.execSQL("CREATE TABLE " + constraint + "\"QUESTION\" (" + // 49 | "\"_id\" INTEGER PRIMARY KEY ," + // 0: id 50 | "\"QUESTION_ID\" INTEGER NOT NULL UNIQUE ," + // 1: questionId 51 | "\"TITLE\" TEXT NOT NULL ," + // 2: title 52 | "\"ANSWER_COUNT\" INTEGER NOT NULL );"); // 3: answerCount 53 | } 54 | 55 | /** Drops the underlying database table. */ 56 | public static void dropTable(Database db, boolean ifExists) { 57 | String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"QUESTION\""; 58 | db.execSQL(sql); 59 | } 60 | 61 | @Override 62 | protected final void bindValues(DatabaseStatement stmt, Question entity) { 63 | stmt.clearBindings(); 64 | 65 | Long id = entity.getId(); 66 | if (id != null) { 67 | stmt.bindLong(1, id); 68 | } 69 | stmt.bindLong(2, entity.getQuestionId()); 70 | stmt.bindString(3, entity.getTitle()); 71 | stmt.bindLong(4, entity.getAnswerCount()); 72 | } 73 | 74 | @Override 75 | protected final void bindValues(SQLiteStatement stmt, Question entity) { 76 | stmt.clearBindings(); 77 | 78 | Long id = entity.getId(); 79 | if (id != null) { 80 | stmt.bindLong(1, id); 81 | } 82 | stmt.bindLong(2, entity.getQuestionId()); 83 | stmt.bindString(3, entity.getTitle()); 84 | stmt.bindLong(4, entity.getAnswerCount()); 85 | } 86 | 87 | @Override 88 | protected final void attachEntity(Question entity) { 89 | super.attachEntity(entity); 90 | entity.__setDaoSession(daoSession); 91 | } 92 | 93 | @Override 94 | public Long readKey(Cursor cursor, int offset) { 95 | return cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0); 96 | } 97 | 98 | @Override 99 | public Question readEntity(Cursor cursor, int offset) { 100 | Question entity = new Question( // 101 | cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0), // id 102 | cursor.getInt(offset + 1), // questionId 103 | cursor.getString(offset + 2), // title 104 | cursor.getInt(offset + 3) // answerCount 105 | ); 106 | return entity; 107 | } 108 | 109 | @Override 110 | public void readEntity(Cursor cursor, Question entity, int offset) { 111 | entity.setId(cursor.isNull(offset + 0) ? null : cursor.getLong(offset + 0)); 112 | entity.setQuestionId(cursor.getInt(offset + 1)); 113 | entity.setTitle(cursor.getString(offset + 2)); 114 | entity.setAnswerCount(cursor.getInt(offset + 3)); 115 | } 116 | 117 | @Override 118 | protected final Long updateKeyAfterInsert(Question entity, long rowId) { 119 | entity.setId(rowId); 120 | return rowId; 121 | } 122 | 123 | @Override 124 | public Long getKey(Question entity) { 125 | if(entity != null) { 126 | return entity.getId(); 127 | } else { 128 | return null; 129 | } 130 | } 131 | 132 | @Override 133 | public boolean hasKey(Question entity) { 134 | return entity.getId() != null; 135 | } 136 | 137 | @Override 138 | protected final boolean isEntityUpdateable() { 139 | return true; 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/db/repository/PrettyRepository.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.db.repository; 2 | 3 | import android.support.annotation.Nullable; 4 | import android.support.annotation.WorkerThread; 5 | 6 | import java.util.List; 7 | 8 | import site.hanschen.pretty.db.bean.Picture; 9 | import site.hanschen.pretty.db.bean.Question; 10 | 11 | /** 12 | * @author HansChen 13 | */ 14 | @WorkerThread 15 | public interface PrettyRepository { 16 | 17 | long insertOrUpdate(Question question); 18 | 19 | @Nullable 20 | Question getQuestion(int questionId); 21 | 22 | List getAllQuestion(); 23 | 24 | void deleteQuestion(int questionId); 25 | 26 | long insertOrUpdate(Picture picture); 27 | 28 | List getPictures(int questionId); 29 | 30 | @Nullable 31 | Picture getPicture(String url, int questionId); 32 | 33 | void deletePictures(int questionId); 34 | 35 | void deletePicture(String url, int questionId); 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/db/repository/PrettyRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.db.repository; 2 | 3 | import android.support.annotation.Nullable; 4 | import android.util.Log; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import site.hanschen.pretty.db.bean.Picture; 10 | import site.hanschen.pretty.db.bean.Question; 11 | import site.hanschen.pretty.db.gen.PictureDao; 12 | import site.hanschen.pretty.db.gen.QuestionDao; 13 | 14 | /** 15 | * @author HansChen 16 | */ 17 | 18 | public class PrettyRepositoryImpl implements PrettyRepository { 19 | 20 | private PictureDao mPictureDao; 21 | private QuestionDao mQuestionDao; 22 | 23 | public PrettyRepositoryImpl(PictureDao pictureDao, QuestionDao questionDao) { 24 | mPictureDao = pictureDao; 25 | mQuestionDao = questionDao; 26 | } 27 | 28 | @Override 29 | public long insertOrUpdate(Question question) { 30 | Question old = getQuestion(question.getQuestionId()); 31 | if (old == null) { 32 | return mQuestionDao.insert(question); 33 | } else { 34 | if (!old.equals(question)) { 35 | old.setQuestionId(question.getQuestionId()); 36 | old.setTitle(question.getTitle()); 37 | old.setAnswerCount(question.getAnswerCount()); 38 | mQuestionDao.update(old); 39 | } 40 | return old.getId(); 41 | } 42 | } 43 | 44 | @Override 45 | @Nullable 46 | public Question getQuestion(int questionId) { 47 | return mQuestionDao.queryBuilder().where(QuestionDao.Properties.QuestionId.eq(questionId)).build().unique(); 48 | } 49 | 50 | @Override 51 | public List getAllQuestion() { 52 | return mQuestionDao.loadAll(); 53 | } 54 | 55 | @Override 56 | public void deleteQuestion(int questionId) { 57 | Question question = getQuestion(questionId); 58 | List pictures = getPictures(questionId); 59 | mPictureDao.deleteInTx(pictures); 60 | mQuestionDao.delete(question); 61 | } 62 | 63 | @Override 64 | public long insertOrUpdate(Picture picture) { 65 | Picture old = getPicture(picture.getUrl(), picture.getQuestionId()); 66 | if (old == null) { 67 | return mPictureDao.insert(picture); 68 | } else { 69 | if (!old.equals(picture)) { 70 | old.setQuestionId(picture.getQuestionId()); 71 | old.setUrl(picture.getUrl()); 72 | mPictureDao.update(old); 73 | } 74 | return old.getId(); 75 | } 76 | } 77 | 78 | @Override 79 | public List getPictures(int questionId) { 80 | Question question = getQuestion(questionId); 81 | return question == null ? new ArrayList() : question.getPictures(); 82 | } 83 | 84 | @Override 85 | @Nullable 86 | public Picture getPicture(String url, int questionId) { 87 | return mPictureDao.queryBuilder() 88 | .where(PictureDao.Properties.Url.eq(url), PictureDao.Properties.QuestionId.eq(questionId)) 89 | .build() 90 | .unique(); 91 | } 92 | 93 | @Override 94 | public void deletePictures(int questionId) { 95 | List pictures = getPictures(questionId); 96 | mPictureDao.deleteInTx(pictures); 97 | } 98 | 99 | @Override 100 | public void deletePicture(String url, int questionId) { 101 | Picture picture = getPicture(url, questionId); 102 | mPictureDao.delete(picture); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/eventbus/EditModeChangedEvent.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.eventbus; 2 | 3 | /** 4 | * @author HansChen 5 | */ 6 | public class EditModeChangedEvent { 7 | 8 | public boolean isEditMode; 9 | 10 | public EditModeChangedEvent(boolean isEditMode) { 11 | this.isEditMode = isEditMode; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/eventbus/NewPictureEvent.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.eventbus; 2 | 3 | import java.util.List; 4 | 5 | import site.hanschen.pretty.db.bean.Picture; 6 | 7 | /** 8 | * @author HansChen 9 | */ 10 | public class NewPictureEvent { 11 | 12 | public int questionId; 13 | public List pictures; 14 | 15 | public NewPictureEvent(int questionId, List pictures) { 16 | this.questionId = questionId; 17 | this.pictures = pictures; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/eventbus/NewQuestionEvent.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.eventbus; 2 | 3 | /** 4 | * @author HansChen 5 | */ 6 | public class NewQuestionEvent { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/eventbus/ShareFromZhihuEvent.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.eventbus; 2 | 3 | /** 4 | * @author HansChen 5 | */ 6 | public class ShareFromZhihuEvent { 7 | 8 | public String title; 9 | public String url; 10 | 11 | public ShareFromZhihuEvent(String title, String url) { 12 | this.title = title; 13 | this.url = url; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/service/Dispatcher.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.service; 2 | 3 | import android.os.Handler; 4 | import android.os.HandlerThread; 5 | import android.os.Message; 6 | import android.support.annotation.WorkerThread; 7 | import android.util.Log; 8 | import android.util.SparseArray; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Iterator; 12 | import java.util.List; 13 | import java.util.concurrent.ThreadPoolExecutor; 14 | 15 | import site.hanschen.pretty.db.bean.Question; 16 | import site.hanschen.pretty.db.repository.PrettyRepository; 17 | import site.hanschen.pretty.zhihu.ZhiHuApi; 18 | 19 | /** 20 | * @author HansChen 21 | */ 22 | public class Dispatcher { 23 | 24 | private static final int MSG_ENQUEUE_TASK = 0; 25 | private static final int MSG_DEQUEUE_TASK = 1; 26 | private static final int MSG_HUNTER_COMPLETE = 2; 27 | private static final int MSG_HUNTER_FAILED = 3; 28 | private static final int MSG_NEXT_BATCH = 4; 29 | 30 | private Handler mWorkerHandler; 31 | private Handler mMainHandler; 32 | private HandlerThread mWorkerThread; 33 | private PrettyRepository mPrettyRepository; 34 | private ZhiHuApi mApi; 35 | private ThreadPoolExecutor mExecutor; 36 | 37 | private volatile List mTaskArray; 38 | private SparseArray> mBatch; 39 | 40 | public Dispatcher(PrettyRepository repository, ZhiHuApi api, ThreadPoolExecutor executor, Handler mainHandler) { 41 | this.mPrettyRepository = repository; 42 | this.mApi = api; 43 | this.mExecutor = executor; 44 | this.mMainHandler = mainHandler; 45 | 46 | mWorkerThread = new HandlerThread("worker thread"); 47 | mWorkerThread.start(); 48 | mWorkerHandler = new Handler(mWorkerThread.getLooper(), mWorkerCallback); 49 | mTaskArray = new ArrayList<>(); 50 | mBatch = new SparseArray<>(); 51 | } 52 | 53 | void shutdown() { 54 | mWorkerHandler.removeCallbacksAndMessages(null); 55 | mWorkerThread.quit(); 56 | mTaskArray.clear(); 57 | } 58 | 59 | public boolean isFetching(int questionId) { 60 | for (UrlHunter h : mTaskArray) { 61 | if (h.getQuestionId() == questionId) { 62 | return true; 63 | } 64 | } 65 | return false; 66 | } 67 | 68 | public void dispatchAddTask(int questionId) { 69 | Message message = mWorkerHandler.obtainMessage(MSG_ENQUEUE_TASK); 70 | message.arg1 = questionId; 71 | mWorkerHandler.sendMessage(message); 72 | } 73 | 74 | public void dispatchRemoveTask(int questionId) { 75 | Message message = mWorkerHandler.obtainMessage(MSG_DEQUEUE_TASK); 76 | message.arg1 = questionId; 77 | mWorkerHandler.sendMessage(message); 78 | } 79 | 80 | public void dispatchHuntComplete(UrlHunter hunter) { 81 | Message message = mWorkerHandler.obtainMessage(MSG_HUNTER_COMPLETE); 82 | message.obj = hunter; 83 | mWorkerHandler.sendMessage(message); 84 | } 85 | 86 | public void dispatchHuntFailed(UrlHunter hunter) { 87 | Message message = mWorkerHandler.obtainMessage(MSG_HUNTER_FAILED); 88 | message.obj = hunter; 89 | mWorkerHandler.sendMessage(message); 90 | } 91 | 92 | @WorkerThread 93 | private void performAddTask(int questionId) { 94 | Question question = mPrettyRepository.getQuestion(questionId); 95 | if (question != null) { 96 | for (int offset = 0; offset < question.getAnswerCount(); offset += 10) { 97 | UrlHunter hunter = new UrlHunter(questionId, 10, offset, mApi, this); 98 | mTaskArray.add(hunter); 99 | hunter.setFuture(mExecutor.submit(hunter)); 100 | Log.d("Hans", "submit: " + hunter); 101 | } 102 | } 103 | } 104 | 105 | @WorkerThread 106 | private void performRemoveTask(int questionId) { 107 | Iterator iterator = mTaskArray.iterator(); 108 | while (iterator.hasNext()) { 109 | UrlHunter h = iterator.next(); 110 | if (h.getQuestionId() == questionId && h.cancel()) { 111 | iterator.remove(); 112 | } 113 | } 114 | } 115 | 116 | @WorkerThread 117 | private void performHuntComplete(UrlHunter hunter) { 118 | mTaskArray.remove(hunter); 119 | batch(hunter); 120 | } 121 | 122 | @WorkerThread 123 | private void batch(UrlHunter hunter) { 124 | if (hunter.isCancelled()) { 125 | return; 126 | } 127 | List urls = mBatch.get(hunter.getQuestionId()); 128 | if (urls == null) { 129 | mBatch.put(hunter.getQuestionId(), hunter.getResult()); 130 | } else { 131 | urls.addAll(hunter.getResult()); 132 | } 133 | if (!mWorkerHandler.hasMessages(MSG_NEXT_BATCH)) { 134 | mWorkerHandler.sendEmptyMessageDelayed(MSG_NEXT_BATCH, 1000); 135 | } 136 | } 137 | 138 | @WorkerThread 139 | private void performHuntFailed(UrlHunter hunter) { 140 | mTaskArray.remove(hunter); 141 | } 142 | 143 | @WorkerThread 144 | private void performBatch() { 145 | SparseArray copy = mBatch.clone(); 146 | mBatch.clear(); 147 | mMainHandler.sendMessage(mMainHandler.obtainMessage(TaskService.MSG_HUNT_COMPLETE, copy)); 148 | } 149 | 150 | @SuppressWarnings("FieldCanBeLocal") 151 | private Handler.Callback mWorkerCallback = new Handler.Callback() { 152 | 153 | @Override 154 | public boolean handleMessage(Message msg) { 155 | switch (msg.what) { 156 | case MSG_ENQUEUE_TASK: 157 | performAddTask(msg.arg1); 158 | break; 159 | case MSG_DEQUEUE_TASK: 160 | performRemoveTask(msg.arg1); 161 | break; 162 | case MSG_HUNTER_COMPLETE: 163 | performHuntComplete((UrlHunter) msg.obj); 164 | break; 165 | case MSG_HUNTER_FAILED: 166 | performHuntFailed((UrlHunter) msg.obj); 167 | break; 168 | case MSG_NEXT_BATCH: 169 | performBatch(); 170 | break; 171 | } 172 | return true; 173 | } 174 | }; 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/service/TaskManager.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.service; 2 | 3 | /** 4 | * @author HansChen 5 | */ 6 | public interface TaskManager { 7 | 8 | boolean isFetching(final int questionId); 9 | 10 | void startFetchPicture(final int questionId); 11 | 12 | void stopFetchPicture(final int questionId); 13 | 14 | void registerObserver(TaskObserver observer); 15 | 16 | void unregisterObserver(TaskObserver observer); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/service/TaskObservable.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.service; 2 | 3 | 4 | import android.database.Observable; 5 | 6 | import java.util.List; 7 | 8 | import site.hanschen.pretty.db.bean.Picture; 9 | 10 | /** 11 | * @author HansChen 12 | */ 13 | class TaskObservable extends Observable { 14 | 15 | public void notifyFetchStart(final int questionId) { 16 | synchronized (mObservers) { 17 | for (int i = mObservers.size() - 1; i >= 0; i--) { 18 | mObservers.get(i).onFetchStart(questionId); 19 | } 20 | } 21 | } 22 | 23 | public void notifyFetchProgress(final int questionId, final int progress) { 24 | synchronized (mObservers) { 25 | for (int i = mObservers.size() - 1; i >= 0; i--) { 26 | mObservers.get(i).onFetchProgress(questionId, progress); 27 | } 28 | } 29 | } 30 | 31 | public void notifyFetch(final int questionId, final List pictures) { 32 | synchronized (mObservers) { 33 | for (int i = mObservers.size() - 1; i >= 0; i--) { 34 | mObservers.get(i).onFetch(questionId, pictures); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/service/TaskObserver.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.service; 2 | 3 | import java.util.List; 4 | 5 | import site.hanschen.pretty.db.bean.Picture; 6 | 7 | /** 8 | * @author HansChen 9 | */ 10 | public interface TaskObserver { 11 | 12 | void onFetchStart(final int questionId); 13 | 14 | void onFetchProgress(final int questionId, final int progress); 15 | 16 | void onFetch(final int questionId, final List pictures); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/service/TaskService.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.service; 2 | 3 | import android.app.Service; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.os.Binder; 8 | import android.os.Handler; 9 | import android.os.HandlerThread; 10 | import android.os.IBinder; 11 | import android.os.Message; 12 | import android.util.SparseArray; 13 | 14 | import org.greenrobot.eventbus.EventBus; 15 | 16 | import java.util.ArrayList; 17 | import java.util.List; 18 | import java.util.concurrent.LinkedBlockingQueue; 19 | import java.util.concurrent.ThreadPoolExecutor; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | import site.hanschen.pretty.application.PrettyApplication; 23 | import site.hanschen.pretty.db.bean.Picture; 24 | import site.hanschen.pretty.db.repository.PrettyRepository; 25 | import site.hanschen.pretty.eventbus.NewPictureEvent; 26 | import site.hanschen.pretty.zhihu.ZhiHuApi; 27 | 28 | /** 29 | * @author HansChen 30 | */ 31 | public class TaskService extends Service implements TaskManager { 32 | 33 | public static void bind(Context context, ServiceConnection conn) { 34 | Intent intent = new Intent(context, TaskService.class); 35 | context.bindService(intent, conn, BIND_AUTO_CREATE); 36 | } 37 | 38 | public static void unbind(Context context, ServiceConnection conn) { 39 | context.unbindService(conn); 40 | } 41 | 42 | public static final int MSG_HUNT_COMPLETE = 0; 43 | 44 | private Context mContext; 45 | private PrettyRepository mPrettyRepository; 46 | private ZhiHuApi mApi; 47 | private ThreadPoolExecutor mExecutor; 48 | private Dispatcher mDispatcher; 49 | private Handler mMainHandler; 50 | private TaskObservable mTaskObservable; 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | mContext = this; 56 | mTaskObservable = new TaskObservable(); 57 | mPrettyRepository = PrettyApplication.getInstance().getPrettyRepository(); 58 | mApi = PrettyApplication.getInstance().getApi(); 59 | mExecutor = new ThreadPoolExecutor(getThreadPoolSize(), 60 | getThreadPoolSize(), 61 | 60, 62 | TimeUnit.MINUTES, 63 | new LinkedBlockingQueue()); 64 | HandlerThread handlerThread = new HandlerThread("work"); 65 | handlerThread.start(); 66 | mMainHandler = new Handler(handlerThread.getLooper(), mMainCallback); 67 | mDispatcher = new Dispatcher(mPrettyRepository, mApi, mExecutor, mMainHandler); 68 | } 69 | 70 | @Override 71 | public void onDestroy() { 72 | super.onDestroy(); 73 | } 74 | 75 | @Override 76 | public int onStartCommand(Intent intent, int flags, int startId) { 77 | return START_STICKY; 78 | } 79 | 80 | @Override 81 | public IBinder onBind(Intent intent) { 82 | return new TaskBinder(); 83 | } 84 | 85 | @Override 86 | public boolean isFetching(final int questionId) { 87 | return mDispatcher.isFetching(questionId); 88 | } 89 | 90 | @Override 91 | public void startFetchPicture(final int questionId) { 92 | mDispatcher.dispatchAddTask(questionId); 93 | } 94 | 95 | @Override 96 | public void stopFetchPicture(int questionId) { 97 | mDispatcher.dispatchRemoveTask(questionId); 98 | } 99 | 100 | @Override 101 | public void registerObserver(TaskObserver observer) { 102 | mTaskObservable.registerObserver(observer); 103 | } 104 | 105 | @Override 106 | public void unregisterObserver(TaskObserver observer) { 107 | mTaskObservable.unregisterObserver(observer); 108 | } 109 | 110 | private Handler.Callback mMainCallback = new Handler.Callback() { 111 | @Override 112 | public boolean handleMessage(Message msg) { 113 | switch (msg.what) { 114 | case MSG_HUNT_COMPLETE: 115 | @SuppressWarnings("unchecked") SparseArray> urls = (SparseArray>) msg.obj; 116 | for (int i = 0; i < urls.size(); i++) { 117 | List pictures = new ArrayList<>(); 118 | int questionId = urls.keyAt(i); 119 | for (String url : urls.valueAt(i)) { 120 | if (mPrettyRepository.getPicture(url, questionId) == null) { 121 | Picture picture = new Picture(null, questionId, url); 122 | mPrettyRepository.insertOrUpdate(picture); 123 | pictures.add(picture); 124 | } 125 | } 126 | if (pictures.size() > 0) { 127 | EventBus.getDefault().post(new NewPictureEvent(questionId, pictures)); 128 | mTaskObservable.notifyFetch(questionId, pictures); 129 | } 130 | } 131 | break; 132 | } 133 | return false; 134 | } 135 | }; 136 | 137 | public class TaskBinder extends Binder { 138 | 139 | public TaskManager getPrettyManager() { 140 | return TaskService.this; 141 | } 142 | } 143 | 144 | private int getThreadPoolSize() { 145 | int threadPoolSize = 2 * Runtime.getRuntime().availableProcessors() + 1; 146 | return threadPoolSize > 8 ? 8 : threadPoolSize; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/service/UrlHunter.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.service; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.Future; 9 | 10 | import site.hanschen.pretty.zhihu.ZhiHuApi; 11 | import site.hanschen.pretty.zhihu.bean.AnswerList; 12 | 13 | /** 14 | * @author HansChen 15 | */ 16 | public class UrlHunter implements Runnable { 17 | 18 | private int mQuestionId; 19 | private int mPageSize; 20 | private int mOffset; 21 | private ZhiHuApi mApi; 22 | private Dispatcher mDispatcher; 23 | private Future mFuture; 24 | private List mResult; 25 | 26 | public UrlHunter(int questionId, int pageSize, int offset, ZhiHuApi api, Dispatcher dispatcher) { 27 | this.mQuestionId = questionId; 28 | this.mPageSize = pageSize; 29 | this.mOffset = offset; 30 | this.mApi = api; 31 | this.mDispatcher = dispatcher; 32 | } 33 | 34 | public Future getFuture() { 35 | return mFuture; 36 | } 37 | 38 | public void setFuture(Future future) { 39 | this.mFuture = future; 40 | } 41 | 42 | public boolean cancel() { 43 | Log.d("Hans", "cancel: " + toString()); 44 | return mFuture != null && mFuture.cancel(false); 45 | } 46 | 47 | public boolean isCancelled() { 48 | return mFuture != null && mFuture.isCancelled(); 49 | } 50 | 51 | public int getQuestionId() { 52 | return mQuestionId; 53 | } 54 | 55 | public List getResult() { 56 | return mResult; 57 | } 58 | 59 | public List hunt() throws IOException { 60 | List urls = new ArrayList<>(); 61 | AnswerList answerList = mApi.getAnswerList(mQuestionId, mPageSize, mOffset); 62 | for (String answer : answerList.msg) { 63 | urls.addAll(mApi.parsePictureList(answer)); 64 | } 65 | return urls; 66 | } 67 | 68 | @Override 69 | public void run() { 70 | try { 71 | mResult = hunt(); 72 | mDispatcher.dispatchHuntComplete(this); 73 | } catch (IOException e) { 74 | mDispatcher.dispatchHuntFailed(this); 75 | } 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return "UrlHunter{" + "mQuestionId=" + mQuestionId + ", mPageSize=" + mPageSize + ", mOffset=" + mOffset + '}'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/picture/GalleryActivity.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.picture; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v4.view.PagerAdapter; 7 | import android.support.v4.view.ViewPager; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.widget.TextView; 10 | 11 | import org.greenrobot.eventbus.EventBus; 12 | import org.greenrobot.eventbus.Subscribe; 13 | import org.greenrobot.eventbus.ThreadMode; 14 | 15 | import java.util.List; 16 | import java.util.Locale; 17 | 18 | import butterknife.BindView; 19 | import butterknife.ButterKnife; 20 | import io.reactivex.Observable; 21 | import io.reactivex.ObservableEmitter; 22 | import io.reactivex.ObservableOnSubscribe; 23 | import io.reactivex.Observer; 24 | import io.reactivex.android.schedulers.AndroidSchedulers; 25 | import io.reactivex.disposables.Disposable; 26 | import io.reactivex.schedulers.Schedulers; 27 | import site.hanschen.pretty.R; 28 | import site.hanschen.pretty.application.PrettyApplication; 29 | import site.hanschen.pretty.db.bean.Picture; 30 | import site.hanschen.pretty.db.repository.PrettyRepository; 31 | import site.hanschen.pretty.eventbus.NewPictureEvent; 32 | import site.hanschen.pretty.widget.DepthPageTransformer; 33 | import site.hanschen.pretty.widget.ViewPagerCatchException; 34 | 35 | /** 36 | * @author HansChen 37 | */ 38 | public class GalleryActivity extends AppCompatActivity { 39 | 40 | private static final String KEY_SELECT = "KEY_SELECT"; 41 | private static final String KEY_QUESTION_ID = "KEY_QUESTION_ID"; 42 | 43 | @BindView(R.id.gallery_content) 44 | ViewPagerCatchException mPager; 45 | 46 | @BindView(R.id.gallery_indicate) 47 | TextView mIndicate; 48 | 49 | private List mPictures; 50 | private GalleryPagerAdapter mPagerAdapter; 51 | private int mSelected; 52 | private int mQuestionId; 53 | private PrettyRepository mPrettyRepository; 54 | 55 | 56 | public static void open(Context context, int questionId, int selected) { 57 | Intent intent = new Intent(context, GalleryActivity.class); 58 | intent.putExtra(KEY_SELECT, selected); 59 | intent.putExtra(KEY_QUESTION_ID, questionId); 60 | context.startActivity(intent); 61 | } 62 | 63 | @Override 64 | protected void onCreate(Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | setContentView(R.layout.activity_gallery); 67 | ButterKnife.bind(this); 68 | mPrettyRepository = PrettyApplication.getInstance().getPrettyRepository(); 69 | parseData(); 70 | initViews(); 71 | initData(); 72 | EventBus.getDefault().register(this); 73 | } 74 | 75 | @Override 76 | protected void onDestroy() { 77 | super.onDestroy(); 78 | EventBus.getDefault().unregister(this); 79 | } 80 | 81 | private void parseData() { 82 | Bundle bundle = getIntent().getExtras(); 83 | if (bundle == null || (mQuestionId = bundle.getInt(KEY_QUESTION_ID)) == 0) { 84 | throw new IllegalArgumentException("bundle must contain QuestionId"); 85 | } 86 | mSelected = bundle.getInt(KEY_SELECT); 87 | } 88 | 89 | 90 | private void initViews() { 91 | 92 | mPagerAdapter = new GalleryPagerAdapter(this) { 93 | @Override 94 | public int getItemPosition(Object object) { 95 | return PagerAdapter.POSITION_NONE; 96 | } 97 | }; 98 | mPager.setAdapter(mPagerAdapter); 99 | mPager.setPageTransformer(true, new DepthPageTransformer()); 100 | mPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { 101 | @Override 102 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 103 | 104 | } 105 | 106 | @Override 107 | public void onPageSelected(int position) { 108 | mSelected = position; 109 | updateIndicate(); 110 | } 111 | 112 | @Override 113 | public void onPageScrollStateChanged(int state) { 114 | 115 | } 116 | }); 117 | } 118 | 119 | private void initData() { 120 | Observable.create(new ObservableOnSubscribe>() { 121 | @Override 122 | public void subscribe(ObservableEmitter> e) throws Exception { 123 | e.onNext(mPrettyRepository.getPictures(mQuestionId)); 124 | e.onComplete(); 125 | } 126 | }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer>() { 127 | @Override 128 | public void onSubscribe(Disposable d) { 129 | 130 | } 131 | 132 | @Override 133 | public void onNext(List pictures) { 134 | mPictures = pictures; 135 | mPagerAdapter.setData(mPictures); 136 | mPager.setCurrentItem(mSelected); 137 | updateIndicate(); 138 | } 139 | 140 | @Override 141 | public void onError(Throwable e) { 142 | 143 | } 144 | 145 | @Override 146 | public void onComplete() { 147 | 148 | } 149 | }); 150 | } 151 | 152 | private void updateIndicate() { 153 | mIndicate.setText(String.format(Locale.getDefault(), "%d/%d", mSelected, mPictures.size())); 154 | } 155 | 156 | @Subscribe(threadMode = ThreadMode.MAIN) 157 | public void onMessageEvent(NewPictureEvent event) { 158 | if (event.questionId != mQuestionId || event.pictures.size() <= 0) { 159 | return; 160 | } 161 | mPagerAdapter.notifyDataSetChanged(); 162 | updateIndicate(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/picture/GalleryPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.picture; 2 | 3 | import android.content.Context; 4 | import android.os.Parcelable; 5 | import android.support.v4.view.PagerAdapter; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import com.bumptech.glide.Glide; 11 | import com.bumptech.glide.load.engine.DiskCacheStrategy; 12 | import com.bumptech.glide.load.resource.drawable.GlideDrawable; 13 | import com.bumptech.glide.request.RequestListener; 14 | import com.bumptech.glide.request.target.Target; 15 | import com.github.chrisbanes.photoview.PhotoView; 16 | import com.pnikosis.materialishprogress.ProgressWheel; 17 | 18 | import java.util.List; 19 | 20 | import site.hanschen.pretty.R; 21 | import site.hanschen.pretty.db.bean.Picture; 22 | 23 | 24 | /** 25 | * @author HansChen 26 | */ 27 | public class GalleryPagerAdapter extends PagerAdapter { 28 | 29 | private List mData; 30 | private LayoutInflater mInflater; 31 | private Context mContext; 32 | 33 | public GalleryPagerAdapter(Context context) { 34 | mContext = context; 35 | mInflater = LayoutInflater.from(context); 36 | } 37 | 38 | public void setData(List data) { 39 | mData = data; 40 | notifyDataSetChanged(); 41 | } 42 | 43 | @Override 44 | public void destroyItem(ViewGroup container, int position, Object object) { 45 | container.removeView((View) object); 46 | } 47 | 48 | @Override 49 | public int getCount() { 50 | return mData == null ? 0 : mData.size(); 51 | } 52 | 53 | @Override 54 | public Object instantiateItem(ViewGroup view, int position) { 55 | View layout = mInflater.inflate(R.layout.item_gallery_pager, view, false); 56 | assert layout != null; 57 | PhotoView imageView = (PhotoView) layout.findViewById(R.id.gallery_pager_image); 58 | final ProgressWheel progress = (ProgressWheel) layout.findViewById(R.id.gallery_pager_progress); 59 | 60 | Picture picture = mData.get(position); 61 | progress.setVisibility(View.VISIBLE); 62 | Glide.with(mContext) 63 | .load(picture.getUrl()) 64 | .fitCenter() 65 | .crossFade() 66 | .diskCacheStrategy(DiskCacheStrategy.ALL) 67 | .listener(new RequestListener() { 68 | @Override 69 | public boolean onException(Exception e, 70 | String model, 71 | Target target, 72 | boolean isFirstResource) { 73 | progress.setVisibility(View.GONE); 74 | return false; 75 | } 76 | 77 | @Override 78 | public boolean onResourceReady(GlideDrawable resource, 79 | String model, 80 | Target target, 81 | boolean isFromMemoryCache, 82 | boolean isFirstResource) { 83 | progress.setVisibility(View.GONE); 84 | return false; 85 | } 86 | }) 87 | .into(imageView); 88 | view.addView(layout, 0); 89 | return layout; 90 | } 91 | 92 | @Override 93 | public boolean isViewFromObject(View view, Object object) { 94 | return view.equals(object); 95 | } 96 | 97 | @Override 98 | public void restoreState(Parcelable state, ClassLoader loader) { 99 | } 100 | 101 | @Override 102 | public Parcelable saveState() { 103 | return null; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/picture/PictureAdapter.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.picture; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.ColorDrawable; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | 11 | import com.bumptech.glide.Glide; 12 | import com.bumptech.glide.load.engine.DiskCacheStrategy; 13 | 14 | import java.util.List; 15 | 16 | import butterknife.BindView; 17 | import butterknife.ButterKnife; 18 | import site.hanschen.pretty.R; 19 | import site.hanschen.pretty.db.bean.Picture; 20 | import site.hanschen.pretty.utils.ColorUtils; 21 | import site.hanschen.pretty.utils.CommonUtils; 22 | 23 | /** 24 | * @author HansChen 25 | */ 26 | public class PictureAdapter extends RecyclerView.Adapter implements View.OnClickListener { 27 | 28 | private Context mContext; 29 | private List mPictures; 30 | private int mPhotoSize; 31 | private OnItemClickListener mOnItemClickListener; 32 | 33 | public PictureAdapter(Context context, int photoSize) { 34 | this.mContext = context; 35 | this.mPhotoSize = photoSize; 36 | } 37 | 38 | public PictureAdapter(Context context, List pictures, int photoSize) { 39 | this.mContext = context; 40 | this.mPictures = pictures; 41 | this.mPhotoSize = photoSize; 42 | } 43 | 44 | public void setItemClickListener(OnItemClickListener onItemClickListener) { 45 | this.mOnItemClickListener = onItemClickListener; 46 | } 47 | 48 | public void setData(List pictures) { 49 | this.mPictures = pictures; 50 | notifyDataSetChanged(); 51 | } 52 | 53 | @Override 54 | public void onClick(View v) { 55 | if (mOnItemClickListener != null) { 56 | Picture picture = mPictures.get((Integer) v.getTag()); 57 | mOnItemClickListener.onItemClick((Integer) v.getTag(), picture); 58 | } 59 | } 60 | 61 | @Override 62 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 63 | View view = LayoutInflater.from(mContext).inflate(R.layout.item_picture, parent, false); 64 | ViewGroup.LayoutParams params = view.getLayoutParams(); 65 | params.width = mPhotoSize; 66 | params.height = mPhotoSize; 67 | view.setOnClickListener(this); 68 | return new ViewHolder(view); 69 | } 70 | 71 | @Override 72 | public void onBindViewHolder(ViewHolder holder, int position) { 73 | holder.itemView.setTag(position); 74 | 75 | Picture picture = mPictures.get(position); 76 | Glide.with(mContext) 77 | .load(CommonUtils.getSmallPicture(picture.getUrl())) 78 | .centerCrop() 79 | .placeholder(new ColorDrawable(ColorUtils.getColor(picture))) 80 | .crossFade() 81 | .diskCacheStrategy(DiskCacheStrategy.ALL) 82 | .into(holder.picture); 83 | } 84 | 85 | @Override 86 | public int getItemCount() { 87 | return mPictures == null ? 0 : mPictures.size(); 88 | } 89 | 90 | static class ViewHolder extends RecyclerView.ViewHolder { 91 | 92 | @BindView(R.id.item_picture) 93 | ImageView picture; 94 | 95 | ViewHolder(View view) { 96 | super(view); 97 | ButterKnife.bind(this, view); 98 | } 99 | } 100 | 101 | public interface OnItemClickListener { 102 | 103 | void onItemClick(int position, Picture picture); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/picture/PictureListActivity.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.picture; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.graphics.Rect; 6 | import android.os.Bundle; 7 | import android.support.annotation.NonNull; 8 | import android.support.design.widget.FloatingActionButton; 9 | import android.support.v7.widget.GridLayoutManager; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.support.v7.widget.Toolbar; 12 | import android.view.View; 13 | import android.widget.ProgressBar; 14 | import android.widget.Toast; 15 | 16 | import com.afollestad.materialdialogs.DialogAction; 17 | import com.afollestad.materialdialogs.MaterialDialog; 18 | 19 | import org.greenrobot.eventbus.EventBus; 20 | import org.greenrobot.eventbus.Subscribe; 21 | import org.greenrobot.eventbus.ThreadMode; 22 | 23 | import java.util.List; 24 | 25 | import butterknife.BindView; 26 | import butterknife.ButterKnife; 27 | import butterknife.OnClick; 28 | import io.reactivex.Observable; 29 | import io.reactivex.ObservableEmitter; 30 | import io.reactivex.ObservableOnSubscribe; 31 | import io.reactivex.Observer; 32 | import io.reactivex.android.schedulers.AndroidSchedulers; 33 | import io.reactivex.disposables.Disposable; 34 | import io.reactivex.schedulers.Schedulers; 35 | import site.hanschen.pretty.R; 36 | import site.hanschen.pretty.application.PrettyApplication; 37 | import site.hanschen.pretty.base.BaseActivity; 38 | import site.hanschen.pretty.db.bean.Picture; 39 | import site.hanschen.pretty.db.repository.PrettyRepository; 40 | import site.hanschen.pretty.eventbus.NewPictureEvent; 41 | import site.hanschen.pretty.service.TaskManager; 42 | 43 | /** 44 | * @author HansChen 45 | */ 46 | public class PictureListActivity extends BaseActivity { 47 | 48 | private static final String KEY_QUESTION_ID = "KEY_QUESTION_ID"; 49 | private static final String KEY_TITLE = "KEY_TITLE"; 50 | 51 | public static void open(Context context, int questionId, String title) { 52 | Intent intent = new Intent(context, PictureListActivity.class); 53 | intent.putExtra(KEY_QUESTION_ID, questionId); 54 | intent.putExtra(KEY_TITLE, title); 55 | context.startActivity(intent); 56 | } 57 | 58 | @BindView(R.id.picture_list_toolbar) 59 | Toolbar mToolbar; 60 | 61 | @BindView(R.id.picture_list_pictures) 62 | RecyclerView mPictureView; 63 | 64 | @BindView(R.id.picture_list_refresh) 65 | FloatingActionButton mFabBtn; 66 | 67 | @BindView(R.id.picture_list_progress) 68 | ProgressBar mProgressBar; 69 | 70 | private PictureAdapter mAdapter; 71 | private List mPictures; 72 | private PrettyRepository mPrettyRepository; 73 | private TaskManager mTaskManager; 74 | private int mQuestionId; 75 | private String mTitle; 76 | 77 | @Override 78 | protected void onCreate(Bundle savedInstanceState) { 79 | super.onCreate(savedInstanceState); 80 | setContentView(R.layout.activity_picture_list); 81 | ButterKnife.bind(PictureListActivity.this); 82 | EventBus.getDefault().register(this); 83 | mPrettyRepository = PrettyApplication.getInstance().getPrettyRepository(); 84 | mTaskManager = PrettyApplication.getInstance().getTaskManager(); 85 | parseData(); 86 | initViews(); 87 | initData(); 88 | } 89 | 90 | @Override 91 | protected void onDestroy() { 92 | super.onDestroy(); 93 | EventBus.getDefault().unregister(this); 94 | } 95 | 96 | private void parseData() { 97 | Bundle bundle = getIntent().getExtras(); 98 | if (bundle == null 99 | || (mQuestionId = bundle.getInt(KEY_QUESTION_ID)) == 0 100 | || (mTitle = bundle.getString(KEY_TITLE)) == null) { 101 | throw new IllegalArgumentException("bundle must contain QuestionId"); 102 | } 103 | } 104 | 105 | private int getPhotoSize(int column) { 106 | int margin = getResources().getDimensionPixelOffset(R.dimen.grid_margin); 107 | return getResources().getDisplayMetrics().widthPixels / column - 2 * margin; 108 | } 109 | 110 | private void initViews() { 111 | mToolbar.setTitle(mTitle); 112 | setSupportActionBar(mToolbar); 113 | mToolbar.setNavigationIcon(R.drawable.ic_close_black_24dp); 114 | mToolbar.setNavigationOnClickListener(new View.OnClickListener() { 115 | @Override 116 | public void onClick(View v) { 117 | onBackPressed(); 118 | } 119 | }); 120 | 121 | mPictureView.setLayoutManager(new GridLayoutManager(this, 3)); 122 | mPictureView.addItemDecoration(new RecyclerView.ItemDecoration() { 123 | @Override 124 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 125 | int margin = getResources().getDimensionPixelOffset(R.dimen.grid_margin); 126 | outRect.set(margin, margin, margin, margin); 127 | } 128 | }); 129 | mAdapter = new PictureAdapter(this, getPhotoSize(3)); 130 | mAdapter.setItemClickListener(mOnItemClickListener); 131 | mPictureView.setAdapter(mAdapter); 132 | 133 | if (mTaskManager.isFetching(mQuestionId)) { 134 | displayFetchingState(); 135 | } else { 136 | displayNoFetchingState(); 137 | } 138 | } 139 | 140 | private PictureAdapter.OnItemClickListener mOnItemClickListener = new PictureAdapter.OnItemClickListener() { 141 | @Override 142 | public void onItemClick(int position, Picture picture) { 143 | GalleryActivity.open(PictureListActivity.this, mQuestionId, position); 144 | } 145 | }; 146 | 147 | private void initData() { 148 | Observable.create(new ObservableOnSubscribe>() { 149 | @Override 150 | public void subscribe(ObservableEmitter> e) throws Exception { 151 | e.onNext(mPrettyRepository.getPictures(mQuestionId)); 152 | e.onComplete(); 153 | } 154 | }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer>() { 155 | @Override 156 | public void onSubscribe(Disposable d) { 157 | 158 | } 159 | 160 | @Override 161 | public void onNext(List pictures) { 162 | mPictures = pictures; 163 | mAdapter.setData(mPictures); 164 | if (mPictures.size() <= 0) { 165 | showFetchDialog(); 166 | } 167 | } 168 | 169 | @Override 170 | public void onError(Throwable e) { 171 | 172 | } 173 | 174 | @Override 175 | public void onComplete() { 176 | 177 | } 178 | }); 179 | } 180 | 181 | private void showFetchDialog() { 182 | new MaterialDialog.Builder(this).title("抓取图片") 183 | .content("抓取该话题下所有图片?请尽量使用Wi-Fi,土豪随意") 184 | .positiveText("抓取") 185 | .onPositive(new MaterialDialog.SingleButtonCallback() { 186 | @Override 187 | public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { 188 | mTaskManager.startFetchPicture(mQuestionId); 189 | displayFetchingState(); 190 | } 191 | }) 192 | .negativeText("取消") 193 | .build() 194 | .show(); 195 | } 196 | 197 | @Subscribe(threadMode = ThreadMode.MAIN) 198 | public void onMessageEvent(NewPictureEvent event) { 199 | if (event.questionId != mQuestionId || event.pictures.size() <= 0) { 200 | return; 201 | } 202 | mAdapter.notifyDataSetChanged(); 203 | } 204 | 205 | @OnClick(R.id.picture_list_refresh) 206 | void onFabClick() { 207 | if (mTaskManager.isFetching(mQuestionId)) { 208 | mTaskManager.stopFetchPicture(mQuestionId); 209 | displayNoFetchingState(); 210 | Toast.makeText(getApplicationContext(), "已停止抓取图片", Toast.LENGTH_SHORT).show(); 211 | } else { 212 | showFetchDialog(); 213 | } 214 | } 215 | 216 | private void displayFetchingState() { 217 | mProgressBar.setVisibility(View.VISIBLE); 218 | mFabBtn.setImageResource(R.drawable.ic_close_black_24dp); 219 | } 220 | 221 | private void displayNoFetchingState() { 222 | mProgressBar.setVisibility(View.GONE); 223 | mFabBtn.setImageResource(R.drawable.ic_refresh_black_24dp); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/question/QuestionActivity.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.question; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | import android.support.design.widget.FloatingActionButton; 7 | import android.support.design.widget.TabLayout; 8 | import android.support.v4.view.ViewPager; 9 | import android.support.v7.widget.Toolbar; 10 | import android.text.TextUtils; 11 | import android.view.View; 12 | 13 | import org.greenrobot.eventbus.EventBus; 14 | import org.greenrobot.eventbus.Subscribe; 15 | import org.greenrobot.eventbus.ThreadMode; 16 | 17 | import java.util.Arrays; 18 | 19 | import butterknife.BindView; 20 | import butterknife.ButterKnife; 21 | import butterknife.OnClick; 22 | import site.hanschen.pretty.R; 23 | import site.hanschen.pretty.base.BaseActivity; 24 | import site.hanschen.pretty.eventbus.EditModeChangedEvent; 25 | import site.hanschen.pretty.eventbus.NewQuestionEvent; 26 | import site.hanschen.pretty.eventbus.ShareFromZhihuEvent; 27 | import site.hanschen.pretty.utils.CommonUtils; 28 | import site.hanschen.pretty.widget.BackHandlerHelper; 29 | import site.hanschen.pretty.widget.ScrollViewPager; 30 | 31 | /** 32 | * @author HansChen 33 | */ 34 | public class QuestionActivity extends BaseActivity { 35 | 36 | @BindView(R.id.question_tab_layout) 37 | TabLayout mTabLayout; 38 | @BindView(R.id.question_pager) 39 | ScrollViewPager mViewPager; 40 | @BindView(R.id.question_add) 41 | FloatingActionButton mFabBtn; 42 | 43 | @Override 44 | protected void onCreate(@Nullable Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.activity_question); 47 | ButterKnife.bind(this); 48 | EventBus.getDefault().register(this); 49 | initViews(); 50 | getExtras(getIntent()); 51 | } 52 | 53 | @Override 54 | protected void onNewIntent(Intent intent) { 55 | super.onNewIntent(intent); 56 | getExtras(intent); 57 | } 58 | 59 | private void getExtras(Intent intent) { 60 | Bundle extras = intent.getExtras(); 61 | if (extras != null) { 62 | String shareText = extras.getString(Intent.EXTRA_TEXT); 63 | String title = CommonUtils.getTitleFromShare(shareText); 64 | String url = CommonUtils.getUrlFromShare(shareText); 65 | if (TextUtils.isEmpty(title) || TextUtils.isEmpty(url)) { 66 | return; 67 | } 68 | 69 | EventBus.getDefault().post(new ShareFromZhihuEvent(title, url)); 70 | } 71 | } 72 | 73 | private void initViews() { 74 | Toolbar toolbar = (Toolbar) findViewById(R.id.question_toolbar); 75 | toolbar.setTitle(R.string.app_name); 76 | setSupportActionBar(toolbar); 77 | 78 | mViewPager.setOffscreenPageLimit(2); 79 | QuestionCategory[] categories = new QuestionCategory[]{ 80 | QuestionCategory.HISTORY, QuestionCategory.FAVORITES, QuestionCategory.HOT}; 81 | mViewPager.setAdapter(new QuestionPagerAdapter(getSupportFragmentManager(), Arrays.asList(categories))); 82 | mTabLayout.setupWithViewPager(mViewPager); 83 | mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { 84 | @Override 85 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 86 | 87 | } 88 | 89 | @Override 90 | public void onPageSelected(int position) { 91 | if (position == 0) { 92 | mFabBtn.show(); 93 | } else { 94 | mFabBtn.hide(); 95 | } 96 | } 97 | 98 | @Override 99 | public void onPageScrollStateChanged(int state) { 100 | 101 | } 102 | }); 103 | } 104 | 105 | @OnClick(R.id.question_add) 106 | void onFabClick() { 107 | EventBus.getDefault().post(new NewQuestionEvent()); 108 | } 109 | 110 | @Override 111 | public void onBackPressed() { 112 | if (!BackHandlerHelper.handleBackPress(this)) { 113 | super.onBackPressed(); 114 | } 115 | } 116 | 117 | @Subscribe(threadMode = ThreadMode.MAIN) 118 | public void onMessageEvent(EditModeChangedEvent event) { 119 | if (event.isEditMode) { 120 | mFabBtn.hide(); 121 | mViewPager.setScrollable(false); 122 | mTabLayout.setVisibility(View.GONE); 123 | } else { 124 | mFabBtn.show(); 125 | mViewPager.setScrollable(true); 126 | mTabLayout.setVisibility(View.VISIBLE); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/question/QuestionCategory.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.question; 2 | 3 | /** 4 | * @author HansChen 5 | */ 6 | enum QuestionCategory { 7 | 8 | HISTORY, 9 | FAVORITES, 10 | HOT 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/question/QuestionFragment.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.question; 2 | 3 | 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v7.widget.LinearLayoutManager; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.text.InputType; 11 | import android.text.TextUtils; 12 | import android.view.LayoutInflater; 13 | import android.view.Menu; 14 | import android.view.MenuInflater; 15 | import android.view.MenuItem; 16 | import android.view.View; 17 | import android.view.ViewGroup; 18 | import android.widget.Toast; 19 | 20 | import com.afollestad.materialdialogs.DialogAction; 21 | import com.afollestad.materialdialogs.MaterialDialog; 22 | 23 | import org.greenrobot.eventbus.EventBus; 24 | import org.greenrobot.eventbus.Subscribe; 25 | import org.greenrobot.eventbus.ThreadMode; 26 | 27 | import java.util.HashSet; 28 | import java.util.Iterator; 29 | import java.util.List; 30 | import java.util.Set; 31 | 32 | import butterknife.BindView; 33 | import butterknife.ButterKnife; 34 | import io.reactivex.Observable; 35 | import io.reactivex.ObservableEmitter; 36 | import io.reactivex.ObservableOnSubscribe; 37 | import io.reactivex.ObservableSource; 38 | import io.reactivex.Observer; 39 | import io.reactivex.android.schedulers.AndroidSchedulers; 40 | import io.reactivex.disposables.Disposable; 41 | import io.reactivex.functions.Function; 42 | import io.reactivex.schedulers.Schedulers; 43 | import site.hanschen.pretty.R; 44 | import site.hanschen.pretty.application.PrettyApplication; 45 | import site.hanschen.pretty.db.bean.Question; 46 | import site.hanschen.pretty.db.repository.PrettyRepository; 47 | import site.hanschen.pretty.eventbus.EditModeChangedEvent; 48 | import site.hanschen.pretty.eventbus.NewPictureEvent; 49 | import site.hanschen.pretty.eventbus.NewQuestionEvent; 50 | import site.hanschen.pretty.eventbus.ShareFromZhihuEvent; 51 | import site.hanschen.pretty.ui.picture.PictureListActivity; 52 | import site.hanschen.pretty.widget.FragmentBackHandler; 53 | import site.hanschen.pretty.zhihu.ZhiHuApi; 54 | 55 | /** 56 | * @author HansChen 57 | */ 58 | public class QuestionFragment extends Fragment implements FragmentBackHandler { 59 | 60 | private static String KEY_CATEGORY = "KEY_CATEGORY"; 61 | 62 | public static QuestionFragment newInstance(QuestionCategory category) { 63 | QuestionFragment fragment = new QuestionFragment(); 64 | Bundle args = new Bundle(); 65 | args.putSerializable(KEY_CATEGORY, category); 66 | fragment.setArguments(args); 67 | return fragment; 68 | } 69 | 70 | @BindView(R.id.question_list) 71 | RecyclerView mRecyclerView; 72 | 73 | private QuestionListAdapter mAdapter; 74 | private QuestionCategory mCategory; 75 | private List mQuestion; 76 | private MaterialDialog mWaitingDialog; 77 | 78 | private ZhiHuApi mApi; 79 | private PrettyRepository mPrettyRepository; 80 | protected Set mSelections; 81 | 82 | private Menu mMenu; 83 | 84 | @Override 85 | public void onCreate(@Nullable Bundle savedInstanceState) { 86 | super.onCreate(savedInstanceState); 87 | if (getArguments() == null || !getArguments().containsKey(KEY_CATEGORY)) { 88 | throw new IllegalStateException("bundle must contain category info"); 89 | } 90 | mCategory = (QuestionCategory) getArguments().getSerializable(KEY_CATEGORY); 91 | EventBus.getDefault().register(this); 92 | } 93 | 94 | @Nullable 95 | @Override 96 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { 97 | setHasOptionsMenu(true); 98 | return inflater.inflate(R.layout.fragment_question_list, container, false); 99 | } 100 | 101 | @Override 102 | public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 103 | super.onViewCreated(view, savedInstanceState); 104 | ButterKnife.bind(this, view); 105 | mPrettyRepository = PrettyApplication.getInstance().getPrettyRepository(); 106 | mApi = PrettyApplication.getInstance().getApi(); 107 | mSelections = new HashSet<>(); 108 | initViews(); 109 | initData(); 110 | } 111 | 112 | @Override 113 | public void onDestroy() { 114 | super.onDestroy(); 115 | EventBus.getDefault().unregister(this); 116 | } 117 | 118 | @Override 119 | public boolean onBackPressed() { 120 | if (mAdapter.isEditMode()) { 121 | exitEditMode(); 122 | return true; 123 | } 124 | return false; 125 | } 126 | 127 | @Override 128 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 129 | inflater.inflate(R.menu.menu_question_fragment, menu); 130 | mMenu = menu; 131 | mMenu.findItem(R.id.select_all).setVisible(false); 132 | mMenu.findItem(R.id.un_select_all).setVisible(false); 133 | mMenu.findItem(R.id.delete).setVisible(false); 134 | } 135 | 136 | @Override 137 | public boolean onOptionsItemSelected(MenuItem item) { 138 | switch (item.getItemId()) { 139 | case R.id.select_all: 140 | selectAll(); 141 | break; 142 | case R.id.un_select_all: 143 | clearAll(); 144 | break; 145 | case R.id.delete: 146 | showDeleteDialog(); 147 | break; 148 | } 149 | return true; 150 | } 151 | 152 | private void initData() { 153 | switch (mCategory) { 154 | case HISTORY: 155 | Observable.create(new ObservableOnSubscribe>() { 156 | @Override 157 | public void subscribe(ObservableEmitter> e) throws Exception { 158 | e.onNext(mPrettyRepository.getAllQuestion()); 159 | e.onComplete(); 160 | } 161 | }) 162 | .subscribeOn(Schedulers.io()) 163 | .observeOn(AndroidSchedulers.mainThread()) 164 | .subscribe(new Observer>() { 165 | @Override 166 | public void onSubscribe(Disposable d) { 167 | 168 | } 169 | 170 | @Override 171 | public void onNext(List questions) { 172 | mQuestion = questions; 173 | mAdapter.setData(mQuestion); 174 | } 175 | 176 | @Override 177 | public void onError(Throwable e) { 178 | 179 | } 180 | 181 | @Override 182 | public void onComplete() { 183 | 184 | } 185 | }); 186 | break; 187 | } 188 | 189 | } 190 | 191 | private void initViews() { 192 | mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); 193 | mAdapter = new QuestionListAdapter(getActivity()); 194 | mAdapter.setItemClickListener(mOnItemClickListener); 195 | mRecyclerView.setAdapter(mAdapter); 196 | } 197 | 198 | private QuestionListAdapter.OnItemClickListener mOnItemClickListener = new QuestionListAdapter.OnItemClickListener() { 199 | @Override 200 | public void onItemClick(final Question question) { 201 | if (mAdapter.isEditMode()) { 202 | if (mSelections.contains(question.hashCode())) { 203 | unSelectQuestion(question); 204 | } else { 205 | selectQuestion(question); 206 | } 207 | mAdapter.notifyDataSetChanged(); 208 | } else { 209 | PictureListActivity.open(getActivity(), question.getQuestionId(), question.getTitle()); 210 | } 211 | } 212 | 213 | @Override 214 | public void onItemLongClick(Question question) { 215 | if (!mAdapter.isEditMode()) { 216 | selectQuestion(question); 217 | enterEditMode(mSelections); 218 | } 219 | } 220 | }; 221 | 222 | private void selectQuestion(Question question) { 223 | mSelections.add(question.hashCode()); 224 | mAdapter.notifyDataSetChanged(); 225 | } 226 | 227 | private void selectAll() { 228 | for (Question q : mQuestion) { 229 | mSelections.add(q.hashCode()); 230 | } 231 | mAdapter.notifyDataSetChanged(); 232 | } 233 | 234 | private void clearAll() { 235 | mSelections.clear(); 236 | mAdapter.notifyDataSetChanged(); 237 | } 238 | 239 | private void unSelectQuestion(Question question) { 240 | mSelections.remove(question.hashCode()); 241 | mAdapter.notifyDataSetChanged(); 242 | } 243 | 244 | private void enterEditMode(Set selections) { 245 | mAdapter.enterEditMode(selections); 246 | mMenu.findItem(R.id.select_all).setVisible(true); 247 | mMenu.findItem(R.id.un_select_all).setVisible(true); 248 | mMenu.findItem(R.id.delete).setVisible(true); 249 | EventBus.getDefault().post(new EditModeChangedEvent(true)); 250 | } 251 | 252 | private void exitEditMode() { 253 | clearAll(); 254 | mAdapter.exitEditMode(); 255 | mMenu.findItem(R.id.select_all).setVisible(false); 256 | mMenu.findItem(R.id.un_select_all).setVisible(false); 257 | mMenu.findItem(R.id.delete).setVisible(false); 258 | EventBus.getDefault().post(new EditModeChangedEvent(false)); 259 | } 260 | 261 | @Subscribe(threadMode = ThreadMode.MAIN) 262 | public void onMessageEvent(NewQuestionEvent event) { 263 | if (mCategory == QuestionCategory.HISTORY) { 264 | showNewQuestionDialog(); 265 | } 266 | } 267 | 268 | @Subscribe(threadMode = ThreadMode.MAIN) 269 | public void onMessageEvent(NewPictureEvent event) { 270 | if (mCategory == QuestionCategory.HISTORY) { 271 | if (event.pictures.size() > 0) { 272 | for (Question q : mQuestion) { 273 | if (q.getQuestionId() == event.questionId) { 274 | q.getPictures().addAll(event.pictures); 275 | break; 276 | } 277 | } 278 | mAdapter.notifyDataSetChanged(); 279 | } 280 | } 281 | } 282 | 283 | @Subscribe(threadMode = ThreadMode.MAIN) 284 | public void onMessageEvent(ShareFromZhihuEvent event) { 285 | if (mCategory == QuestionCategory.HISTORY) { 286 | addNewQuestionDialog(event.title, event.url); 287 | } 288 | } 289 | 290 | private void addNewQuestionDialog(final String title, final String url) { 291 | new MaterialDialog.Builder(getActivity()).title("添加话题") 292 | .content("添加话题: " + title) 293 | .negativeText("取消") 294 | .onNegative(new MaterialDialog.SingleButtonCallback() { 295 | @Override 296 | public void onClick(@NonNull MaterialDialog dialog, 297 | @NonNull DialogAction which) { 298 | dialog.dismiss(); 299 | 300 | } 301 | }) 302 | .positiveText("添加") 303 | .onPositive(new MaterialDialog.SingleButtonCallback() { 304 | @Override 305 | public void onClick(@NonNull MaterialDialog dialog, 306 | @NonNull DialogAction which) { 307 | dialog.dismiss(); 308 | fetchQuestionDetail(url); 309 | } 310 | }) 311 | .build() 312 | .show(); 313 | } 314 | 315 | private void showNewQuestionDialog() { 316 | new MaterialDialog.Builder(getActivity()).title("添加话题") 317 | .content("请输入话题地址") 318 | .input("地址", 319 | "https://www.zhihu.com/question/37787176", 320 | false, 321 | new MaterialDialog.InputCallback() { 322 | @Override 323 | public void onInput(@NonNull MaterialDialog dialog, 324 | CharSequence input) { 325 | if (mApi.isUrlValid(input.toString())) { 326 | dialog.dismiss(); 327 | fetchQuestionDetail(input.toString()); 328 | } else { 329 | //noinspection ConstantConditions 330 | dialog.getContentView().setText("请输入正确的地址"); 331 | dialog.getContentView() 332 | .setTextColor(getResources().getColor(R.color.error)); 333 | } 334 | } 335 | }) 336 | .inputType(InputType.TYPE_CLASS_NUMBER) 337 | .negativeText("取消") 338 | .onNegative(new MaterialDialog.SingleButtonCallback() { 339 | @Override 340 | public void onClick(@NonNull MaterialDialog dialog, 341 | @NonNull DialogAction which) { 342 | dialog.dismiss(); 343 | 344 | } 345 | }) 346 | .autoDismiss(false) 347 | .build() 348 | .show(); 349 | } 350 | 351 | private void fetchQuestionDetail(final String url) { 352 | final int questionId = mApi.parseQuestionId(url); 353 | for (Question q : mQuestion) { 354 | if (q.getQuestionId() == questionId) { 355 | Toast.makeText(getActivity().getApplicationContext(), "话题已存在", Toast.LENGTH_SHORT).show(); 356 | return; 357 | } 358 | } 359 | Observable.create(new ObservableOnSubscribe() { 360 | @Override 361 | public void subscribe(@NonNull ObservableEmitter emitter) throws Exception { 362 | emitter.onNext(mApi.getHtml(questionId)); 363 | emitter.onComplete(); 364 | } 365 | }) 366 | .subscribeOn(Schedulers.io()) 367 | .observeOn(Schedulers.io()) 368 | .flatMap(new Function>() { 369 | @Override 370 | public ObservableSource apply(final @NonNull String html) throws Exception { 371 | return Observable.create(new ObservableOnSubscribe() { 372 | @Override 373 | public void subscribe(@NonNull ObservableEmitter emitter) throws Exception { 374 | int count = mApi.parseAnswerCount(html); 375 | String title = mApi.parseQuestionTitle(html); 376 | if (count == 0 && TextUtils.isEmpty(title)) { 377 | emitter.onError(new Exception()); 378 | } else { 379 | Question question = new Question(null, questionId, title, count); 380 | mPrettyRepository.insertOrUpdate(question); 381 | emitter.onNext(question); 382 | emitter.onComplete(); 383 | } 384 | } 385 | }); 386 | } 387 | }) 388 | .observeOn(AndroidSchedulers.mainThread()) 389 | .subscribe(new Observer() { 390 | @Override 391 | public void onSubscribe(@NonNull Disposable d) { 392 | showWaitingDialog("请稍等", "正在获取话题..."); 393 | } 394 | 395 | @Override 396 | public void onNext(@NonNull Question question) { 397 | dismissDialog(); 398 | mQuestion.add(question); 399 | mAdapter.notifyDataSetChanged(); 400 | mRecyclerView.smoothScrollToPosition(mAdapter.getItemCount() - 1); 401 | Toast.makeText(getActivity().getApplicationContext(), "话题已添加", Toast.LENGTH_SHORT).show(); 402 | } 403 | 404 | @Override 405 | public void onError(@NonNull Throwable e) { 406 | Toast.makeText(getActivity().getApplicationContext(), "话题添加失败", Toast.LENGTH_SHORT).show(); 407 | dismissDialog(); 408 | } 409 | 410 | @Override 411 | public void onComplete() { 412 | 413 | } 414 | }); 415 | } 416 | 417 | private void showDeleteDialog() { 418 | new MaterialDialog.Builder(getActivity()).title("删除话题") 419 | .content("是否删除选中话题?") 420 | .positiveText("删除") 421 | .onPositive(new MaterialDialog.SingleButtonCallback() { 422 | @Override 423 | public void onClick(@NonNull MaterialDialog dialog, 424 | @NonNull DialogAction which) { 425 | Iterator iterator = mQuestion.iterator(); 426 | while (iterator.hasNext()) { 427 | Question q = iterator.next(); 428 | if (mSelections.contains(q.hashCode())) { 429 | iterator.remove(); 430 | mPrettyRepository.deleteQuestion(q.getQuestionId()); 431 | } 432 | } 433 | mAdapter.notifyDataSetChanged(); 434 | exitEditMode(); 435 | } 436 | }) 437 | .negativeText("取消") 438 | .onNegative(new MaterialDialog.SingleButtonCallback() { 439 | @Override 440 | public void onClick(@NonNull MaterialDialog dialog, 441 | @NonNull DialogAction which) { 442 | dialog.dismiss(); 443 | } 444 | }) 445 | .build() 446 | .show(); 447 | } 448 | 449 | protected void showWaitingDialog(String title, String message) { 450 | dismissDialog(); 451 | mWaitingDialog = new MaterialDialog.Builder(getActivity()).title(title) 452 | .cancelable(false) 453 | .canceledOnTouchOutside(false) 454 | .progress(true, 0) 455 | .progressIndeterminateStyle(true) 456 | .content(message) 457 | .build(); 458 | mWaitingDialog.show(); 459 | } 460 | 461 | protected void dismissDialog() { 462 | if (mWaitingDialog != null && mWaitingDialog.isShowing()) { 463 | mWaitingDialog.dismiss(); 464 | mWaitingDialog = null; 465 | } 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/question/QuestionListAdapter.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.question; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.util.Log; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.CheckBox; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import com.amulyakhare.textdrawable.TextDrawable; 14 | 15 | import java.util.List; 16 | import java.util.Locale; 17 | import java.util.Set; 18 | 19 | import butterknife.BindView; 20 | import butterknife.ButterKnife; 21 | import site.hanschen.pretty.R; 22 | import site.hanschen.pretty.db.bean.Question; 23 | import site.hanschen.pretty.utils.ColorUtils; 24 | 25 | /** 26 | * @author HansChen 27 | */ 28 | public class QuestionListAdapter extends RecyclerView.Adapter implements View.OnClickListener, View.OnLongClickListener { 29 | 30 | private List mQuestions; 31 | private Context mContext; 32 | private LayoutInflater mInflater; 33 | private OnItemClickListener mOnItemClickListener; 34 | private TextDrawable.IBuilder mBuilder; 35 | private boolean mEditMode; 36 | protected Set mSelections; 37 | 38 | public QuestionListAdapter(Context context) { 39 | this.mContext = context; 40 | this.mInflater = LayoutInflater.from(context); 41 | mBuilder = TextDrawable.builder().round(); 42 | } 43 | 44 | public void setData(List questions) { 45 | this.mQuestions = questions; 46 | notifyDataSetChanged(); 47 | } 48 | 49 | public void setItemClickListener(OnItemClickListener onItemClickListener) { 50 | this.mOnItemClickListener = onItemClickListener; 51 | } 52 | 53 | public void enterEditMode(Set selections) { 54 | mEditMode = true; 55 | mSelections = selections; 56 | notifyDataSetChanged(); 57 | } 58 | 59 | public void exitEditMode() { 60 | mEditMode = false; 61 | if (mSelections != null) { 62 | mSelections.clear(); 63 | mSelections = null; 64 | } 65 | notifyDataSetChanged(); 66 | } 67 | 68 | public boolean isEditMode() { 69 | return mEditMode; 70 | } 71 | 72 | @Override 73 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 74 | View root = mInflater.inflate(R.layout.list_item_two_line_with_icon_and_check, parent, false); 75 | root.setOnClickListener(QuestionListAdapter.this); 76 | root.setOnLongClickListener(QuestionListAdapter.this); 77 | return new ViewHolder(root); 78 | } 79 | 80 | @Override 81 | public void onBindViewHolder(ViewHolder holder, int position) { 82 | holder.itemView.setTag(position); 83 | 84 | Question question = mQuestions.get(position); 85 | String firstChar = question.getTitle().substring(0, 1); 86 | int color = ColorUtils.getColor(question.getTitle()); 87 | TextDrawable drawable = mBuilder.build(firstChar, color); 88 | holder.icon.setImageDrawable(drawable); 89 | 90 | holder.title.setText(question.getTitle()); 91 | holder.detail.setText(String.format(Locale.getDefault(), 92 | "%d个回答, 已抓取%d张照片", 93 | question.getAnswerCount(), 94 | question.getPictures().size())); 95 | 96 | if (mEditMode) { 97 | holder.checkBox.setVisibility(View.VISIBLE); 98 | if (mSelections.contains(question.hashCode())) { 99 | holder.checkBox.setChecked(true); 100 | } else { 101 | holder.checkBox.setChecked(false); 102 | } 103 | } else { 104 | holder.checkBox.setVisibility(View.GONE); 105 | } 106 | } 107 | 108 | @Override 109 | public int getItemCount() { 110 | return mQuestions == null ? 0 : mQuestions.size(); 111 | } 112 | 113 | @Override 114 | public void onClick(View v) { 115 | if (mOnItemClickListener != null) { 116 | Question question = mQuestions.get((Integer) v.getTag()); 117 | mOnItemClickListener.onItemClick(question); 118 | } 119 | } 120 | 121 | @Override 122 | public boolean onLongClick(View v) { 123 | if (mOnItemClickListener != null) { 124 | Question question = mQuestions.get((Integer) v.getTag()); 125 | mOnItemClickListener.onItemLongClick(question); 126 | return true; 127 | } 128 | return false; 129 | } 130 | 131 | public interface OnItemClickListener { 132 | 133 | void onItemClick(Question question); 134 | 135 | void onItemLongClick(Question question); 136 | } 137 | 138 | static class ViewHolder extends RecyclerView.ViewHolder { 139 | 140 | @BindView(R.id.item_icon) 141 | ImageView icon; 142 | @BindView(R.id.item_primary_text) 143 | TextView title; 144 | @BindView(R.id.item_secondary_text) 145 | TextView detail; 146 | @BindView(R.id.item_check) 147 | CheckBox checkBox; 148 | 149 | ViewHolder(View itemView) { 150 | super(itemView); 151 | ButterKnife.bind(this, itemView); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/question/QuestionPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.question; 2 | 3 | import android.support.v4.app.Fragment; 4 | import android.support.v4.app.FragmentManager; 5 | import android.support.v4.app.FragmentPagerAdapter; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author HansChen 11 | */ 12 | public class QuestionPagerAdapter extends FragmentPagerAdapter { 13 | 14 | private List mCategories; 15 | 16 | public QuestionPagerAdapter(FragmentManager fm, List categories) { 17 | super(fm); 18 | this.mCategories = categories; 19 | } 20 | 21 | @Override 22 | public long getItemId(int position) { 23 | return position; 24 | } 25 | 26 | @Override 27 | public int getCount() { 28 | return mCategories != null ? mCategories.size() : 0; 29 | } 30 | 31 | @Override 32 | public CharSequence getPageTitle(int position) { 33 | switch (mCategories.get(position)) { 34 | case HISTORY: 35 | return "历史"; 36 | case FAVORITES: 37 | return "收藏"; 38 | case HOT: 39 | return "热门"; 40 | default: 41 | throw new IllegalStateException("unknown category: " + mCategories.get(position)); 42 | } 43 | } 44 | 45 | @Override 46 | public Fragment getItem(int position) { 47 | return QuestionFragment.newInstance(mCategories.get(position)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/ui/splash/SplashActivity.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.ui.splash; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.annotation.Nullable; 6 | 7 | import site.hanschen.pretty.base.BaseActivity; 8 | import site.hanschen.pretty.ui.question.QuestionActivity; 9 | 10 | /** 11 | * @author HansChen 12 | */ 13 | public class SplashActivity extends BaseActivity { 14 | 15 | @Override 16 | protected void onCreate(@Nullable Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | startActivity(new Intent(SplashActivity.this, QuestionActivity.class)); 19 | finish(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/utils/ColorUtils.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.utils; 2 | 3 | import android.support.annotation.ColorInt; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.Random; 8 | 9 | /** 10 | * @author HansChen 11 | */ 12 | public class ColorUtils { 13 | 14 | private ColorUtils() { 15 | } 16 | 17 | public static List MATERIAL = Arrays.asList(0xffe57373, 18 | 0xfff06292, 19 | 0xffba68c8, 20 | 0xff9575cd, 21 | 0xff7986cb, 22 | 0xff64b5f6, 23 | 0xff4fc3f7, 24 | 0xff4dd0e1, 25 | 0xff4db6ac, 26 | 0xff81c784, 27 | 0xffaed581, 28 | 0xffff8a65, 29 | 0xffd4e157, 30 | 0xffffd54f, 31 | 0xffffb74d, 32 | 0xffa1887f, 33 | 0xff90a4ae); 34 | 35 | 36 | private static final Random mRandom = new Random(System.currentTimeMillis()); 37 | 38 | @ColorInt 39 | public static int getRandomColor() { 40 | return MATERIAL.get(mRandom.nextInt(MATERIAL.size())); 41 | } 42 | 43 | @ColorInt 44 | public static int getColor(Object key) { 45 | return MATERIAL.get(Math.abs(key.hashCode()) % MATERIAL.size()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/utils/CommonUtils.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.utils; 2 | 3 | import java.util.regex.Matcher; 4 | import java.util.regex.Pattern; 5 | 6 | /** 7 | * @author HansChen 8 | */ 9 | public class CommonUtils { 10 | 11 | private CommonUtils() { 12 | } 13 | 14 | public static String getSmallPicture(String url) { 15 | return url.replace("_b", "_100w"); 16 | } 17 | 18 | public static String getTitleFromShare(String shareText) { 19 | String regex = "([\\S ]+)(http[s]?://www\\.zhihu\\.com/question/[0-9]+)([\\S ]+)"; 20 | Pattern p = Pattern.compile(regex); 21 | Matcher matcher = p.matcher(shareText); 22 | if (matcher.matches()) { 23 | return matcher.group(1); 24 | } 25 | return null; 26 | } 27 | 28 | public static String getUrlFromShare(String shareText) { 29 | String regex = "([\\S ]+)(http[s]?://www\\.zhihu\\.com/question/[0-9]+)([\\S ]+)"; 30 | Pattern p = Pattern.compile(regex); 31 | Matcher matcher = p.matcher(shareText); 32 | if (matcher.matches()) { 33 | return matcher.group(2); 34 | } 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/utils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.utils; 2 | 3 | 4 | import com.google.gson.Gson; 5 | import com.google.gson.reflect.TypeToken; 6 | 7 | import java.util.List; 8 | 9 | public class JsonUtils { 10 | 11 | private JsonUtils() { 12 | } 13 | 14 | private static Gson gson = new Gson(); 15 | 16 | public static T fromJsonObject(String jsonStr, Class targetClass) { 17 | return gson.fromJson(jsonStr, targetClass); 18 | } 19 | 20 | public static List fromJsonArray(String jsonStr) { 21 | return gson.fromJson(jsonStr, new TypeToken>() { 22 | }.getType()); 23 | } 24 | 25 | public static String toJson(Object o) { 26 | return gson.toJson(o); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/widget/BackHandlerHelper.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.widget; 2 | 3 | 4 | import android.support.v4.app.Fragment; 5 | import android.support.v4.app.FragmentActivity; 6 | import android.support.v4.app.FragmentManager; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * @author HansChen 12 | */ 13 | public class BackHandlerHelper { 14 | 15 | /** 16 | * 将back事件分发给 FragmentManager 中管理的子Fragment,如果该 FragmentManager 中的所有Fragment都 17 | * 没有处理back事件,则尝试 FragmentManager.popBackStack() 18 | * 19 | * @return 如果处理了back键则返回 true 20 | * @see #handleBackPress(Fragment) 21 | * @see #handleBackPress(FragmentActivity) 22 | */ 23 | public static boolean handleBackPress(FragmentManager fragmentManager) { 24 | List fragments = fragmentManager.getFragments(); 25 | 26 | if (fragments == null) { 27 | return false; 28 | } 29 | 30 | for (int i = fragments.size() - 1; i >= 0; i--) { 31 | Fragment child = fragments.get(i); 32 | 33 | if (isFragmentBackHandled(child)) { 34 | return true; 35 | } 36 | } 37 | 38 | if (fragmentManager.getBackStackEntryCount() > 0) { 39 | fragmentManager.popBackStack(); 40 | return true; 41 | } 42 | return false; 43 | } 44 | 45 | public static boolean handleBackPress(Fragment fragment) { 46 | return handleBackPress(fragment.getChildFragmentManager()); 47 | } 48 | 49 | public static boolean handleBackPress(FragmentActivity fragmentActivity) { 50 | return handleBackPress(fragmentActivity.getSupportFragmentManager()); 51 | } 52 | 53 | /** 54 | * 判断Fragment是否处理了Back键 55 | * 56 | * @return 如果处理了back键则返回 true 57 | */ 58 | public static boolean isFragmentBackHandled(Fragment fragment) { 59 | return fragment != null && fragment.isVisible() && fragment.getUserVisibleHint() //for ViewPager 60 | && fragment instanceof FragmentBackHandler && ((FragmentBackHandler) fragment).onBackPressed(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/widget/DepthPageTransformer.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.widget; 2 | 3 | import android.support.v4.view.ViewPager; 4 | import android.view.View; 5 | 6 | public class DepthPageTransformer implements ViewPager.PageTransformer { 7 | 8 | private static final float MIN_SCALE = 0.75f; 9 | 10 | public void transformPage(View view, float position) { 11 | int pageWidth = view.getWidth(); 12 | 13 | if (position < -1) { // [-Infinity,-1) 14 | // This page is way off-screen to the left. 15 | view.setAlpha(0); 16 | 17 | } else if (position <= 0) { // [-1,0] 18 | // Use the default slide transition when moving to the left page 19 | view.setAlpha(1); 20 | view.setTranslationX(0); 21 | view.setScaleX(1); 22 | view.setScaleY(1); 23 | 24 | } else if (position <= 1) { // (0,1] 25 | // Fade the page out. 26 | view.setAlpha(1 - position); 27 | 28 | // Counteract the default slide transition 29 | view.setTranslationX(pageWidth * -position); 30 | 31 | // Scale the page down (between MIN_SCALE and 1) 32 | float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position)); 33 | view.setScaleX(scaleFactor); 34 | view.setScaleY(scaleFactor); 35 | 36 | } else { // (1,+Infinity] 37 | // This page is way off-screen to the right. 38 | view.setAlpha(0); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/widget/FragmentBackHandler.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.widget; 2 | 3 | /** 4 | * @author HansChen 5 | */ 6 | public interface FragmentBackHandler { 7 | boolean onBackPressed(); 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/widget/ScrollBehavior.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.widget; 2 | 3 | import android.content.Context; 4 | import android.support.design.widget.CoordinatorLayout; 5 | import android.support.design.widget.FloatingActionButton; 6 | import android.support.v13.view.ViewCompat; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | 10 | /** 11 | * @author HansChen 12 | */ 13 | public class ScrollBehavior extends FloatingActionButton.Behavior { 14 | 15 | public ScrollBehavior(Context context, AttributeSet attrs) { 16 | super(); 17 | } 18 | 19 | @Override 20 | public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, 21 | final FloatingActionButton child, 22 | final View directTargetChild, 23 | final View target, 24 | final int nestedScrollAxes) { 25 | if (directTargetChild instanceof ScrollViewPager && !((ScrollViewPager) directTargetChild).isScrollable()) { 26 | return false; 27 | } 28 | return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL || super.onStartNestedScroll(coordinatorLayout, 29 | child, 30 | directTargetChild, 31 | target, 32 | nestedScrollAxes); 33 | } 34 | 35 | @Override 36 | public void onNestedScroll(final CoordinatorLayout coordinatorLayout, 37 | final FloatingActionButton child, 38 | final View target, 39 | final int dxConsumed, 40 | final int dyConsumed, 41 | final int dxUnconsumed, 42 | final int dyUnconsumed) { 43 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); 44 | if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) { 45 | child.hide(); 46 | } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) { 47 | child.show(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/widget/ScrollViewPager.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.widget; 2 | 3 | import android.content.Context; 4 | import android.support.v4.view.ViewPager; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | 8 | /** 9 | * @author HansChen 10 | */ 11 | public class ScrollViewPager extends ViewPager { 12 | 13 | 14 | private boolean scrollable = true; 15 | 16 | public ScrollViewPager(Context context) { 17 | super(context); 18 | } 19 | 20 | public ScrollViewPager(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | @Override 25 | public boolean onInterceptTouchEvent(MotionEvent ev) { 26 | return scrollable && super.onInterceptTouchEvent(ev); 27 | } 28 | 29 | @Override 30 | public boolean onTouchEvent(MotionEvent ev) { 31 | return scrollable && super.onTouchEvent(ev); 32 | } 33 | 34 | public boolean isScrollable() { 35 | return scrollable; 36 | } 37 | 38 | public void setScrollable(boolean scrollable) { 39 | this.scrollable = scrollable; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/widget/ViewPagerCatchException.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.widget; 2 | 3 | import android.content.Context; 4 | import android.support.v4.view.ViewPager; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | 8 | /** 9 | * fix the bug when image zooming crashing in ViewPager (catch IllegalArgumentException exceptions) 10 | * 11 | * @author HansChen 12 | */ 13 | public class ViewPagerCatchException extends ViewPager { 14 | 15 | public ViewPagerCatchException(Context context) { 16 | super(context); 17 | } 18 | 19 | public ViewPagerCatchException(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | } 22 | 23 | 24 | @Override 25 | public boolean onInterceptTouchEvent(MotionEvent ev) { 26 | try { 27 | return super.onInterceptTouchEvent(ev); 28 | } catch (IllegalArgumentException e) { 29 | return false; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/zhihu/ZhiHuApi.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.zhihu; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | import site.hanschen.pretty.zhihu.bean.AnswerList; 7 | 8 | /** 9 | * @author HansChen 10 | */ 11 | public interface ZhiHuApi { 12 | 13 | boolean isUrlValid(String url); 14 | 15 | int parseQuestionId(String url); 16 | 17 | String parseQuestionTitle(String html); 18 | 19 | int parseAnswerCount(String html); 20 | 21 | List parsePictureList(String answer); 22 | 23 | String getHtml(int questionId) throws IOException; 24 | 25 | AnswerList getAnswerList(int questionId, int pageSize, int offset) throws IOException; 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/zhihu/ZhiHuApiApiImpl.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.zhihu; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | import okhttp3.FormBody; 14 | import site.hanschen.pretty.base.HttpClient; 15 | import site.hanschen.pretty.utils.JsonUtils; 16 | import site.hanschen.pretty.zhihu.bean.AnswerList; 17 | import site.hanschen.pretty.zhihu.bean.RequestAnswerParams; 18 | 19 | /** 20 | * @author HansChen 21 | */ 22 | public class ZhiHuApiApiImpl implements ZhiHuApi { 23 | 24 | private HttpClient httpClient = new HttpClient(); 25 | 26 | @Override 27 | public boolean isUrlValid(String url) { 28 | String regex = "https://www\\.zhihu\\.com/question/\\d+"; 29 | Pattern p = Pattern.compile(regex); 30 | Matcher matcher = p.matcher(url); 31 | return matcher.matches(); 32 | } 33 | 34 | @Override 35 | public int parseQuestionId(String url) { 36 | int index = url.lastIndexOf('/'); 37 | return Integer.parseInt(url.substring(index + 1)); 38 | } 39 | 40 | @Override 41 | public String parseQuestionTitle(String html) { 42 | String regex = "

([\\S ]+)

"; 43 | Pattern p = Pattern.compile(regex); 44 | Matcher matcher = p.matcher(html); 45 | if (matcher.find()) { 46 | return matcher.group(1); 47 | } 48 | 49 | return null; 50 | } 51 | 52 | @Override 53 | public int parseAnswerCount(String html) { 54 | String regex = "

(\\d+) 个回答

"; 55 | Pattern p = Pattern.compile(regex); 56 | Matcher matcher = p.matcher(html); 57 | if (matcher.find()) { 58 | String count = matcher.group(1); 59 | return Integer.valueOf(count); 60 | } 61 | 62 | return 0; 63 | } 64 | 65 | @Override 66 | public List parsePictureList(String answer) { 67 | List pictures = new ArrayList<>(); 68 | String regex = "data-actualsrc=\"(https://pic[0-9]+\\.zhimg\\.com/[0-9a-zA-Z\\-]+_b\\.(jpg|png))\""; 69 | Pattern p = Pattern.compile(regex); 70 | Matcher matcher = p.matcher(answer); 71 | while (matcher.find()) { 72 | pictures.add(matcher.group(1)); 73 | } 74 | return pictures; 75 | } 76 | 77 | @Override 78 | public String getHtml(int questionId) throws IOException { 79 | Log.d("Hans", "getHtml: " + "https://www.zhihu.com/question/" + questionId); 80 | return httpClient.httpGet("https://www.zhihu.com/question/" + questionId); 81 | } 82 | 83 | @Override 84 | public AnswerList getAnswerList(int questionId, int pageSize, int offset) throws IOException { 85 | RequestAnswerParams params = new RequestAnswerParams(questionId, pageSize, offset); 86 | Log.d("Hans", "getAnswerList: " + JsonUtils.toJson(params)); 87 | FormBody body = new FormBody.Builder().add("method", "next").add("params", JsonUtils.toJson(params)).build(); 88 | Map header = new HashMap<>(); 89 | header.put("Content-Type", "application/x-www-form-urlencoded"); 90 | 91 | String answer = httpClient.httpPost("https://www.zhihu.com/node/QuestionAnswerListV2", header, body); 92 | AnswerList answerList = JsonUtils.fromJsonObject(answer, AnswerList.class); 93 | Log.d("Hans", "answerList: " + answerList.msg.size()); 94 | return answerList; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/zhihu/bean/AnswerList.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.zhihu.bean; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author HansChen 7 | */ 8 | public class AnswerList { 9 | 10 | public int r; 11 | public List msg; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/site/hanschen/pretty/zhihu/bean/RequestAnswerParams.java: -------------------------------------------------------------------------------- 1 | package site.hanschen.pretty.zhihu.bean; 2 | 3 | /** 4 | * @author HansChen 5 | */ 6 | public class RequestAnswerParams { 7 | 8 | public int url_token; 9 | public int pagesize; 10 | public int offset; 11 | 12 | public RequestAnswerParams() { 13 | } 14 | 15 | public RequestAnswerParams(int url_token, int pagesize, int offset) { 16 | this.url_token = url_token; 17 | this.pagesize = pagesize; 18 | this.offset = offset; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v21/bg_ripple_rectangle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/drawable-xhdpi/ic_action_back.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_back_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/drawable-xhdpi/icon_back_normal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_back_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/drawable-xhdpi/icon_back_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_menu_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/drawable-xhdpi/icon_menu_delete.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/drawable-xhdpi/icon_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/icon_share_press.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/drawable-xhdpi/icon_share_press.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/profile_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/drawable-xhdpi/profile_cover.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/back_picture_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_gallery_select_mark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_ripple_rectangle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_refresh_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_select_all_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_tab_unselected_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_share_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_circle_primary_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_gallery.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_picture_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 21 | 22 | 23 | 24 | 29 | 30 | 39 | 40 | 47 | 48 | 49 | 50 | 51 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_question.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 21 | 22 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_question_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_gallery_pager.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_gallery_recycle_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_picture.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_two_line_with_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 28 | 29 | 39 | 40 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_two_line_with_icon_and_check.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 28 | 29 | 34 | 35 | 45 | 46 | 54 | 55 | 56 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_picture_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_question_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-v19/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 知乎看图 3 | 分享 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #e6e6e6 8 | #00000000 9 | #ea482d 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Pretty-Zhihu 3 | Share 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | 26 | 30 | 31 | 34 | 35 |