├── .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 | [](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 | 
9 | 
10 | 
11 | 
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 = "";
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 = "";
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 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_question_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/test/java/site/hanschen/pretty/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package site.hanschen.pretty;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() throws Exception {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | google()
6 | jcenter()
7 | }
8 | dependencies {
9 | classpath 'com.android.tools.build:gradle:3.0.1'
10 | classpath 'org.greenrobot:greendao-gradle-plugin:3.2.2'
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 | maven {
22 | url "https://jitpack.io"
23 | }
24 | maven {
25 | url 'http://dl.bintray.com/amulyakhare/maven'
26 | }
27 | }
28 | }
29 |
30 | task clean(type: Delete) {
31 | delete rootProject.buildDir
32 | }
33 |
--------------------------------------------------------------------------------
/download/Pretty_v1.0.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/download/Pretty_v1.0.apk
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Mar 28 10:03:30 CST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/image/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/image/1.jpg
--------------------------------------------------------------------------------
/image/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/image/2.jpg
--------------------------------------------------------------------------------
/image/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/image/3.jpg
--------------------------------------------------------------------------------
/image/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hanschencoder/Pretty-Zhihu/55edf8f9270a941261f7f6a212041ebfb0f2f8c4/image/4.jpg
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------