├── .gitignore ├── LICENSE ├── README.md ├── ZhihuDaily.iml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── me │ │ └── chen_wei │ │ └── zhihu │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── zhihu.css │ ├── ic_launcher-web.png │ ├── java │ │ └── me │ │ │ └── chen_wei │ │ │ └── zhihu │ │ │ ├── Constants.java │ │ │ ├── MyApplication.java │ │ │ ├── cache │ │ │ └── ACache.java │ │ │ ├── event │ │ │ ├── AllStoriedDownloadedEvent.java │ │ │ ├── ContentsLoadedEvent.java │ │ │ ├── LatestContentsLoadedEvent.java │ │ │ ├── LoadContentEvent.java │ │ │ ├── LoadFailureEvent.java │ │ │ ├── NewsDownloadedEvent.java │ │ │ ├── NewsLoadedEvent.java │ │ │ └── TopStoriesLoadedEvent.java │ │ │ ├── network │ │ │ ├── api │ │ │ │ └── ZhihuAPI.java │ │ │ ├── model │ │ │ │ ├── Contents.java │ │ │ │ ├── Latest.java │ │ │ │ └── News.java │ │ │ └── processor │ │ │ │ ├── ContentsProcessor.java │ │ │ │ ├── IContentsProcessor.java │ │ │ │ ├── INewsProcessor.java │ │ │ │ ├── IOfflineDownloadProcessor.java │ │ │ │ ├── NewsProcessor.java │ │ │ │ └── OfflineDownloadProcessor.java │ │ │ ├── presenter │ │ │ ├── MainPresenter.java │ │ │ └── StoryPresenter.java │ │ │ ├── util │ │ │ ├── DateUtil.java │ │ │ └── NetworkUtil.java │ │ │ └── views │ │ │ ├── EndlessRecyclerViewScrollListener.java │ │ │ ├── activities │ │ │ ├── AboutMeActivity.java │ │ │ ├── IMainActivity.java │ │ │ ├── IStoryActivity.java │ │ │ ├── MainActivity.java │ │ │ └── StoryActivity.java │ │ │ └── adapter │ │ │ ├── StoryListAdapter.java │ │ │ └── TopStoriesAdapter.java │ ├── logo-web.png │ └── res │ │ ├── anim │ │ └── hold.xml │ │ ├── drawable-hdpi │ │ ├── ic_action_download.png │ │ ├── ic_launcher.png │ │ └── ic_share.png │ │ ├── drawable-mdpi │ │ ├── ic_action_download.png │ │ └── ic_share.png │ │ ├── drawable-xhdpi │ │ ├── ic_action_download.png │ │ └── ic_share.png │ │ ├── drawable-xxhdpi │ │ ├── ic_action_download.png │ │ └── ic_share.png │ │ ├── drawable │ │ ├── dot_bg_selector.xml │ │ ├── point_bg_enable.xml │ │ └── point_bg_normal.xml │ │ ├── layout-night │ │ └── item_story.xml │ │ ├── layout │ │ ├── activity_about_me.xml │ │ ├── activity_main.xml │ │ ├── activity_story.xml │ │ ├── item_story.xml │ │ └── item_top_story.xml │ │ ├── menu │ │ ├── menu_main.xml │ │ └── menu_story_content.xml │ │ ├── values-night-v21 │ │ └── styles.xml │ │ ├── values-night │ │ └── styles.xml │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── me │ └── chen_wei │ └── zhihu │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── mine_1.jpg ├── mine_2.jpg ├── off_1.jpg └── off_2.jpg ├── infiniteviewpager ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zanlabs │ │ └── widget │ │ └── infiniteviewpager │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── zanlabs │ │ └── widget │ │ └── infiniteviewpager │ │ ├── InfinitePagerAdapter.java │ │ ├── InfiniteViewPager.java │ │ ├── RecycleBin.java │ │ ├── RecyclingPagerAdapter.java │ │ └── indicator │ │ ├── CirclePageIndicator.java │ │ ├── LinePageIndicator.java │ │ ├── PageIndicator.java │ │ ├── TitlePageIndicator.java │ │ └── UnderlinePageIndicator.java │ └── res │ ├── color │ ├── vpi__dark_theme.xml │ └── vpi__light_theme.xml │ ├── drawable-hdpi │ ├── vpi__tab_selected_focused_holo.9.png │ ├── vpi__tab_selected_holo.9.png │ ├── vpi__tab_selected_pressed_holo.9.png │ ├── vpi__tab_unselected_focused_holo.9.png │ ├── vpi__tab_unselected_holo.9.png │ └── vpi__tab_unselected_pressed_holo.9.png │ ├── drawable-mdpi │ ├── vpi__tab_selected_focused_holo.9.png │ ├── vpi__tab_selected_holo.9.png │ ├── vpi__tab_selected_pressed_holo.9.png │ ├── vpi__tab_unselected_focused_holo.9.png │ ├── vpi__tab_unselected_holo.9.png │ └── vpi__tab_unselected_pressed_holo.9.png │ ├── drawable-xhdpi │ ├── vpi__tab_selected_focused_holo.9.png │ ├── vpi__tab_selected_holo.9.png │ ├── vpi__tab_selected_pressed_holo.9.png │ ├── vpi__tab_unselected_focused_holo.9.png │ ├── vpi__tab_unselected_holo.9.png │ └── vpi__tab_unselected_pressed_holo.9.png │ ├── drawable │ └── vpi__tab_indicator.xml │ └── values │ ├── strings.xml │ ├── vpi__attrs.xml │ ├── vpi__colors.xml │ ├── vpi__defaults.xml │ └── vpi__styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | lt application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Android Studio Navigation editor temp files 29 | .navigation/ 30 | 31 | # Android Studio captures folder 32 | captures/*.iml 33 | .gradle 34 | /local.properties 35 | /.idea/workspace.xml 36 | /.idea/libraries 37 | .DS_Store 38 | /build 39 | /captures 40 | 41 | .idea/ 42 | 43 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | pache 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 | # 知乎日报Android客户端(非官方版) 2 | 欢迎大家`Star`、`Fork`、`Issue` 3 | 4 | ## 声明 5 | ***项目仅供学习使用,请遵循知乎相关规定。如有侵权,告知后必定第一时间删除该项目。*** 6 | 7 | ## 界面效果 8 | ***界面与官方风格相似度极高,遵循Material Design规范*** 9 | 10 | | UI | 非官方 | 官方 | 11 | |----|-------|------| 12 | |主界面|![非官方主界面](/img/mine_2.jpg)|![官方主界面](/img/off_2.jpg)| 13 | |文章界面|![非官方文章界面](/img/mine_1.jpg)|![官方文章界面](/img/off_1.jpg)| 14 | 15 | ## 与官方知乎日报有哪些区别? 16 | * 没有烦人的推送 17 | * 权限极少 18 | * 不会常驻内存 19 | * 体积更小(2MB<5.3MB) 20 | * 给你最纯粹的阅读享受 21 | * ... ... 22 | 23 | ***好吧,我承认这些都是由于时间紧所有没来得及完成。。。*** 24 | 25 | 26 | ## 学到了哪些? 27 | * 本项目参考了[Best practices in Android development](https://github.com/futurice/android-best-practices)给出的一些建议 28 | * Android `MVP` 模式的运用 29 | * 使用`Retrofit`获取RESTful API内容 30 | * `RecyclerView`的常规使用方法(下拉刷新,Endless Scrolling...) 31 | * `ViewPager`滚动效果 32 | * `WebView`加载HTML、CSS文件 33 | * `DayNight Theme`切换 34 | * ... ... 35 | 36 | **本项目适合对Android基础知识有一定了解但未做过项目的同学进行阅读** 37 | 38 | 39 | ## 后续将会增加的功能 40 | * `主题日报` 41 | * 日报内容根据日期进行分组 42 | * 监听网络状态 43 | * ... ... 44 | 45 | *注:由于最近学校事情较多,这些功能还不能及时完成,如有人有兴趣参与到项目中,欢迎PullRequest* 46 | 47 | 48 | ## 感谢 49 | * 感谢[Xiao Liang](https://github.com/izzyleung)分享的知乎日报API分析 50 | * 感谢[Square公司](http://square.github.io/)提供了众多非常优秀的开源库 51 | 52 | ## 关于作者 53 | * Website: [chen-wei.me](http://chen-wei.me) 54 | * Email: [hander_wei@163.com](hander_wei@163.com) 55 | * Github: [https://github.com/HanderWei](https://github.com/HanderWei) 56 | * 中国科学技术大学软件工程研究生在读 57 | 58 | ***现寻Android实习工作一份,如有相关工作机会,欢迎与我联系!*** 59 | 60 | 61 | ## License 62 | The MIT License (MIT) Copyright (c) 2016 Chen Wei 63 | 64 | Permission is hereby granted, free of charge, to any person obtaining a copy of 65 | this software and associated documentation files (the "Software"), to deal in 66 | the Software without restriction, including without limitation the rights to 67 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 68 | of the Software, and to permit persons to whom the Software is furnished to do 69 | so, subject to the following conditions: 70 | 71 | 72 | The above copyright notice and this permission notice shall be included in all 73 | copies or substantial portions of the Software. 74 | 75 | 76 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 77 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 78 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 79 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 80 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 81 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 82 | SOFTWARE. 83 | -------------------------------------------------------------------------------- /ZhihuDaily.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | def gitVersionCode() { 4 | def cmd = 'git rev-list HEAD --first-parent --count' 5 | cmd.execute().text.trim().toInteger() 6 | } 7 | 8 | def gitVersionTag() { 9 | def cmd = 'git describe --tags' 10 | def version = cmd.execute().text.trim() 11 | 12 | def pattern = "-(\\d+)-g" 13 | def matcher = version =~ pattern 14 | 15 | if (matcher) { 16 | version = version.substring(0, matcher.start()) + "." + matcher[0][1] 17 | } else { 18 | version = version + ".0" 19 | } 20 | 21 | return version 22 | } 23 | 24 | android { 25 | compileSdkVersion 23 26 | buildToolsVersion "23.0.1" 27 | 28 | defaultConfig { 29 | applicationId "me.chen_wei.zhihu" 30 | minSdkVersion 16 31 | targetSdkVersion 23 32 | versionCode 1 33 | versionName '1.0' 34 | } 35 | buildTypes { 36 | debug { 37 | // 为了不和 release 版本冲突 38 | applicationIdSuffix ".debug" 39 | } 40 | release { 41 | minifyEnabled false 42 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 43 | } 44 | applicationVariants.all { variant -> 45 | if (variant.buildType.name.equals('release')) { 46 | variant.mergedFlavor.versionCode = gitVersionCode() 47 | variant.mergedFlavor.versionName = gitVersionTag() 48 | } 49 | } 50 | } 51 | } 52 | 53 | dependencies { 54 | compile fileTree(dir: 'libs', include: ['*.jar']) 55 | testCompile 'junit:junit:4.12' 56 | compile 'com.android.support:appcompat-v7:23.3.0' 57 | compile 'com.android.support:design:23.3.0' 58 | 59 | //Network 60 | compile 'com.squareup.picasso:picasso:2.5.2' 61 | compile 'com.squareup.okhttp3:okhttp:3.0.1' 62 | 63 | //Butter Knife 64 | compile 'com.jakewharton:butterknife:7.0.1' 65 | //Event Bus 66 | compile 'de.greenrobot:eventbus:2.4.0' 67 | //Sectioned RecyclerView 68 | compile 'com.truizlop.sectionedrecyclerview:library:1.1.0' 69 | //RecyclerView 70 | compile 'com.android.support:recyclerview-v7:23.1.1' 71 | //CardView 72 | compile 'com.android.support:cardview-v7:23.1.1' 73 | 74 | //Retrofit 75 | compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4' 76 | compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4' 77 | 78 | compile 'com.bartoszlipinski.recyclerviewheader:library:1.2.1' 79 | 80 | debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1 81 | releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1 82 | testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1 83 | 84 | compile project(path: ':infiniteviewpager') 85 | } 86 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Hander/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/me/chen_wei/zhihu/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/Constants.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu; 2 | 3 | /** 4 | * Created by Hander on 16/2/26. 5 | *

6 | * Email : hander_wei@163.com 7 | */ 8 | public class Constants { 9 | 10 | public static final String API_URL = "http://news-at.zhihu.com/"; 11 | 12 | public static final String STORY_URL = "http://daily.zhihu.com/story/"; 13 | 14 | public static final String KEY_STORY_ID = "story_id"; 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/MyApplication.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu; 2 | 3 | import android.app.Application; 4 | import android.support.v7.app.AppCompatDelegate; 5 | 6 | /** 7 | * Created by Hander on 16/2/28. 8 | *

9 | * Email : hander_wei@163.com 10 | */ 11 | public class MyApplication extends Application { 12 | 13 | static{ 14 | //设置DayNightTheme模式 15 | AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO); 16 | } 17 | 18 | @Override 19 | public void onCreate() { 20 | super.onCreate(); 21 | // LeakCanary.install(this); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/event/AllStoriedDownloadedEvent.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.event; 2 | 3 | /** 4 | * Created by Hander on 16/3/2. 5 | *

6 | * Email : hander_wei@163.com 7 | */ 8 | public class AllStoriedDownloadedEvent { 9 | 10 | public AllStoriedDownloadedEvent() { 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/event/ContentsLoadedEvent.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.event; 2 | 3 | import me.chen_wei.zhihu.network.model.Contents; 4 | 5 | /** 6 | * Created by Hander on 16/2/26. 7 | *

8 | * Email : hander_wei@163.com 9 | */ 10 | public class ContentsLoadedEvent { 11 | 12 | public Contents contents; 13 | 14 | public ContentsLoadedEvent(Contents contents){ 15 | this.contents = contents; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/event/LatestContentsLoadedEvent.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.event; 2 | 3 | import me.chen_wei.zhihu.network.model.Contents; 4 | 5 | /** 6 | * Created by Hander on 16/3/2. 7 | *

8 | * Email : hander_wei@163.com 9 | */ 10 | public class LatestContentsLoadedEvent { 11 | 12 | public Contents contents; 13 | 14 | public LatestContentsLoadedEvent(Contents contents) { 15 | this.contents = contents; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/event/LoadContentEvent.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.event; 2 | 3 | /** 4 | * Created by Hander on 16/2/27. 5 | *

6 | * Email : hander_wei@163.com 7 | */ 8 | public class LoadContentEvent { 9 | 10 | public int id; 11 | 12 | public LoadContentEvent(int id) { 13 | this.id = id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/event/LoadFailureEvent.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.event; 2 | 3 | /** 4 | * Created by Hander on 16/2/26. 5 | *

6 | * Email : hander_wei@163.com 7 | */ 8 | public class LoadFailureEvent { 9 | public String msg; 10 | 11 | public LoadFailureEvent(String msg){ 12 | this.msg = msg; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/event/NewsDownloadedEvent.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.event; 2 | 3 | /** 4 | * Created by Hander on 16/3/2. 5 | *

6 | * Email : hander_wei@163.com 7 | */ 8 | public class NewsDownloadedEvent { 9 | 10 | public NewsDownloadedEvent(){ 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/event/NewsLoadedEvent.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.event; 2 | 3 | import me.chen_wei.zhihu.network.model.News; 4 | 5 | /** 6 | * Created by Hander on 16/2/27. 7 | *

8 | * Email : hander_wei@163.com 9 | */ 10 | public class NewsLoadedEvent { 11 | 12 | public News news; 13 | 14 | public NewsLoadedEvent(News news){ 15 | this.news = news; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/event/TopStoriesLoadedEvent.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.event; 2 | 3 | import me.chen_wei.zhihu.network.model.Latest; 4 | 5 | /** 6 | * Created by Hander on 16/2/27. 7 | *

8 | * Email : hander_wei@163.com 9 | */ 10 | public class TopStoriesLoadedEvent { 11 | 12 | public Latest latest; 13 | 14 | public TopStoriesLoadedEvent(Latest latest){ 15 | this.latest = latest; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/api/ZhihuAPI.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.api; 2 | 3 | import me.chen_wei.zhihu.network.model.Contents; 4 | import me.chen_wei.zhihu.network.model.Latest; 5 | import me.chen_wei.zhihu.network.model.News; 6 | import retrofit2.Call; 7 | import retrofit2.http.GET; 8 | import retrofit2.http.Path; 9 | 10 | /** 11 | * Created by Hander on 16/2/26. 12 | *

13 | * Email : hander_wei@163.com 14 | */ 15 | public interface ZhihuAPI { 16 | 17 | /* 18 | * 获取最新故事列表 19 | * 包含热门故事列表(5个) 20 | * 21 | * */ 22 | @GET("api/4/news/latest") 23 | Call getLatestContent(); 24 | 25 | //获取单条新闻内容 26 | @GET("api/4/news/{id}") 27 | Call getNews(@Path("id") int id); 28 | 29 | /* 30 | * 获取指定日期的故事列表 31 | * 32 | * Eg: date = 20160228 33 | * 则获取的是2016年2月27日的故事列表 34 | * */ 35 | @GET("api/4/news/before/{date}") 36 | Call getContents(@Path("date") String date); 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/model/Contents.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by Hander on 16/2/27. 8 | *

9 | * Email : hander_wei@163.com 10 | */ 11 | public class Contents implements Serializable{ 12 | 13 | 14 | /** 15 | * date : 20160225 16 | * stories : [{"images":["http://pic3.zhimg.com/625c29a726de316807851e3af8bfd19a.jpg"],"type":0,"id":7915284,"ga_prefix":"022522","title":"深夜惊奇 · 赌球一时爽"},{"images":["http://pic4.zhimg.com/5a788559ae4069ef2a317154a4e875d7.jpg"],"type":0,"id":7920008,"ga_prefix":"022521","title":"这三部片的共同点是有很多液体,有热、有冷、有黏稠"},{"images":["http://pic2.zhimg.com/9a5c9bec1a56f41b5ad58e8c9c3e518d.jpg"],"type":0,"id":7875647,"ga_prefix":"022520","title":"这首叮当咣当的校铃,最早可能出自爱我中华的神剧"},{"images":["http://pic2.zhimg.com/291814ed80c1e407f8a0f5b67bd71339.jpg"],"type":0,"id":7902359,"ga_prefix":"022519","title":"为什么鼻子和嘴巴联通?这还要从鱼类说起\u2026\u2026"},{"images":["http://pic3.zhimg.com/191cde8bdbb36aa315d196546910ee66.jpg"],"type":0,"id":7918337,"ga_prefix":"022518","title":"男性在网络上的购买欲真的弱于女性吗?"},{"images":["http://pic3.zhimg.com/071b58fcf2d35444b749e40baf00c57a.jpg"],"type":0,"id":7888997,"ga_prefix":"022517","title":"我的专业是人类学,平时会去部落里当个女王啥的(误)"},{"images":["http://pic2.zhimg.com/aed859a292250f66690c80824e905315.jpg"],"type":0,"id":7919463,"ga_prefix":"022516","title":"「白米饭比可乐还容易让人血糖高」,还能吃米饭吗?"},{"images":["http://pic3.zhimg.com/aae31fef75f4d67e89c9a406d865199a.jpg"],"type":0,"id":7918982,"ga_prefix":"022515","title":"「我以为你都懂,可是你没有」"},{"images":["http://pic3.zhimg.com/eb526023315b1b74c3895fb6597ad10e.jpg"],"type":0,"id":7918297,"ga_prefix":"022514","title":"没错,一张「毛爷爷」一旦拆散,就花得更快了"},{"images":["http://pic1.zhimg.com/026fe4dd95ad206e1fefa594b9c1d9f8.jpg"],"type":0,"id":7918246,"ga_prefix":"022513","title":"萝卜只卖一角钱,居然因为太便宜被罚一万块"},{"images":["http://pic3.zhimg.com/cc60329095ece019f97ab68e6695396e.jpg"],"type":0,"id":7910806,"ga_prefix":"022512","title":"如何看待「日本专家炮轰中国新能源车政策」?"},{"images":["http://pic2.zhimg.com/05320e4ed8d74ba7fe2e9d544c63d121.jpg"],"type":0,"id":7918261,"ga_prefix":"022511","title":"为什么有的人喜欢责备别人并且得理不饶人?"},{"images":["http://pic2.zhimg.com/942e0d44ed0c5cf340ac7988d8b173b5.jpg"],"type":0,"id":7910403,"ga_prefix":"022510","title":"花钱支持歌手,这几个公司可能都会谢谢你"},{"images":["http://pic4.zhimg.com/d1bd811467ddff55f9640f04756d3173.jpg"],"type":0,"id":7884280,"ga_prefix":"022508","title":"都别争了,选爱我的还是我爱的有科学解释了"},{"images":["http://pic1.zhimg.com/dd912e5217e6c6f26ac93182a86893d8.jpg"],"type":0,"id":7915946,"ga_prefix":"022507","title":"《功夫熊猫 3》是中美合拍,负责人说中方主要做了这些"},{"images":["http://pic1.zhimg.com/1e87e49386f4c6161f8704e168b60948.jpg"],"type":0,"id":7917099,"ga_prefix":"022507","title":"外国人爱用的这些 app,中国人怎么也想不到"},{"images":["http://pic4.zhimg.com/997afbf82c668046d647fa5a78cf500f.jpg"],"type":0,"id":7914881,"ga_prefix":"022507","title":"这项规定一出台,苹果用户心里有点慌"},{"images":["http://pic1.zhimg.com/bb20b715b634f49322d3050c5e5b28cc.jpg"],"type":0,"id":7917162,"ga_prefix":"022507","title":"读读日报 24 小时热门:在战火纷飞的地道里走私 KFC"},{"images":["http://pic2.zhimg.com/722d059ececb8be03295a10d359f3e8d.jpg"],"type":0,"id":7907685,"ga_prefix":"022506","title":"瞎扯 · 多管闲事与不懂礼貌"}] 17 | */ 18 | 19 | private String date; 20 | /** 21 | * images : ["http://pic3.zhimg.com/625c29a726de316807851e3af8bfd19a.jpg"] 22 | * type : 0 23 | * id : 7915284 24 | * ga_prefix : 022522 25 | * title : 深夜惊奇 · 赌球一时爽 26 | */ 27 | 28 | private List stories; 29 | 30 | public void setDate(String date) { 31 | this.date = date; 32 | } 33 | 34 | public void setStories(List stories) { 35 | this.stories = stories; 36 | } 37 | 38 | public String getDate() { 39 | return date; 40 | } 41 | 42 | public List getStories() { 43 | return stories; 44 | } 45 | 46 | public static class StoriesEntity implements Serializable{ 47 | private int type; 48 | private int id; 49 | private String ga_prefix; 50 | private String title; 51 | private List images; 52 | 53 | public void setType(int type) { 54 | this.type = type; 55 | } 56 | 57 | public void setId(int id) { 58 | this.id = id; 59 | } 60 | 61 | public void setGa_prefix(String ga_prefix) { 62 | this.ga_prefix = ga_prefix; 63 | } 64 | 65 | public void setTitle(String title) { 66 | this.title = title; 67 | } 68 | 69 | public void setImages(List images) { 70 | this.images = images; 71 | } 72 | 73 | public int getType() { 74 | return type; 75 | } 76 | 77 | public int getId() { 78 | return id; 79 | } 80 | 81 | public String getGa_prefix() { 82 | return ga_prefix; 83 | } 84 | 85 | public String getTitle() { 86 | return title; 87 | } 88 | 89 | public List getImages() { 90 | return images; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/model/Latest.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by Hander on 16/2/26. 8 | *

9 | * Email : hander_wei@163.com 10 | */ 11 | public class Latest implements Serializable{ 12 | 13 | /** 14 | * date : 20160226 15 | * stories : [{"images":["http://pic1.zhimg.com/f7c1de18d969350a86774230ab4a00b4.jpg"],"type":0,"id":7923064,"ga_prefix":"022619","title":"为什么近视的人不戴眼镜会听不清别人在说什么?"},{"images":["http://pic2.zhimg.com/1d5535f5becd198a26a5e4474c2f45cd.jpg"],"type":0,"id":7924329,"ga_prefix":"022618","title":"如果你身边有人想自杀,先看见并承认他的痛苦"},{"title":"穿着情趣制服招摇过市,这是我的第一次 Cosplay","ga_prefix":"022617","images":["http://pic3.zhimg.com/7701cb73c08c6bfe7eecb3c3b6752dde.jpg"],"multipic":true,"type":0,"id":7924059},{"images":["http://pic1.zhimg.com/6a7b6d2ce2543dece406c59bdb0ed0c0.jpg"],"type":0,"id":7919743,"ga_prefix":"022616","title":"比尔 · 盖茨夫妇写了封年度公开信,想要「改变世界的超能力」"},{"title":"简单来说,我的工作就是让你「看到」声音","ga_prefix":"022615","images":["http://pic3.zhimg.com/7db93f371bca7a18ad8c3c7ff53c130e.jpg"],"multipic":true,"type":0,"id":7923282},{"title":"为了好吃的涮羊肉,我一定要学会这些挑肉的方法","ga_prefix":"022614","images":["http://pic2.zhimg.com/e4419de35b1471985bf78745a28968b9.jpg"],"multipic":true,"type":0,"id":7912102},{"images":["http://pic3.zhimg.com/2799867f89aa16fe81ff8c1c840097fa.jpg"],"type":0,"id":7914085,"ga_prefix":"022613","title":"「错的不是我,是世界」,这里有了合理的解释"},{"images":["http://pic1.zhimg.com/b81e4222190f32a1cf0bde205be8d550.jpg"],"type":0,"id":7910087,"ga_prefix":"022612","title":"全世界最稀有的植物,我猜你们一定想听点新的"},{"images":["http://pic2.zhimg.com/0133a45257c460b2736dbe0fb8045c61.jpg"],"type":0,"id":7919331,"ga_prefix":"022611","title":"只要你用的是这五大银行,以后手机转账都免手续费"},{"images":["http://pic2.zhimg.com/668300e97c69ecc5b7c8f756505c762d.jpg"],"type":0,"id":7921049,"ga_prefix":"022610","title":"给艺人贴标签有时候是必要的,像王若琳这样的是异类"},{"images":["http://pic3.zhimg.com/b989d608c59d7156bc24d941fb8747ca.jpg"],"type":0,"id":7918511,"ga_prefix":"022609","title":"咦,为什么国外名校同学的成绩都那么好看?"},{"images":["http://pic3.zhimg.com/95dadb9e9fa901d9edc6d1e57211dab6.jpg"],"type":0,"id":7921365,"ga_prefix":"022608","title":"都上小学了还会尿床\u2026\u2026"},{"images":["http://pic2.zhimg.com/53b19dd1f7b6617f6b2a60a8fa088379.jpg"],"type":0,"id":7920604,"ga_prefix":"022607","title":"跟国外相比,北上广深成杭武南人口密度还不够大"},{"images":["http://pic1.zhimg.com/5e5521af241f9b77a1af501b70c8d314.jpg"],"type":0,"id":7920157,"ga_prefix":"022607","title":"从婴儿爽身粉致癌案谈起,为什么不推荐用爽身粉"},{"images":["http://pic4.zhimg.com/afce9eb8e6bd613c582ffd231425e8b3.jpg"],"type":0,"id":7919280,"ga_prefix":"022607","title":"留学之后你都形成了什么「坏习惯」?"},{"images":["http://pic3.zhimg.com/c04b5715b7a55d06b0ead498a6bef8d2.jpg"],"type":0,"id":7921768,"ga_prefix":"022607","title":"读读日报 24 小时热门:小李拿不到奥斯卡活该?求这烂梗快消失"},{"images":["http://pic3.zhimg.com/33459fa23c930a66f4dfe8de432a8106.jpg"],"type":0,"id":7900419,"ga_prefix":"022606","title":"瞎扯 · 如何正确地吐槽"}] 16 | * top_stories : [{"image":"http://pic1.zhimg.com/a18faea79ae2a851f2c6927e5d4b4f48.jpg","type":0,"id":7924059,"ga_prefix":"022617","title":"穿着情趣制服招摇过市,这是我的第一次 Cosplay"},{"image":"http://pic2.zhimg.com/06fdfa6a49051bf161f4703ed17657b5.jpg","type":0,"id":7919743,"ga_prefix":"022616","title":"比尔 · 盖茨夫妇写了封年度公开信,想要「改变世界的超能力」"},{"image":"http://pic3.zhimg.com/39c3b94db27347a3e3c72a3b5b375d4a.jpg","type":0,"id":7918511,"ga_prefix":"022609","title":"咦,为什么国外名校同学的成绩都那么好看?"},{"image":"http://pic2.zhimg.com/6d03c693d157cbf16966d8a929ad58d1.jpg","type":0,"id":7920157,"ga_prefix":"022607","title":"从婴儿爽身粉致癌案谈起,为什么不推荐用爽身粉"},{"image":"http://pic1.zhimg.com/82b3103761f9374ed0f1b2d6ffa0c134.jpg","type":0,"id":7920604,"ga_prefix":"022607","title":"跟国外相比,北上广深成杭武南人口密度还不够大"}] 17 | */ 18 | 19 | private String date; 20 | /** 21 | * images : ["http://pic1.zhimg.com/f7c1de18d969350a86774230ab4a00b4.jpg"] 22 | * type : 0 23 | * id : 7923064 24 | * ga_prefix : 022619 25 | * title : 为什么近视的人不戴眼镜会听不清别人在说什么? 26 | */ 27 | 28 | private List stories; 29 | /** 30 | * image : http://pic1.zhimg.com/a18faea79ae2a851f2c6927e5d4b4f48.jpg 31 | * type : 0 32 | * id : 7924059 33 | * ga_prefix : 022617 34 | * title : 穿着情趣制服招摇过市,这是我的第一次 Cosplay 35 | */ 36 | 37 | private List top_stories; 38 | 39 | public void setDate(String date) { 40 | this.date = date; 41 | } 42 | 43 | public void setStories(List stories) { 44 | this.stories = stories; 45 | } 46 | 47 | public void setTop_stories(List top_stories) { 48 | this.top_stories = top_stories; 49 | } 50 | 51 | public String getDate() { 52 | return date; 53 | } 54 | 55 | public List getStories() { 56 | return stories; 57 | } 58 | 59 | public List getTop_stories() { 60 | return top_stories; 61 | } 62 | 63 | public static class StoriesEntity implements Serializable{ 64 | private int type; 65 | private int id; 66 | private String ga_prefix; 67 | private String title; 68 | private List images; 69 | 70 | public void setType(int type) { 71 | this.type = type; 72 | } 73 | 74 | public void setId(int id) { 75 | this.id = id; 76 | } 77 | 78 | public void setGa_prefix(String ga_prefix) { 79 | this.ga_prefix = ga_prefix; 80 | } 81 | 82 | public void setTitle(String title) { 83 | this.title = title; 84 | } 85 | 86 | public void setImages(List images) { 87 | this.images = images; 88 | } 89 | 90 | public int getType() { 91 | return type; 92 | } 93 | 94 | public int getId() { 95 | return id; 96 | } 97 | 98 | public String getGa_prefix() { 99 | return ga_prefix; 100 | } 101 | 102 | public String getTitle() { 103 | return title; 104 | } 105 | 106 | public List getImages() { 107 | return images; 108 | } 109 | } 110 | 111 | public static class TopStoriesEntity implements Serializable{ 112 | private String image; 113 | private int type; 114 | private int id; 115 | private String ga_prefix; 116 | private String title; 117 | 118 | public void setImage(String image) { 119 | this.image = image; 120 | } 121 | 122 | public void setType(int type) { 123 | this.type = type; 124 | } 125 | 126 | public void setId(int id) { 127 | this.id = id; 128 | } 129 | 130 | public void setGa_prefix(String ga_prefix) { 131 | this.ga_prefix = ga_prefix; 132 | } 133 | 134 | public void setTitle(String title) { 135 | this.title = title; 136 | } 137 | 138 | public String getImage() { 139 | return image; 140 | } 141 | 142 | public int getType() { 143 | return type; 144 | } 145 | 146 | public int getId() { 147 | return id; 148 | } 149 | 150 | public String getGa_prefix() { 151 | return ga_prefix; 152 | } 153 | 154 | public String getTitle() { 155 | return title; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/model/News.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.model; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by Hander on 16/2/27. 8 | *

9 | * Email : hander_wei@163.com 10 | */ 11 | public class News implements Serializable { 12 | 13 | /** 14 | * body :

15 |
16 | 17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 |

ZARA、H&M、GAP 和优衣库各自的竞争优势是什么?

30 | 31 |
32 | 33 |
34 | 35 | 曹暘暘 36 |
37 | 38 |
39 |

我上个月刚刚在米兰参加了一个 ZARA 的买手和 Merchandising 的面试,终于看到了一个和自己专业相关的。本来已经很困了,看到这个问题立马来了精神。

40 |

先说 ZARA,四个牌子里面最强势的,也是当前最成功的但是最特立独行的,因此可能需要多废点话。我想说,ZARA 的品牌核心竞争力是目前世界上所有时尚品牌都模仿不了的。且不说规模,毕竟规模人人都有可能做到,这里着重说它的不可复制的模式。

41 |

ZARA 采用的模式叫做 Vertical Integration,垂直出货。极大地缩短了出货时间:平均为 2 周,因此以 ZARA 为代表的快时尚品牌一年可以有 15-20 个 Collection。与之相比,普通的品牌出货的整个流程需要 4 至 6 个月,一年一般只有两个 Collection。但是,由于采用了 Vertical Intergation 模式,ZARA 相对于其他快时尚品牌能更好更快地控制整个流程(从市场调研,到设计,打板,制作样衣,批量生产,运输,零售),比同样以出货速度著称的 H&M,快了 5 天。为了追求快,ZARA 可谓牺牲了很多的成本:

42 |

1.在生产流程中,ZARA 依靠总部所在的拉科鲁尼亚的无数手工作坊,家庭工厂起家,很多产品直接在当地生产,直到最近几年才逐渐外包,然而 H&M 前些年有 75% 的产品在亚洲制造,现在已经将生产全部外包。然而也因为这个原因,H&M 的价格大约为 ZARA 的 50%-70%(暂且不考虑原材料成本)

43 |

2.所有的远程运输都是飞机,而不用货船,甘愿支付高额的运费而不愿意花费广告费和市场营销的费用,ZARA 的市场营销费用只占总成本的 0.3%-0.4%,然而其他品牌大约占 3%-4%。纵使花费了高昂的成本去追求快,ZARA 的毛利率和净利率仍然和 H&M 不相上下,同时 ZARA 也不愿为了提高利润率去节省上述成本。因此,ZARA 达到了所有时尚品牌和零售商都前所未有高度(我在米兰的老师从来不将 ZARA 称作品牌,因为它更着重于生产和零售环节,从未用设计去定位品牌产品的风格,也并没有一个时装品牌应拥有的 Brand Identity):

44 |

1. ZARA 总部仓库里的所有衣服不会停留超过三天,店铺每周会向总部下单两次以补充产品,存货周转率比其他品牌高 3-4 倍

45 |

2.平均每季只有 15% 的衣服需要打折出售,其他品牌则为 50%。

46 |

3. 顾客平均一年去 ZARA17 次,其他品牌只有 4 次 。

47 |

同时,ZARA 的快也归功于他们“倒过来”的设计概念。在我参加 ZARA 面试的时候,HR 给我们讲,ZARA 的核心,是店铺,因为只有在店铺才能真正接触到顾客,才能了解顾客的需求。因此,店铺提供销售数据,再将其递交给店面经理,店面经理整理完毕后将结果交给设计部门,设计部分按照顾客需求设计出款式,再将其递交给商业部门去评估成本和价格,之后开始打板,样衣制作,在移交给工厂生产,最后存放于 ZARA 超级大的物流仓库(是亚马逊的 9 倍),仓库门口都会有无数的货车每天两次将产品运输到欧洲其他地区或者机场。在这个流程中,单就设计而言,平均 20 分钟设计出一件衣服,每年可以设计出 2 万 5 千件以上的新款,是 H&M 的 4-6 倍。因为顾客对于时尚的需求是变化的,从店铺收集的资料是具有时效性的,因此,快才是这一模式最根本的也是最重要的制胜法宝。

48 |

正是因为 ZARA 这一特立独行的模式,才使得其余现有品牌完全无法效仿,因为如果效仿就意味着品牌的设计师们不再对设计起决定性作用,甚至需要重建设计师团队,物流系统,生产流程等等。

49 |

但是这一模式也有着弊端:

50 |

1. 因为对于全部流程的掌控,使得运营风险增加,如果一旦出现经济衰落或者行业不景气,无法将压力转移给供货商(比如要求供货商降价……)

51 |

2. 无法整合各国优势,实现利益最大化。

52 |

3. 店铺被品牌直接管理,无法通过代理等形式快速扩张(比如意大利的贝纳通),并且财产也有一部分需要投资于新店铺和已有店铺的翻新整修,降低了资产周转率(Zara 的 Assets Turnover 为 1.4,GAP 为 1.8,H&M 为 2)

53 |

4. 众所周知,抄袭问题,Zara 已经是明着抄了很多年,你告我就告,官司输了就赔你钱,反正我都能挣回来(不像美国的 Forever21 被控告之后搞得沸沸扬扬……)

54 |

总之,Zara 创造的是一个全新的商业模式,一个完完全全基于顾客需求的商业模式。因此,Zara 目前的敌人,只有它自己。只有完全认识并控制住利弊,才能得到长久稳定而持续的发展。

55 |

-------------------------

56 |

然后是 GAP,我只想用一句话来形容它现在的处境:瘦死的骆驼比马大。作为曾经的时装界的销量霸主,GAP 居然在 2014 年被福布斯评为未来十年最后可能消失的十个时装品牌之一(你没有看错,还有最近在国内大火的美国的 AF,我那天放学回家室友就幸灾乐祸地跟我说的),GAP 今年刚刚找到我们专业帮助他们做一个项目,在意大利做品牌推广的,接了这个项目之后也查阅了很多关于 GAP 的资料。这里既然问到竞争优势,那么就不需要提商业模式了,倒也省了很多废话。

57 |

从设计来说,提到 GAP,往往都会想到那些简简单单的款式,牛仔裤,印着 LOGO 的上衣。但是今年 GAP 的广告充分显示了 GAP 以不变应万变的宗旨,GAP 的 Marketing Department Manager 来做 presentation 的时候说,GAP 是希望可以用简单的款式来塑造属于每个人的风格。但是如果看 GAP 的 facebook 和 instagram,你就会发现完全走的是小清新路线,其中很多设计还是比较符合 WGSN 上的潮流预测中的简约时尚。虽然最近几年 GAP 由于缺少固定风格难以满足不断求新求异的青少年消费群体,但是因为简约百搭的设计和优质的面料以及版型(尤其是裤子,我大爱),还是赢得了不少忠实消费者的青睐,再加上乘上了简约功能性为主导的潮流风格,使得设计方面算是勉强找到了一个灵魂归宿。在此奉上 GAP 的 Instagram 小清新截图一张。

58 |

另外,似乎知道了 Zara 和 H&M 一直被外界冠以环境污染的罪名,GAP 的联合创始人 Doris 和 Don Fisher 有意发起了“DO MORE THAN SELL CLOTHES”的倡议,似乎更要采用有机棉去促进农业可持续性生产,希望能够以品牌的社会使命唤起人们的品牌意识,重新树立品牌形象。

59 |

最后,采用那个 Manager 在做 presentation 时候的原话吧,GAP is confident not boastful; simple not boring; optimistic not delusional; courageous not radical; inclusive not lofty; youthful spirit not young; smart not smart-ass; current not trendy; classic not conservative; accessible not exclusive; liberating not revolutionary; human not heroic. 这几句话算是非常全面地体现了 GAP 的品牌定位和形象了。

60 |

-------------------------

61 |

 H&M,另一个巨人。同样十分优秀的 fast fashion 品牌,在模式上面来说,更加倾向于兼顾出货时间和产品成本,因此速度不及 Zara,但是依靠着成本领先优势也在这一行业占有一席之地。时尚程度也不及 Zara,毕竟人家 Zara 也是抄大牌啊,而且抄到了精髓。H&M 是欧洲这边屌丝青年的首选,最近的设计偏街头,什么大印花,牛仔,大字母,大迷彩……产品缺点也很明显,质量不敢恭维,面料算是这四个品牌里面最差劲得了(洗过一次就知道了)。但是和 Zara 一样,都是把服装从耐用消费品变革为快速消费品的革新者,因此质量也就不那么重要了。

62 |

其中 H&M 最出彩的地方,当属运动衣,这个系列在国内不卖(至少我出国的时候国内还没有),估计人家觉得中国人不爱运动。其实 H&M 的运动衣做的很专业,分类很细(有跑步,网球,甚至还有瑜伽),价格便宜,比阿迪耐克便宜不是一点,因此大受欢迎,销量很出彩。下面的图是 H&M 运动衣在 2013 年的 Replenishment Rate(补货率?不知道该怎么翻译),达到了 36%,最后打折出售的只占 10.4%(Zara 的存货周转率这么高也还有 15%需要打折出售),相比之下运动衣绝对是 H&M 的一大亮点。

63 |

64 |

另外一点,H&M 在尺码的选择上有更多选择,甚至还有很多是为了准妈妈而量身定制的。下图是英国的 H&M 的附加尺码的统计和当地主要竞争品牌的对比:

65 |

66 |

因此,可以说 H&M 和 Zara 是各自在不同的方面满足顾客需求,前者是在日常穿着和使用上,后者是在设计上。在运营上面,H&M 注重低成本,Zara 注重产品更新速度。

67 |

-------------------------

68 |

最后一个,优衣库。这是我在国内最喜欢的品牌,便宜,舒适,面料优质,款式简单容易搭配,后来增加了 UT 系列,我曾经一个夏天买了他们家 13 件 T 恤,就是为了不同的艺术家所创作的图案,有的特别喜欢的甚至都不舍得穿。在给 GAP 做 Competitor Analysis 的时候我第一个想到的就是优衣库。我曾经因为喜欢优衣库而买了两本柳井正的书以及他推荐的日本管理和经济学家大前研一的书,均获益匪浅。

69 |

如果要说优衣库的核心竞争力,有几项不得不提:

70 |

1. 面料:这和优衣库的历史有关系,当年火遍全日本并且至今仍然每年秋冬都会推出新款的抓绒外套让优衣库尝到了因为面料而带来的甜头,自此优衣库在面料的使用和研发上不断创新,比如内蒙古的一个羊毛牧场专门饲养给优衣库提供面料的羊,以及轻薄的 Heattech 系列保暖内衣,均是优衣库对于优质面料不断追求的产物。

71 |

2. 服务:这个不需要多说了,我一个朋友曾经在优衣库上班,所有的你身边经过的顾客全要问好,一天下来口干舌燥。单这一点对顾客打招呼的要求就不是别的品牌所能比的。别的服务大家也都能体会得到。

72 |

3. 细节:和无印良品一样,店铺的细节要求到极致,所有衣服叠放的方式均是十分讲究,比如可以使顾客很容易就看到裤腿宽窄的叠法。另一个细节我不得不说,相信很多人都忽略了,就是镜子。在国内,优衣库的镜子照出来的人和 GAP,H&M 以及 Zara 的镜子照出来的效果完全不一样。其中一部分是因为镜子的摆放,另一部分是因为灯光。因为黄种人轮廓,尤其是脸部轮廓不深,相比于欧美人更加“平”,因此其实黄种人并不适合灯光直接从上面打下来的照出来的效果,那样会将脸部轮廓的缺陷通过阴影完全展现出来,然而轮廓较深的欧美人则可以展现出立体的骨骼轮廓。因此,在照镜子的时候灯光从前面或者后面斜照下来更适用于黄种人。同时灯光的选择也恰到好处,不黄,不刺眼,光线柔和,可以烘托甚至美化出肤质。

73 |

4. 工艺,我曾经认真观察了优衣库的衬衣工艺,胸前袋的缝纫针迹倒三角完全是西服的工艺要求,这个细节都被要求了,还有什么理由质疑优衣库的工艺呢?

74 |

5. UT 系列。T 恤本来就是传达文化和艺术的最简单直接的方式,UT 这个系列是和艺术家的合作,讲艺术的图案直接展现在 T 恤上,还原 T 恤原本的作用,并且将便宜的产品卖出了一丝艺术品的味道。

75 |

-------------------------

76 |

不知不觉已经打了三个小时,最后再说一点吧。其实这四个品牌之所以能够屹立时装行业,还是因为找到了不同的定位和优势并将其发扬光大,无论是对成本追求的 H&M,还是对更新速度追求的 Zara,亦或是对细节和质量不懈追求的优衣库,都是相信并坚定着自己,方才能够做到今日的规模,目前 GAP 正处在重新定位阶段,并开始逐渐转型,寻找新的自己,因为以前的优势已经都被 Zara,H&M 和优衣库不同程度的覆盖,并且发展出来了新的优势。对于其余的相似的品牌,比如 C&A,Topshop, 贝纳通,OVS,Forever21 等等,都或多或少地有这些品牌的影子,但是由于都基于不同的地区发展,利用地域优势和本土风格来获得市场的认可。相信未来,在良性的市场竞争中,这些品牌都会基于不同的产品定位和用户需求,用便宜的价格,让更多的人享受到时尚带给人们的乐趣和自信。

77 |
78 |
79 | 80 | 81 | 82 | 83 |
84 | 85 | 86 |
87 |
88 | * image_source : Angel Abril Ruiz / CC BY 89 | * title : 卖衣服的新手段:把耐用品变成「不停买新的」 90 | * image : http://p4.zhimg.com/30/59/30594279d368534c6c2f91b2c00c7806.jpg 91 | * share_url : http://daily.zhihu.com/story/3892357 92 | * js : [] 93 | * ga_prefix : 050615 94 | * type : 0 95 | * id : 3892357 96 | * css : ["http://news-at.zhihu.com/css/news_qa.auto.css?v=77778"] 97 | */ 98 | 99 | private String body; 100 | private String image_source; 101 | private String title; 102 | private String image; 103 | private String share_url; 104 | private String ga_prefix; 105 | private int type; 106 | private int id; 107 | private List js; 108 | private List css; 109 | 110 | public void setBody(String body) { 111 | this.body = body; 112 | } 113 | 114 | public void setImage_source(String image_source) { 115 | this.image_source = image_source; 116 | } 117 | 118 | public void setTitle(String title) { 119 | this.title = title; 120 | } 121 | 122 | public void setImage(String image) { 123 | this.image = image; 124 | } 125 | 126 | public void setShare_url(String share_url) { 127 | this.share_url = share_url; 128 | } 129 | 130 | public void setGa_prefix(String ga_prefix) { 131 | this.ga_prefix = ga_prefix; 132 | } 133 | 134 | public void setType(int type) { 135 | this.type = type; 136 | } 137 | 138 | public void setId(int id) { 139 | this.id = id; 140 | } 141 | 142 | public void setJs(List js) { 143 | this.js = js; 144 | } 145 | 146 | public void setCss(List css) { 147 | this.css = css; 148 | } 149 | 150 | public String getBody() { 151 | return body; 152 | } 153 | 154 | public String getImage_source() { 155 | return image_source; 156 | } 157 | 158 | public String getTitle() { 159 | return title; 160 | } 161 | 162 | public String getImage() { 163 | return image; 164 | } 165 | 166 | public String getShare_url() { 167 | return share_url; 168 | } 169 | 170 | public String getGa_prefix() { 171 | return ga_prefix; 172 | } 173 | 174 | public int getType() { 175 | return type; 176 | } 177 | 178 | public int getId() { 179 | return id; 180 | } 181 | 182 | public List getJs() { 183 | return js; 184 | } 185 | 186 | public List getCss() { 187 | return css; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/processor/ContentsProcessor.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.processor; 2 | 3 | import android.content.Context; 4 | 5 | import de.greenrobot.event.EventBus; 6 | import me.chen_wei.zhihu.Constants; 7 | import me.chen_wei.zhihu.cache.ACache; 8 | import me.chen_wei.zhihu.event.ContentsLoadedEvent; 9 | import me.chen_wei.zhihu.event.LatestContentsLoadedEvent; 10 | import me.chen_wei.zhihu.event.LoadFailureEvent; 11 | import me.chen_wei.zhihu.event.TopStoriesLoadedEvent; 12 | import me.chen_wei.zhihu.network.api.ZhihuAPI; 13 | import me.chen_wei.zhihu.network.model.Contents; 14 | import me.chen_wei.zhihu.network.model.Latest; 15 | import me.chen_wei.zhihu.util.NetworkUtil; 16 | import retrofit2.Call; 17 | import retrofit2.Callback; 18 | import retrofit2.Response; 19 | import retrofit2.Retrofit; 20 | import retrofit2.converter.gson.GsonConverterFactory; 21 | 22 | /** 23 | * Created by Hander on 16/2/26. 24 | *

25 | * Email : hander_wei@163.com 26 | */ 27 | public class ContentsProcessor implements IContentsProcessor { 28 | 29 | Retrofit retrofit; 30 | 31 | @Override 32 | public void getContents(final Context context, final String dateStr) { 33 | final ACache cache = ACache.get(context); 34 | Contents contents; 35 | if (!NetworkUtil.isConnected(context)) { 36 | contents = readContentsFromCache(cache, dateStr); 37 | EventBus.getDefault().post(new ContentsLoadedEvent(contents)); 38 | } else { 39 | //从Cache中获取了文章列表 40 | retrofit = new Retrofit.Builder().baseUrl(Constants.API_URL).addConverterFactory(GsonConverterFactory.create()).build(); 41 | 42 | ZhihuAPI zhihuAPI = retrofit.create(ZhihuAPI.class); 43 | Call call = zhihuAPI.getContents(dateStr); 44 | call.enqueue(new Callback() { 45 | @Override 46 | public void onResponse(Call call, Response response) { 47 | //利用EventBus通知Presenter内容已经下载完成 48 | EventBus.getDefault().post(new ContentsLoadedEvent(response.body())); 49 | putContentsToCache(cache, dateStr, response.body()); 50 | } 51 | 52 | @Override 53 | public void onFailure(Call call, Throwable t) { 54 | //利用EventBus通知Presenter加载失败 55 | EventBus.getDefault().post(new LoadFailureEvent("文章列表加载失败")); 56 | } 57 | }); 58 | } 59 | 60 | } 61 | 62 | /** 63 | * 获取最新文章列表 64 | * 65 | * @param dateStr 66 | */ 67 | @Override 68 | public void getLatestContents(final Context context, final String dateStr) { 69 | final ACache cache = ACache.get(context); 70 | 71 | //从Cache中获取了文章列表 72 | retrofit = new Retrofit.Builder().baseUrl(Constants.API_URL).addConverterFactory(GsonConverterFactory.create()).build(); 73 | 74 | ZhihuAPI zhihuAPI = retrofit.create(ZhihuAPI.class); 75 | Call call = zhihuAPI.getContents(dateStr); 76 | call.enqueue(new Callback() { 77 | @Override 78 | public void onResponse(Call call, Response response) { 79 | //利用EventBus通知Presenter内容已经下载完成 80 | EventBus.getDefault().post(new LatestContentsLoadedEvent(response.body())); 81 | 82 | putContentsToCache(cache, dateStr, response.body()); 83 | } 84 | 85 | @Override 86 | public void onFailure(Call call, Throwable t) { 87 | //利用EventBus通知Presenter加载失败 88 | EventBus.getDefault().post(new LoadFailureEvent("文章列表加载失败")); 89 | } 90 | }); 91 | 92 | } 93 | 94 | /** 95 | * 从Cache中读取某一天的文章列表 96 | * 97 | * @param cache 98 | * @param dateString 99 | * @return 100 | */ 101 | public Contents readContentsFromCache(ACache cache, String dateString) { 102 | Contents contents = (Contents) cache.getAsObject(dateString); 103 | return contents; 104 | } 105 | 106 | /** 107 | * 将文章列表保存在Cache中(保存两周) 108 | * 109 | * @param cache 110 | * @param dateString 111 | * @param contents 112 | */ 113 | public void putContentsToCache(ACache cache, String dateString, Contents contents) { 114 | cache.put(dateString, contents, ACache.TIME_DAY * 14); 115 | } 116 | 117 | @Override 118 | public void getTopStories(Context context) { 119 | getTopStories(context, false); 120 | } 121 | 122 | /** 123 | * 获取热门文章列表 124 | * 125 | * @param context 126 | * @param refresh 是否需要刷新 127 | */ 128 | @Override 129 | public void getTopStories(Context context, boolean refresh) { 130 | final ACache cache = ACache.get(context); 131 | 132 | Latest latest; 133 | if (!refresh && (latest = getLatestFromCache(cache)) != null) { 134 | EventBus.getDefault().post(new TopStoriesLoadedEvent(latest)); 135 | } else { 136 | retrofit = new Retrofit.Builder().baseUrl(Constants.API_URL).addConverterFactory(GsonConverterFactory.create()).build(); 137 | 138 | ZhihuAPI zhihuAPI = retrofit.create(ZhihuAPI.class); 139 | 140 | Call call = zhihuAPI.getLatestContent(); 141 | call.enqueue(new Callback() { 142 | @Override 143 | public void onResponse(Call call, Response response) { 144 | //利用EventBus通知Presenter内容已经下载完成 145 | EventBus.getDefault().post(new TopStoriesLoadedEvent(response.body())); 146 | 147 | putLatestToCache(cache, response.body()); 148 | } 149 | 150 | @Override 151 | public void onFailure(Call call, Throwable t) { 152 | //利用EventBus通知Presenter加载失败 153 | EventBus.getDefault().post(new LoadFailureEvent("热门文章列表加载失败")); 154 | } 155 | }); 156 | } 157 | } 158 | 159 | /** 160 | * 将Latest保存到Cache中 161 | * 162 | * @param cache 163 | * @return 164 | */ 165 | public Latest getLatestFromCache(ACache cache) { 166 | Latest latest = (Latest) cache.getAsObject(Integer.toString(1)); 167 | return latest; 168 | } 169 | 170 | /** 171 | * 保存Latest信息(保存三天) 172 | * 173 | * @param cache 174 | * @param latest 175 | */ 176 | public void putLatestToCache(ACache cache, Latest latest) { 177 | cache.put(String.valueOf(1), latest, ACache.TIME_DAY * 3); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/processor/IContentsProcessor.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.processor; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Created by Hander on 16/2/26. 7 | *

8 | * Email : hander_wei@163.com 9 | */ 10 | public interface IContentsProcessor { 11 | 12 | /** 13 | * 获取某一天的文章列表 14 | * 15 | * @param context 16 | * @param dateStr 17 | */ 18 | void getContents(Context context, String dateStr); 19 | 20 | /** 21 | * 获取最新文章列表 22 | * 23 | * @param context 24 | * @param dateStr 25 | */ 26 | void getLatestContents(Context context, String dateStr); 27 | 28 | /** 29 | * 获取热门文章列表 30 | * 31 | * @param context 32 | */ 33 | void getTopStories(Context context); 34 | 35 | /** 36 | * 获取热门文章列表 37 | * 38 | * @param context 39 | * @param refresh 是否要刷新 40 | */ 41 | void getTopStories(Context context, boolean refresh); 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/processor/INewsProcessor.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.processor; 2 | 3 | import android.content.Context; 4 | 5 | /** 6 | * Created by Hander on 16/2/27. 7 | *

8 | * Email : hander_wei@163.com 9 | */ 10 | public interface INewsProcessor { 11 | 12 | /** 13 | * 获取某篇文章的内容 14 | * 15 | * @param context 16 | * @param id 文章的唯一ID 17 | */ 18 | void getNewsContent(Context context, int id); 19 | 20 | /** 21 | * 下载文章内容(不显示) 22 | * 23 | * @param context 24 | * @param id 25 | */ 26 | void loadNewsContent(Context context, int id); 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/processor/IOfflineDownloadProcessor.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.processor; 2 | 3 | /** 4 | * Created by Hander on 16/3/2. 5 | *

6 | * Email : hander_wei@163.com 7 | */ 8 | public interface IOfflineDownloadProcessor { 9 | 10 | /** 11 | * 下载文章,加入磁盘缓存中 12 | */ 13 | void downloadStories(); 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/processor/NewsProcessor.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.processor; 2 | 3 | import android.content.Context; 4 | 5 | import de.greenrobot.event.EventBus; 6 | import me.chen_wei.zhihu.Constants; 7 | import me.chen_wei.zhihu.cache.ACache; 8 | import me.chen_wei.zhihu.event.NewsDownloadedEvent; 9 | import me.chen_wei.zhihu.event.NewsLoadedEvent; 10 | import me.chen_wei.zhihu.network.api.ZhihuAPI; 11 | import me.chen_wei.zhihu.network.model.News; 12 | import retrofit2.Call; 13 | import retrofit2.Callback; 14 | import retrofit2.Response; 15 | import retrofit2.Retrofit; 16 | import retrofit2.converter.gson.GsonConverterFactory; 17 | 18 | /** 19 | * Created by Hander on 16/2/27. 20 | *

21 | * Email : hander_wei@163.com 22 | */ 23 | public class NewsProcessor implements INewsProcessor { 24 | 25 | Retrofit retrofit; 26 | 27 | /** 28 | * 获取文章内容 29 | * 30 | * @param context 31 | * @param id 32 | */ 33 | @Override 34 | public void getNewsContent(Context context, final int id) { 35 | final ACache cache = ACache.get(context); 36 | News news; 37 | if ((news = getNewsFromCache(cache, id)) != null) { 38 | EventBus.getDefault().post(new NewsLoadedEvent(news)); 39 | } else { 40 | retrofit = new Retrofit.Builder().baseUrl(Constants.API_URL).addConverterFactory(GsonConverterFactory.create()).build(); 41 | 42 | ZhihuAPI zhihuAPI = retrofit.create(ZhihuAPI.class); 43 | Call call = zhihuAPI.getNews(id); 44 | call.enqueue(new Callback() { 45 | @Override 46 | public void onResponse(Call call, Response response) { 47 | EventBus.getDefault().post(new NewsLoadedEvent(response.body())); 48 | 49 | putNewsToCache(cache, id, response.body()); 50 | } 51 | 52 | @Override 53 | public void onFailure(Call call, Throwable t) { 54 | //TODO 55 | } 56 | }); 57 | } 58 | } 59 | 60 | /** 61 | * 从Cache中获取文章内容 62 | * 63 | * @param cache 64 | * @param id 65 | * @return 66 | */ 67 | public News getNewsFromCache(ACache cache, int id) { 68 | News news = (News) cache.getAsObject(Integer.toString(id)); 69 | return news; 70 | } 71 | 72 | /** 73 | * 将文章内容保存到Cache中(保存两周) 74 | * 75 | * @param cache 76 | * @param id 77 | * @param news 78 | */ 79 | public void putNewsToCache(ACache cache, int id, News news) { 80 | cache.put(Integer.toString(id), news, ACache.TIME_DAY * 14); 81 | } 82 | 83 | /** 84 | * 离线下载文章 85 | * 86 | * @param context 87 | * @param id 88 | */ 89 | @Override 90 | public void loadNewsContent(Context context, final int id) { 91 | final ACache cache = ACache.get(context); 92 | if ((getNewsFromCache(cache, id)) == null) { 93 | retrofit = new Retrofit.Builder().baseUrl(Constants.API_URL).addConverterFactory(GsonConverterFactory.create()).build(); 94 | 95 | ZhihuAPI zhihuAPI = retrofit.create(ZhihuAPI.class); 96 | Call call = zhihuAPI.getNews(id); 97 | call.enqueue(new Callback() { 98 | @Override 99 | public void onResponse(Call call, Response response) { 100 | //加入缓存 101 | putNewsToCache(cache, id, response.body()); 102 | 103 | EventBus.getDefault().post(new NewsDownloadedEvent()); 104 | } 105 | 106 | @Override 107 | public void onFailure(Call call, Throwable t) { 108 | //TODO 109 | } 110 | }); 111 | } else { 112 | EventBus.getDefault().post(new NewsDownloadedEvent()); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/network/processor/OfflineDownloadProcessor.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.network.processor; 2 | 3 | import android.content.Context; 4 | 5 | import java.util.List; 6 | 7 | import de.greenrobot.event.EventBus; 8 | import me.chen_wei.zhihu.cache.ACache; 9 | import me.chen_wei.zhihu.event.AllStoriedDownloadedEvent; 10 | import me.chen_wei.zhihu.event.NewsDownloadedEvent; 11 | import me.chen_wei.zhihu.network.model.Contents; 12 | import me.chen_wei.zhihu.util.DateUtil; 13 | 14 | /** 15 | * Created by Hander on 16/3/2. 16 | *

17 | * Email : hander_wei@163.com 18 | */ 19 | public class OfflineDownloadProcessor implements IOfflineDownloadProcessor { 20 | 21 | private static int storiesSize = 0; 22 | 23 | public Context mContext; 24 | 25 | public OfflineDownloadProcessor(Context context) { 26 | mContext = context; 27 | 28 | EventBus.getDefault().register(this); 29 | } 30 | 31 | public void onEvent(NewsDownloadedEvent event) { 32 | storiesSize--; 33 | if (storiesSize <= 0) { 34 | EventBus.getDefault().post(new AllStoriedDownloadedEvent()); 35 | } 36 | } 37 | 38 | @Override 39 | public void downloadStories() { 40 | String today = DateUtil.getLatestDateString(); 41 | ACache cache = ACache.get(mContext); 42 | Contents contents = (Contents) cache.getAsObject(today); 43 | List stories = contents.getStories(); 44 | storiesSize = stories.size(); 45 | NewsProcessor processor = new NewsProcessor(); 46 | for (Contents.StoriesEntity story : stories) { 47 | //离线下载文章 48 | processor.loadNewsContent(mContext, story.getId()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/presenter/MainPresenter.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.presenter; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | import de.greenrobot.event.EventBus; 7 | import me.chen_wei.zhihu.R; 8 | import me.chen_wei.zhihu.event.AllStoriedDownloadedEvent; 9 | import me.chen_wei.zhihu.event.ContentsLoadedEvent; 10 | import me.chen_wei.zhihu.event.LatestContentsLoadedEvent; 11 | import me.chen_wei.zhihu.event.LoadContentEvent; 12 | import me.chen_wei.zhihu.event.LoadFailureEvent; 13 | import me.chen_wei.zhihu.event.TopStoriesLoadedEvent; 14 | import me.chen_wei.zhihu.network.processor.ContentsProcessor; 15 | import me.chen_wei.zhihu.network.processor.IContentsProcessor; 16 | import me.chen_wei.zhihu.network.processor.OfflineDownloadProcessor; 17 | import me.chen_wei.zhihu.views.activities.IMainActivity; 18 | 19 | /** 20 | * Created by Hander on 16/2/26. 21 | *

22 | * Email : hander_wei@163.com 23 | */ 24 | public class MainPresenter { 25 | 26 | private IMainActivity mMainActivity; 27 | private IContentsProcessor mContentsProcessor; 28 | private Context mContext; 29 | 30 | public MainPresenter(Context context, IMainActivity main) { 31 | mContext = context; 32 | mMainActivity = main; 33 | mContentsProcessor = new ContentsProcessor(); 34 | 35 | EventBus.getDefault().register(this); 36 | } 37 | 38 | /** 39 | * 加载文章列表 40 | * 41 | * @param dateStr 42 | */ 43 | public void loadContents(String dateStr) { 44 | mContentsProcessor.getContents(mContext, dateStr); 45 | } 46 | 47 | public void loadLatestContents(String dateStr) { 48 | mContentsProcessor.getLatestContents(mContext, dateStr); 49 | } 50 | 51 | /** 52 | * 加载热门文章列表 53 | */ 54 | public void loadTopStories() { 55 | mContentsProcessor.getTopStories(mContext); 56 | } 57 | 58 | public void loadTopStories(boolean refresh) { 59 | mContentsProcessor.getTopStories(mContext, refresh); 60 | } 61 | 62 | /** 63 | * 离线下载 64 | */ 65 | public void offlineDownload() { 66 | OfflineDownloadProcessor processor = new OfflineDownloadProcessor(mContext); 67 | processor.downloadStories(); 68 | } 69 | 70 | /** 71 | * 加载失败 72 | * 73 | * @param event 74 | */ 75 | public void onEvent(LoadFailureEvent event) { 76 | //TODO 77 | } 78 | 79 | /** 80 | * 文章列表加载成功 81 | * 82 | * @param event 83 | */ 84 | public void onEvent(ContentsLoadedEvent event) { 85 | mMainActivity.setContents(event.contents.getStories()); 86 | } 87 | 88 | public void onEvent(LatestContentsLoadedEvent event) { 89 | mMainActivity.setLatestContents(event.contents.getStories()); 90 | } 91 | 92 | /** 93 | * 单个文章加载成功 94 | * 95 | * @param event 96 | */ 97 | public void onEvent(LoadContentEvent event) { 98 | mMainActivity.gotoStoryActivity(event.id); 99 | } 100 | 101 | /** 102 | * 热门文章列表加载成功 103 | * 104 | * @param event 105 | */ 106 | public void onEvent(TopStoriesLoadedEvent event) { 107 | mMainActivity.setTopStories(event.latest); 108 | } 109 | 110 | public void onEvent(AllStoriedDownloadedEvent event) { 111 | if (mContext != null) { 112 | Toast.makeText(mContext, R.string.download_finish, Toast.LENGTH_LONG).show(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/presenter/StoryPresenter.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.presenter; 2 | 3 | import android.content.Context; 4 | 5 | import de.greenrobot.event.EventBus; 6 | import me.chen_wei.zhihu.event.NewsLoadedEvent; 7 | import me.chen_wei.zhihu.network.processor.INewsProcessor; 8 | import me.chen_wei.zhihu.network.processor.NewsProcessor; 9 | import me.chen_wei.zhihu.views.activities.IStoryActivity; 10 | 11 | /** 12 | * Created by Hander on 16/2/27. 13 | *

14 | * Email : hander_wei@163.com 15 | */ 16 | public class StoryPresenter { 17 | 18 | private IStoryActivity mStoryActivity; 19 | private INewsProcessor mProcessor; 20 | private Context mContext; 21 | 22 | public StoryPresenter(Context context, IStoryActivity storyActivity) { 23 | mContext = context; 24 | mStoryActivity = storyActivity; 25 | mProcessor = new NewsProcessor(); 26 | 27 | EventBus.getDefault().register(this); 28 | } 29 | 30 | /** 31 | * 加载文章内容 32 | * 33 | * @param id 34 | */ 35 | public void loadNewsContent(int id) { 36 | mProcessor.getNewsContent(mContext, id); 37 | } 38 | 39 | public void onEvent(NewsLoadedEvent event) { 40 | mStoryActivity.setNewsContent(event.news); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/util/DateUtil.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.util; 2 | 3 | import java.text.ParseException; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Calendar; 6 | import java.util.Date; 7 | 8 | /** 9 | * Created by Hander on 16/2/27. 10 | *

11 | * Email : hander_wei@163.com 12 | */ 13 | public class DateUtil { 14 | 15 | /** 16 | * 获取最新日期 17 | * 18 | * @return 19 | */ 20 | public static String getLatestDateString() { 21 | SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); 22 | Calendar calendar = Calendar.getInstance(); 23 | if (calendar.get(Calendar.HOUR_OF_DAY) >= 7) {//知乎日报7点之前不更新,所以7点以前最新列表应该加载之前一天的内容 24 | calendar.add(Calendar.DAY_OF_YEAR, 1); 25 | } 26 | return sdf.format(calendar.getTime()); 27 | } 28 | 29 | /** 30 | * 获取前一天日期 31 | * 32 | * @param dateStr 33 | * @return 34 | */ 35 | public static String getDayBeforeThisDayString(String dateStr) { 36 | SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); 37 | Calendar calendar = Calendar.getInstance(); 38 | Date date = null; 39 | try { 40 | calendar.setTime(sdf.parse(dateStr)); 41 | calendar.add(Calendar.DAY_OF_YEAR, -1); 42 | date = calendar.getTime(); 43 | } catch (ParseException e) { 44 | e.printStackTrace(); 45 | } 46 | return sdf.format(date); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/util/NetworkUtil.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.util; 2 | 3 | import android.content.Context; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | import android.telephony.TelephonyManager; 7 | 8 | /** 9 | * Created by Hander on 16/3/3. 10 | *

11 | * Email : hander_wei@163.com 12 | * 13 | * Modified from :https://gist.githubusercontent.com/emil2k/5130324/raw/064c363329e2ea8ca667894010ea78a7a765a316/Connectivity.java 14 | */ 15 | public class NetworkUtil { 16 | 17 | /** 18 | * Get the network info 19 | * @param context 20 | * @return 21 | */ 22 | public static NetworkInfo getNetworkInfo(Context context){ 23 | ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 24 | return cm.getActiveNetworkInfo(); 25 | } 26 | 27 | /** 28 | * Check if there is any connectivity 29 | * @param context 30 | * @return 31 | */ 32 | public static boolean isConnected(Context context){ 33 | NetworkInfo info = NetworkUtil.getNetworkInfo(context); 34 | return (info != null && info.isConnected()); 35 | } 36 | 37 | /** 38 | * Check if there is any connectivity to a Wifi network 39 | * @param context 40 | * @return 41 | */ 42 | public static boolean isConnectedWifi(Context context){ 43 | NetworkInfo info = NetworkUtil.getNetworkInfo(context); 44 | return (info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_WIFI); 45 | } 46 | 47 | /** 48 | * Check if there is any connectivity to a mobile network 49 | * @param context 50 | * @return 51 | */ 52 | public static boolean isConnectedMobile(Context context){ 53 | NetworkInfo info = NetworkUtil.getNetworkInfo(context); 54 | return (info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_MOBILE); 55 | } 56 | 57 | /** 58 | * Check if there is fast connectivity 59 | * @param context 60 | * @return 61 | */ 62 | public static boolean isConnectedFast(Context context){ 63 | NetworkInfo info = NetworkUtil.getNetworkInfo(context); 64 | return (info != null && info.isConnected() && NetworkUtil.isConnectionFast(info.getType(),info.getSubtype())); 65 | } 66 | 67 | /** 68 | * Check if the connection is fast 69 | * @param type 70 | * @param subType 71 | * @return 72 | */ 73 | public static boolean isConnectionFast(int type, int subType){ 74 | if(type==ConnectivityManager.TYPE_WIFI){ 75 | return true; 76 | }else if(type==ConnectivityManager.TYPE_MOBILE){ 77 | switch(subType){ 78 | case TelephonyManager.NETWORK_TYPE_1xRTT: 79 | return false; // ~ 50-100 kbps 80 | case TelephonyManager.NETWORK_TYPE_CDMA: 81 | return false; // ~ 14-64 kbps 82 | case TelephonyManager.NETWORK_TYPE_EDGE: 83 | return false; // ~ 50-100 kbps 84 | case TelephonyManager.NETWORK_TYPE_EVDO_0: 85 | return true; // ~ 400-1000 kbps 86 | case TelephonyManager.NETWORK_TYPE_EVDO_A: 87 | return true; // ~ 600-1400 kbps 88 | case TelephonyManager.NETWORK_TYPE_GPRS: 89 | return false; // ~ 100 kbps 90 | case TelephonyManager.NETWORK_TYPE_HSDPA: 91 | return true; // ~ 2-14 Mbps 92 | case TelephonyManager.NETWORK_TYPE_HSPA: 93 | return true; // ~ 700-1700 kbps 94 | case TelephonyManager.NETWORK_TYPE_HSUPA: 95 | return true; // ~ 1-23 Mbps 96 | case TelephonyManager.NETWORK_TYPE_UMTS: 97 | return true; // ~ 400-7000 kbps 98 | /* 99 | * Above API level 7, make sure to set android:targetSdkVersion 100 | * to appropriate level to use these 101 | */ 102 | case TelephonyManager.NETWORK_TYPE_EHRPD: // API level 11 103 | return true; // ~ 1-2 Mbps 104 | case TelephonyManager.NETWORK_TYPE_EVDO_B: // API level 9 105 | return true; // ~ 5 Mbps 106 | case TelephonyManager.NETWORK_TYPE_HSPAP: // API level 13 107 | return true; // ~ 10-20 Mbps 108 | case TelephonyManager.NETWORK_TYPE_IDEN: // API level 8 109 | return false; // ~25 kbps 110 | case TelephonyManager.NETWORK_TYPE_LTE: // API level 11 111 | return true; // ~ 10+ Mbps 112 | // Unknown 113 | case TelephonyManager.NETWORK_TYPE_UNKNOWN: 114 | default: 115 | return false; 116 | } 117 | }else{ 118 | return false; 119 | } 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/views/EndlessRecyclerViewScrollListener.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.views; 2 | 3 | import android.support.v7.widget.GridLayoutManager; 4 | import android.support.v7.widget.LinearLayoutManager; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.support.v7.widget.StaggeredGridLayoutManager; 7 | 8 | public abstract class EndlessRecyclerViewScrollListener extends RecyclerView.OnScrollListener { 9 | // The minimum amount of items to have below your current scroll position 10 | // before loading more. 11 | private int visibleThreshold = 5; 12 | // The current offset index of data you have loaded 13 | private int currentPage = 0; 14 | // The total number of items in the dataset after the last load 15 | private int previousTotalItemCount = 0; 16 | // True if we are still waiting for the last set of data to load. 17 | private boolean loading = true; 18 | // Sets the starting page index 19 | private int startingPageIndex = 0; 20 | 21 | RecyclerView.LayoutManager mLayoutManager; 22 | 23 | public EndlessRecyclerViewScrollListener(LinearLayoutManager layoutManager) { 24 | this.mLayoutManager = layoutManager; 25 | } 26 | 27 | public EndlessRecyclerViewScrollListener(GridLayoutManager layoutManager) { 28 | this.mLayoutManager = layoutManager; 29 | visibleThreshold = visibleThreshold * layoutManager.getSpanCount(); 30 | } 31 | 32 | public EndlessRecyclerViewScrollListener(StaggeredGridLayoutManager layoutManager) { 33 | this.mLayoutManager = layoutManager; 34 | visibleThreshold = visibleThreshold * layoutManager.getSpanCount(); 35 | } 36 | 37 | public int getLastVisibleItem(int[] lastVisibleItemPositions) { 38 | int maxSize = 0; 39 | for (int i = 0; i < lastVisibleItemPositions.length; i++) { 40 | if (i == 0) { 41 | maxSize = lastVisibleItemPositions[i]; 42 | } 43 | else if (lastVisibleItemPositions[i] > maxSize) { 44 | maxSize = lastVisibleItemPositions[i]; 45 | } 46 | } 47 | return maxSize; 48 | } 49 | 50 | // This happens many times a second during a scroll, so be wary of the code you place here. 51 | // We are given a few useful parameters to help us work out if we need to load some more data, 52 | // but first we check if we are waiting for the previous load to finish. 53 | @Override 54 | public void onScrolled(RecyclerView view, int dx, int dy) { 55 | int lastVisibleItemPosition = 0; 56 | int totalItemCount = mLayoutManager.getItemCount(); 57 | 58 | if (mLayoutManager instanceof StaggeredGridLayoutManager) { 59 | int[] lastVisibleItemPositions = ((StaggeredGridLayoutManager) mLayoutManager).findLastVisibleItemPositions(null); 60 | // get maximum element within the list 61 | lastVisibleItemPosition = getLastVisibleItem(lastVisibleItemPositions); 62 | } else if (mLayoutManager instanceof LinearLayoutManager) { 63 | lastVisibleItemPosition = ((LinearLayoutManager) mLayoutManager).findLastVisibleItemPosition(); 64 | } else if (mLayoutManager instanceof GridLayoutManager) { 65 | lastVisibleItemPosition = ((GridLayoutManager) mLayoutManager).findLastVisibleItemPosition(); 66 | } 67 | 68 | // If the total item count is zero and the previous isn't, assume the 69 | // list is invalidated and should be reset back to initial state 70 | if (totalItemCount < previousTotalItemCount) { 71 | this.currentPage = this.startingPageIndex; 72 | this.previousTotalItemCount = totalItemCount; 73 | if (totalItemCount == 0) { 74 | this.loading = true; 75 | } 76 | } 77 | // If it’s still loading, we check to see if the dataset count has 78 | // changed, if so we conclude it has finished loading and update the current page 79 | // number and total item count. 80 | if (loading && (totalItemCount > previousTotalItemCount)) { 81 | loading = false; 82 | previousTotalItemCount = totalItemCount; 83 | } 84 | 85 | // If it isn’t currently loading, we check to see if we have breached 86 | // the visibleThreshold and need to reload more data. 87 | // If we do need to reload some more data, we execute onLoadMore to fetch the data. 88 | // threshold should reflect how many total columns there are too 89 | if (!loading && (lastVisibleItemPosition + visibleThreshold) > totalItemCount) { 90 | currentPage++; 91 | onLoadMore(currentPage, totalItemCount); 92 | loading = true; 93 | } 94 | } 95 | 96 | // Defines the process for actually loading more data based on page 97 | public abstract void onLoadMore(int page, int totalItemsCount); 98 | 99 | } 100 | 101 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/views/activities/AboutMeActivity.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.views.activities; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.support.v7.widget.Toolbar; 6 | import android.text.Html; 7 | import android.text.method.LinkMovementMethod; 8 | import android.view.View; 9 | import android.widget.TextView; 10 | 11 | import butterknife.Bind; 12 | import butterknife.ButterKnife; 13 | import me.chen_wei.zhihu.R; 14 | 15 | public class AboutMeActivity extends AppCompatActivity { 16 | 17 | @Bind(R.id.tool_bar_about_me) 18 | Toolbar toolbar; 19 | @Bind(R.id.tv_about_me) 20 | TextView tvAboutMe; 21 | @Bind(R.id.tv_license) 22 | TextView tvLicense; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_about_me); 28 | 29 | ButterKnife.bind(this); 30 | 31 | setSupportActionBar(toolbar); 32 | 33 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 34 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 35 | @Override 36 | public void onClick(View view) { 37 | onBackPressed(); 38 | } 39 | }); 40 | 41 | String aboutMeHtml = "<h3>项目地址:<a href=\"https://github.com/HanderWei/ZhihuDaily\">https://github.com/HanderWei/ZhihuDaily</a>欢迎 <b><font color=\"#0288D1\">Star</b>, <b><font color=\"#0288D1\">Fork</b> , <b><font color=\"#0288D1\">Issue</b></h3>

本项目仅供学习,请勿用于其他用途。若被告侵权,本人会及时删除整个项目。

请您暸解相关情况,并遵守知乎协议。

Email:hander_wei@163.com

Github:https://github.com/HanderWei

个人主页:http://chen-wei.me/

"; 42 | 43 | tvAboutMe.setText(Html.fromHtml(aboutMeHtml)); 44 | tvAboutMe.setMovementMethod(LinkMovementMethod.getInstance()); 45 | 46 | String licenseHtml = "<h3><b>The MIT License (MIT)</b></h3>

Copyright (c) 2016 Chen Wei

Permission is hereby granted, free of charge, to any person obtaining a copy" + 47 | " of this software and associated documentation files (the \"Software\"), to deal" + 48 | " in the Software without restriction, including without limitation the rights" + 49 | " to use, copy, modify, merge, publish, distribute, sublicense, and/or sell" + 50 | " copies of the Software, and to permit persons to whom the Software is" + 51 | " furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in" + 52 | " all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR" + 53 | " IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY," + 54 | " FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE" + 55 | " AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER" + 56 | " LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM," + 57 | " OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN" + 58 | " THE SOFTWARE.

"; 59 | tvLicense.setText(Html.fromHtml(licenseHtml)); 60 | } 61 | 62 | @Override 63 | public void onBackPressed() { 64 | super.onBackPressed(); 65 | finish(); 66 | overridePendingTransition(R.anim.hold, android.R.anim.fade_out); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/views/activities/IMainActivity.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.views.activities; 2 | 3 | import java.util.List; 4 | 5 | import me.chen_wei.zhihu.network.model.Contents; 6 | import me.chen_wei.zhihu.network.model.Latest; 7 | 8 | /** 9 | * Created by Hander on 16/2/26. 10 | *

11 | * Email : hander_wei@163.com 12 | */ 13 | public interface IMainActivity { 14 | 15 | //设置文章列表 16 | void setContents(List entities); 17 | 18 | //设置最新文章列表 19 | void setLatestContents(List entities); 20 | 21 | //加载故事页面 22 | void gotoStoryActivity(int id); 23 | 24 | //设置最热文章 25 | void setTopStories(Latest latest); 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/views/activities/IStoryActivity.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.views.activities; 2 | 3 | import me.chen_wei.zhihu.network.model.News; 4 | 5 | /** 6 | * Created by Hander on 16/2/27. 7 | *

8 | * Email : hander_wei@163.com 9 | */ 10 | public interface IStoryActivity { 11 | 12 | void setNewsContent(News news); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/views/activities/MainActivity.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.views.activities; 2 | 3 | import android.content.Intent; 4 | import android.content.res.Configuration; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.support.v4.widget.SwipeRefreshLayout; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.app.AppCompatDelegate; 10 | import android.support.v7.widget.LinearLayoutManager; 11 | import android.support.v7.widget.RecyclerView; 12 | import android.support.v7.widget.Toolbar; 13 | import android.view.Menu; 14 | import android.view.MenuInflater; 15 | import android.view.MenuItem; 16 | import android.widget.Toast; 17 | 18 | import com.bartoszlipinski.recyclerviewheader.RecyclerViewHeader; 19 | import com.zanlabs.widget.infiniteviewpager.InfiniteViewPager; 20 | import com.zanlabs.widget.infiniteviewpager.indicator.LinePageIndicator; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | import butterknife.Bind; 26 | import butterknife.ButterKnife; 27 | import me.chen_wei.zhihu.Constants; 28 | import me.chen_wei.zhihu.R; 29 | import me.chen_wei.zhihu.network.model.Contents; 30 | import me.chen_wei.zhihu.network.model.Latest; 31 | import me.chen_wei.zhihu.presenter.MainPresenter; 32 | import me.chen_wei.zhihu.util.DateUtil; 33 | import me.chen_wei.zhihu.views.EndlessRecyclerViewScrollListener; 34 | import me.chen_wei.zhihu.views.adapter.StoryListAdapter; 35 | import me.chen_wei.zhihu.views.adapter.TopStoriesAdapter; 36 | 37 | public class MainActivity extends AppCompatActivity implements IMainActivity { 38 | 39 | @Bind(R.id.tool_bar) 40 | Toolbar toolbar; 41 | 42 | @Bind(R.id.srl) 43 | SwipeRefreshLayout srl; 44 | 45 | @Bind(R.id.news_list) 46 | RecyclerView mNewsList; 47 | 48 | @Bind(R.id.header) 49 | RecyclerViewHeader mNewsListHeader; 50 | 51 | @Bind(R.id.view_pager) 52 | InfiniteViewPager viewPager; 53 | 54 | @Bind(R.id.indicator) 55 | LinePageIndicator indicator; 56 | 57 | private MainPresenter mPresenter; 58 | 59 | private List mStories; 60 | 61 | private List dateList; 62 | 63 | @Override 64 | protected void onCreate(Bundle savedInstanceState) { 65 | super.onCreate(savedInstanceState); 66 | setContentView(R.layout.activity_main); 67 | 68 | ButterKnife.bind(this); 69 | 70 | setSupportActionBar(toolbar); 71 | 72 | init(); 73 | } 74 | 75 | private void init() { 76 | mPresenter = new MainPresenter(getApplicationContext(), this); 77 | 78 | //加载热门文章列表 79 | mPresenter.loadTopStories(); 80 | 81 | //加载最新文章列表 82 | String latestDate = DateUtil.getLatestDateString(); 83 | dateList = new ArrayList<>(); 84 | dateList.add(latestDate); 85 | mPresenter.loadContents(latestDate); 86 | 87 | srl.setColorSchemeResources(R.color.colorAccent); 88 | //下拉刷新 89 | srl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 90 | @Override 91 | public void onRefresh() { 92 | srl.setRefreshing(true); 93 | 94 | //刷新热门文章列表 95 | mPresenter.loadTopStories(true); 96 | 97 | //刷新当前文章列表 98 | String curLatestDate = DateUtil.getLatestDateString(); 99 | if (dateList != null) { 100 | dateList.removeAll(dateList); 101 | dateList.add(curLatestDate); 102 | } 103 | mPresenter.loadLatestContents(DateUtil.getLatestDateString()); 104 | 105 | (new Handler()).postDelayed(new Runnable() { 106 | @Override 107 | public void run() { 108 | srl.setRefreshing(false); 109 | } 110 | }, 1200); 111 | } 112 | }); 113 | 114 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this); 115 | mNewsList.setLayoutManager(linearLayoutManager); 116 | 117 | mNewsListHeader.attachTo(mNewsList, true); 118 | 119 | mNewsList.addOnScrollListener(new EndlessRecyclerViewScrollListener(linearLayoutManager) { 120 | @Override 121 | public void onLoadMore(int page, int totalItemsCount) { 122 | String farthestDateStr; 123 | farthestDateStr = dateList.get(dateList.size() - 1); 124 | String beforeFarthestDateStr = DateUtil.getDayBeforeThisDayString(farthestDateStr); 125 | dateList.add(beforeFarthestDateStr); 126 | mPresenter.loadContents(beforeFarthestDateStr); 127 | } 128 | }); 129 | } 130 | 131 | @Override 132 | protected void onPause() { 133 | super.onPause(); 134 | } 135 | 136 | @Override 137 | protected void onDestroy() { 138 | super.onDestroy(); 139 | 140 | ButterKnife.unbind(this); 141 | } 142 | 143 | @Override 144 | public void setContents(List entities) { 145 | StoryListAdapter adapter = null; 146 | if (null == mStories || mStories.size() == 0) { 147 | mStories = entities; 148 | adapter = new StoryListAdapter(mStories, this); 149 | mNewsList.setAdapter(adapter); 150 | } else { 151 | mStories.addAll(entities); 152 | if (adapter == null) { 153 | adapter = new StoryListAdapter(mStories, this); 154 | } 155 | int curSize = adapter.getItemCount(); 156 | adapter.notifyItemRangeChanged(curSize, mStories.size() - 1); 157 | } 158 | } 159 | 160 | @Override 161 | public void setLatestContents(List entities) { 162 | mStories = entities; 163 | StoryListAdapter adapter = new StoryListAdapter(mStories, this); 164 | mNewsList.setAdapter(adapter); 165 | 166 | adapter.notifyItemRangeChanged(0, mStories.size() - 1); 167 | } 168 | 169 | @Override 170 | public void gotoStoryActivity(int id) { 171 | Intent intent = new Intent(MainActivity.this, StoryActivity.class); 172 | intent.putExtra(Constants.KEY_STORY_ID, id); 173 | startActivity(intent); 174 | overridePendingTransition(R.anim.hold, android.R.anim.fade_in); 175 | } 176 | 177 | @Override 178 | public void setTopStories(Latest latest) { 179 | //设置ViewPager 180 | TopStoriesAdapter adapter = new TopStoriesAdapter(this); 181 | adapter.setDataList(latest.getTop_stories()); 182 | viewPager.setAdapter(adapter); 183 | viewPager.setAutoScrollTime(3000); 184 | viewPager.startAutoScroll(); 185 | indicator.setViewPager(viewPager); 186 | } 187 | 188 | @Override 189 | protected void onStart() { 190 | super.onStart(); 191 | if (viewPager != null) { 192 | viewPager.startAutoScroll(); 193 | } 194 | } 195 | 196 | @Override 197 | protected void onStop() { 198 | super.onStop(); 199 | if (viewPager != null) { 200 | viewPager.stopAutoScroll(); 201 | } 202 | } 203 | 204 | @Override 205 | public boolean onCreateOptionsMenu(Menu menu) { 206 | MenuInflater inflater = getMenuInflater(); 207 | inflater.inflate(R.menu.menu_main, menu); 208 | return true; 209 | } 210 | 211 | @Override 212 | public boolean onOptionsItemSelected(MenuItem item) { 213 | switch (item.getItemId()) { 214 | case R.id.action_download: 215 | //离线下载 216 | Toast.makeText(getApplicationContext(), R.string.start_download, Toast.LENGTH_LONG).show(); 217 | mPresenter.offlineDownload(); 218 | return true; 219 | case R.id.action_day_night: 220 | int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; 221 | switch (currentNightMode){ 222 | case Configuration.UI_MODE_NIGHT_NO: 223 | getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES); 224 | recreate(); 225 | break; 226 | case Configuration.UI_MODE_NIGHT_YES: 227 | getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_NO); 228 | recreate(); 229 | break; 230 | case Configuration.UI_MODE_NIGHT_UNDEFINED: 231 | getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_AUTO); 232 | break; 233 | } 234 | return true; 235 | case R.id.action_about_me: 236 | Intent intent = new Intent(this, AboutMeActivity.class); 237 | startActivity(intent); 238 | overridePendingTransition(R.anim.hold, android.R.anim.fade_in); 239 | return true; 240 | default: 241 | return super.onOptionsItemSelected(item); 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/views/activities/StoryActivity.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.views.activities; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.Toolbar; 7 | import android.view.Menu; 8 | import android.view.MenuInflater; 9 | import android.view.MenuItem; 10 | import android.view.View; 11 | import android.webkit.WebView; 12 | import android.widget.ImageView; 13 | import android.widget.TextView; 14 | 15 | import com.squareup.picasso.Picasso; 16 | 17 | import butterknife.Bind; 18 | import butterknife.ButterKnife; 19 | import me.chen_wei.zhihu.Constants; 20 | import me.chen_wei.zhihu.R; 21 | import me.chen_wei.zhihu.network.model.News; 22 | import me.chen_wei.zhihu.presenter.StoryPresenter; 23 | 24 | public class StoryActivity extends AppCompatActivity implements IStoryActivity { 25 | 26 | @Bind(R.id.tool_bar_content) 27 | Toolbar toolbar; 28 | @Bind(R.id.story_content) 29 | WebView content; 30 | @Bind(R.id.tv_news_title) 31 | TextView newsTitle; 32 | @Bind(R.id.tv_img_source) 33 | TextView imgSource; 34 | @Bind(R.id.img_news_header) 35 | ImageView newsHeader; 36 | 37 | StoryPresenter mPresenter; 38 | 39 | News mNews; 40 | 41 | @Override 42 | protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | setContentView(R.layout.activity_story); 45 | 46 | ButterKnife.bind(this); 47 | 48 | setSupportActionBar(toolbar); 49 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 50 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 51 | @Override 52 | public void onClick(View view) { 53 | onBackPressed(); 54 | } 55 | }); 56 | 57 | Bundle bundle = getIntent().getExtras(); 58 | int id = (int) bundle.get(Constants.KEY_STORY_ID); 59 | 60 | mPresenter = new StoryPresenter(getApplicationContext(), this); 61 | 62 | mPresenter.loadNewsContent(id); 63 | } 64 | 65 | @Override 66 | public void setNewsContent(News news) { 67 | mNews = news; 68 | 69 | //Load Html into WebView with CSS 70 | String htmlData = "" + news.getBody(); 71 | content.loadDataWithBaseURL("file:///android_asset/", htmlData, "text/html", "utf-8", null); 72 | // content.loadData(news.getBody(), "text/html", "utf-8"); 73 | 74 | newsTitle.setText(news.getTitle()); 75 | imgSource.setText(news.getImage_source()); 76 | Picasso.with(this).load(news.getImage()).into(newsHeader); 77 | } 78 | 79 | @Override 80 | public void onBackPressed() { 81 | super.onBackPressed(); 82 | finish(); 83 | overridePendingTransition(R.anim.hold, android.R.anim.fade_out); 84 | } 85 | 86 | @Override 87 | public boolean onCreateOptionsMenu(Menu menu) { 88 | MenuInflater inflater = getMenuInflater(); 89 | inflater.inflate(R.menu.menu_story_content, menu); 90 | return true; 91 | } 92 | 93 | @Override 94 | public boolean onOptionsItemSelected(MenuItem item) { 95 | switch (item.getItemId()) { 96 | case R.id.share: 97 | if (mNews != null) { 98 | shareStory(); 99 | } 100 | return true; 101 | default: 102 | return super.onOptionsItemSelected(item); 103 | } 104 | } 105 | 106 | /* 107 | * 分享文章 108 | */ 109 | public void shareStory() { 110 | Intent sendIntent = new Intent(); 111 | sendIntent.setAction(Intent.ACTION_SEND); 112 | sendIntent.putExtra(Intent.EXTRA_TEXT, mNews.getShare_url()); 113 | sendIntent.setType("text/plain"); 114 | startActivity(sendIntent); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/views/adapter/StoryListAdapter.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.views.adapter; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import com.squareup.picasso.Picasso; 12 | 13 | import java.util.List; 14 | 15 | import butterknife.Bind; 16 | import butterknife.ButterKnife; 17 | import de.greenrobot.event.EventBus; 18 | import me.chen_wei.zhihu.R; 19 | import me.chen_wei.zhihu.event.LoadContentEvent; 20 | import me.chen_wei.zhihu.network.model.Contents; 21 | 22 | /** 23 | * Created by Hander on 16/2/26. 24 | *

25 | * Email : hander_wei@163.com 26 | */ 27 | public class StoryListAdapter extends RecyclerView.Adapter{ 28 | 29 | private List entities; 30 | private Context mContext; 31 | 32 | public StoryListAdapter(List entities, Context context){ 33 | this.entities = entities; 34 | mContext = context; 35 | } 36 | 37 | public static class ViewHolder extends RecyclerView.ViewHolder{ 38 | @Bind(R.id.story_title) 39 | TextView title; 40 | @Bind(R.id.story_img) 41 | ImageView img; 42 | 43 | public ViewHolder(View itemView){ 44 | super(itemView); 45 | ButterKnife.bind(this, itemView); 46 | } 47 | 48 | } 49 | 50 | @Override 51 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 52 | Context context = parent.getContext(); 53 | LayoutInflater inflater = LayoutInflater.from(context); 54 | 55 | View storyView = inflater.inflate(R.layout.item_story, parent, false); 56 | final ViewHolder vh = new ViewHolder(storyView); 57 | storyView.setOnClickListener(new View.OnClickListener(){ 58 | 59 | @Override 60 | public void onClick(View view) { 61 | EventBus.getDefault().post(new LoadContentEvent(entities.get(vh.getLayoutPosition()).getId())); 62 | } 63 | }); 64 | 65 | 66 | return vh; 67 | } 68 | 69 | @Override 70 | public void onBindViewHolder(StoryListAdapter.ViewHolder holder, int position) { 71 | Contents.StoriesEntity entity = entities.get(position); 72 | 73 | TextView title = holder.title; 74 | title.setText(entity.getTitle()); 75 | 76 | ImageView img = holder.img; 77 | Picasso.with(mContext).load(entity.getImages().get(0)).into(img); 78 | } 79 | 80 | @Override 81 | public int getItemCount() { 82 | return entities.size(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/me/chen_wei/zhihu/views/adapter/TopStoriesAdapter.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu.views.adapter; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import com.squareup.picasso.Picasso; 11 | import com.zanlabs.widget.infiniteviewpager.InfinitePagerAdapter; 12 | 13 | import java.util.List; 14 | 15 | import butterknife.Bind; 16 | import butterknife.ButterKnife; 17 | import de.greenrobot.event.EventBus; 18 | import me.chen_wei.zhihu.R; 19 | import me.chen_wei.zhihu.event.LoadContentEvent; 20 | import me.chen_wei.zhihu.network.model.Latest; 21 | 22 | /** 23 | * Created by Hander on 16/2/27. 24 | *

25 | * Email : hander_wei@163.com 26 | */ 27 | public class TopStoriesAdapter extends InfinitePagerAdapter { 28 | 29 | private Context mContext; 30 | private LayoutInflater mInflater; 31 | private List topStories; 32 | 33 | public TopStoriesAdapter(Context context) { 34 | mContext = context; 35 | mInflater = LayoutInflater.from(mContext); 36 | } 37 | 38 | public void setDataList(List entities) { 39 | topStories = entities; 40 | } 41 | 42 | @Override 43 | public int getItemCount() { 44 | return topStories == null ? 0 : topStories.size(); 45 | } 46 | 47 | @Override 48 | public View getView(int position, View view, ViewGroup container) { 49 | ViewHolder vh; 50 | if (view != null) { 51 | vh = (ViewHolder) view.getTag(); 52 | } else { 53 | view = mInflater.inflate(R.layout.item_top_story, container, false); 54 | vh = new ViewHolder(view); 55 | view.setTag(vh); 56 | } 57 | final Latest.TopStoriesEntity entity = topStories.get(position); 58 | vh.title.setText(entity.getTitle()); 59 | Picasso.with(mContext).load(entity.getImage()).into(vh.img); 60 | 61 | view.setOnClickListener(new View.OnClickListener() { 62 | @Override 63 | public void onClick(View view) { 64 | EventBus.getDefault().post(new LoadContentEvent(entity.getId())); 65 | } 66 | }); 67 | return view; 68 | } 69 | 70 | static class ViewHolder { 71 | @Bind(R.id.img_top_story) 72 | ImageView img; 73 | @Bind(R.id.tv_top_story_title) 74 | TextView title; 75 | 76 | public ViewHolder(View view) { 77 | ButterKnife.bind(this, view); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/logo-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/logo-web.png -------------------------------------------------------------------------------- /app/src/main/res/anim/hold.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-hdpi/ic_action_download.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-hdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-mdpi/ic_action_download.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-mdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-xhdpi/ic_action_download.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-xhdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-xxhdpi/ic_action_download.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/app/src/main/res/drawable-xxhdpi/ic_share.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/dot_bg_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/point_bg_enable.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/point_bg_normal.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout-night/item_story.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 19 | 20 | 27 | 28 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about_me.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 16 | 17 | 21 | 22 | 26 | 27 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 21 | 22 | 25 | 26 | 32 | 33 | 38 | 39 | 44 | 45 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_story.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 15 | 16 | 22 | 23 | 27 | 28 | 32 | 33 | 39 | 40 | 50 | 51 | 60 | 61 | 62 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_story.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 19 | 20 | 27 | 28 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_top_story.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 |

5 | 11 | 12 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_story_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values-night-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 15 | 16 | 21 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #FFFFFF 6 | #DBDBDB 7 | #939393 8 | #5F5F5F 9 | #323232 10 | 11 | #03A9F4 12 | #0288D1 13 | #FF5722 14 | 15 | 16 | @android:color/background_dark 17 | @android:color/black 18 | #FF9800 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22sp 4 | 18sp 5 | 15sp 6 | 12sp 7 | 8 | 9 | 40dp 10 | 24dp 11 | 14dp 12 | 10dp 13 | 4dp 14 | 15 | 16dp 16 | 16dp 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 知乎日报 3 | 首页 4 | 设置 5 | 分享 6 | 离线下载 7 | 切换模式 8 | 关于作者 9 | 10 | 开始离线下载... 11 | 已完成离线下载 12 | 离线下载失败 13 | AboutMeActivity 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 15 | 16 | 21 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/test/java/me/chen_wei/zhihu/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package me.chen_wei.zhihu; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.1.3' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Sep 09 14:33:08 CST 2016 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-2.14.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 | -------------------------------------------------------------------------------- /img/mine_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/img/mine_1.jpg -------------------------------------------------------------------------------- /img/mine_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/img/mine_2.jpg -------------------------------------------------------------------------------- /img/off_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/img/off_1.jpg -------------------------------------------------------------------------------- /img/off_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/img/off_2.jpg -------------------------------------------------------------------------------- /infiniteviewpager/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /infiniteviewpager/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 8 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile 'com.android.support:support-v4:21.0.0' 24 | } 25 | -------------------------------------------------------------------------------- /infiniteviewpager/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Java\SDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /infiniteviewpager/src/androidTest/java/com/zanlabs/widget/infiniteviewpager/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.zanlabs.widget.infiniteviewpager; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | *
Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /infiniteviewpager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/java/com/zanlabs/widget/infiniteviewpager/InfinitePagerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.zanlabs.widget.infiniteviewpager; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | /** 7 | * Created by RxRead on 2015/9/24. 8 | */ 9 | public abstract class InfinitePagerAdapter extends RecyclingPagerAdapter { 10 | 11 | 12 | @Override 13 | public View getView(int position, View convertView, ViewGroup container) { 14 | return null; 15 | } 16 | 17 | @Override 18 | /** 19 | * Note: use getItemCount instead*/ 20 | public final int getCount() { 21 | return getItemCount() * InfiniteViewPager.FakePositionHelper.MULTIPLIER; 22 | } 23 | 24 | @Deprecated 25 | 26 | protected View getViewInternal(int position, View convertView, ViewGroup container) { 27 | if(getItemCount()==0) 28 | return null; 29 | return getView(position % getItemCount(), convertView, container); 30 | } 31 | 32 | public abstract int getItemCount(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/java/com/zanlabs/widget/infiniteviewpager/InfiniteViewPager.java: -------------------------------------------------------------------------------- 1 | package com.zanlabs.widget.infiniteviewpager; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.Message; 6 | import android.support.v4.view.PagerAdapter; 7 | import android.support.v4.view.ViewPager; 8 | import android.util.AttributeSet; 9 | import android.util.Log; 10 | import android.view.MotionEvent; 11 | 12 | 13 | /** 14 | * Created by RxRead on 2015/9/24. 15 | * https://github.com/waylife/InfiniteViewPager 16 | */ 17 | public class InfiniteViewPager extends ViewPager { 18 | private final static boolean DEBUG = false; 19 | 20 | public static void log(String msg) { 21 | if (DEBUG) 22 | Log.i("InfiniteViewPager", msg); 23 | } 24 | 25 | 26 | private static final long DEFAULT_AUTO_SCROLL_INTERVAL = 3000;//3s 27 | private static final int MSG_AUTO_SCROLL = 1; 28 | private static final int MSG_SET_PAGE = 2; 29 | private Handler mHandler; 30 | private boolean mAutoScroll; 31 | private boolean mIsInfinitePagerAdapter; 32 | /** 33 | * whether view is touched when auto scroll is enable 34 | */ 35 | private boolean mTouchedWhenAutoScroll; 36 | private OnPageChangeListener mOnPageChangeListener; 37 | private long mDelay = DEFAULT_AUTO_SCROLL_INTERVAL; 38 | 39 | public InfiniteViewPager(Context context) { 40 | this(context, null); 41 | } 42 | 43 | public InfiniteViewPager(Context context, AttributeSet attrs) { 44 | super(context, attrs); 45 | init(); 46 | } 47 | 48 | void init() { 49 | setOffscreenPageLimit(1); 50 | //set listeners 51 | super.setOnPageChangeListener(new OnPageChangeListener() { 52 | @Override 53 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 54 | // log("onPageScrolled:" + position + "->" + positionOffset + "-" + positionOffsetPixels); 55 | if (mOnPageChangeListener != null) { 56 | mOnPageChangeListener.onPageScrolled(FakePositionHelper.getFakeFromReal(InfiniteViewPager.this, position), positionOffset, positionOffsetPixels); 57 | } 58 | } 59 | 60 | @Override 61 | public void onPageSelected(int position) { 62 | // log("onPageSelected:" + position); 63 | if (position < FakePositionHelper.getStartPosition(InfiniteViewPager.this) || position > FakePositionHelper.getEndPosition(InfiniteViewPager.this)) { 64 | log("position:" + position + "->" + FakePositionHelper.getRealPositon(InfiniteViewPager.this, position) + "-return"); 65 | mHandler.removeMessages(MSG_SET_PAGE); 66 | Message msg = mHandler.obtainMessage(MSG_SET_PAGE); 67 | msg.arg1 = position; 68 | mHandler.sendMessageDelayed(msg, 500); 69 | return; 70 | } else { 71 | log("position:" + position + "->" + FakePositionHelper.getRealPositon(InfiniteViewPager.this, position)); 72 | } 73 | if (mOnPageChangeListener != null) { 74 | mOnPageChangeListener.onPageSelected(FakePositionHelper.getFakeFromReal(InfiniteViewPager.this, position)); 75 | } 76 | } 77 | 78 | @Override 79 | public void onPageScrollStateChanged(int state) { 80 | // String sta = "default"; 81 | // if (ViewPager.SCROLL_STATE_IDLE == state) { 82 | // sta = "idle"; 83 | // } else if (ViewPager.SCROLL_STATE_DRAGGING == state) { 84 | // sta = "dragging"; 85 | // } else if (ViewPager.SCROLL_STATE_SETTLING == state) { 86 | // sta = "setting"; 87 | // } 88 | // log("onPageScrollStateChanged->" + sta); 89 | if (mOnPageChangeListener != null) { 90 | mOnPageChangeListener.onPageScrollStateChanged(state); 91 | } 92 | } 93 | }); 94 | // 95 | mHandler = new Handler() { 96 | @Override 97 | public void dispatchMessage(Message msg) { 98 | switch (msg.what) { 99 | case MSG_AUTO_SCROLL: 100 | setItemToNext(); 101 | sendDelayMessage(); 102 | break; 103 | case MSG_SET_PAGE: 104 | setFakeCurrentItem(FakePositionHelper.getRealPositon(InfiniteViewPager.this, msg.arg1), false); 105 | break; 106 | } 107 | } 108 | }; 109 | } 110 | 111 | 112 | public void startAutoScroll() { 113 | startAutoScroll(this.mDelay); 114 | } 115 | 116 | public void startAutoScroll(long delayTime) { 117 | if (getAdapter() == null || getAdapter().getCount() == 0) 118 | return; 119 | this.mDelay = delayTime; 120 | this.mAutoScroll = true; 121 | sendDelayMessage(); 122 | 123 | } 124 | 125 | private void sendDelayMessage() { 126 | mHandler.removeMessages(MSG_AUTO_SCROLL); 127 | mHandler.sendEmptyMessageDelayed(MSG_AUTO_SCROLL, mDelay); 128 | } 129 | 130 | 131 | public void stopAutoScroll() { 132 | this.mAutoScroll = false; 133 | mHandler.removeMessages(MSG_AUTO_SCROLL); 134 | } 135 | 136 | public void setAutoScrollTime(long autoScrollTime) { 137 | this.mDelay = autoScrollTime; 138 | } 139 | 140 | @Override 141 | public void setOnPageChangeListener(OnPageChangeListener listener) { 142 | this.mOnPageChangeListener = listener; 143 | } 144 | 145 | 146 | private void setItemToNext() { 147 | PagerAdapter adapter = getAdapter(); 148 | if (adapter == null || adapter.getCount() == 0) { 149 | stopAutoScroll(); 150 | return; 151 | } 152 | int totalCount = isInfinitePagerAdapter() ? FakePositionHelper.getRealAdapterSize(this) : adapter.getCount(); 153 | if (totalCount <= 1) 154 | return; 155 | 156 | int nextItem = getFakeCurrentItem() + 1; 157 | if (isInfinitePagerAdapter()) { 158 | setFakeCurrentItem(nextItem); 159 | } else { 160 | if (nextItem == totalCount) { 161 | setFakeCurrentItem(0); 162 | } 163 | } 164 | } 165 | 166 | 167 | @Override 168 | public boolean onInterceptTouchEvent(MotionEvent ev) { 169 | //to solve conflict with parent ViewGroup 170 | getParent().requestDisallowInterceptTouchEvent(true); 171 | if (this.mAutoScroll || this.mTouchedWhenAutoScroll) { 172 | int action = ev.getAction(); 173 | switch (action) { 174 | case MotionEvent.ACTION_DOWN: 175 | this.mTouchedWhenAutoScroll = true; 176 | stopAutoScroll(); 177 | break; 178 | } 179 | } 180 | return super.onInterceptTouchEvent(ev); 181 | } 182 | 183 | @Override 184 | public void setCurrentItem(int item) { 185 | super.setCurrentItem(FakePositionHelper.getRealFromFake(this, item)); 186 | } 187 | 188 | @Override 189 | public void setCurrentItem(int item, boolean smoothScroll) { 190 | super.setCurrentItem(FakePositionHelper.getRealFromFake(this, item), smoothScroll); 191 | } 192 | 193 | @Override 194 | public int getCurrentItem() { 195 | return FakePositionHelper.getFakeFromReal(this, getFakeCurrentItem()); 196 | } 197 | 198 | private int getFakeCurrentItem() { 199 | return super.getCurrentItem(); 200 | } 201 | 202 | 203 | private void setFakeCurrentItem(int item) { 204 | super.setCurrentItem(item); 205 | } 206 | 207 | private void setFakeCurrentItem(int item, boolean smoothScroll) { 208 | super.setCurrentItem(item, smoothScroll); 209 | } 210 | 211 | private int getAdapterSize() { 212 | return getAdapter() == null ? 0 : getAdapter().getCount(); 213 | } 214 | 215 | private boolean isInfinitePagerAdapter() { 216 | return mIsInfinitePagerAdapter; 217 | } 218 | 219 | @Override 220 | public void setAdapter(PagerAdapter adapter) { 221 | super.setAdapter(adapter); 222 | mIsInfinitePagerAdapter = getAdapter() instanceof InfinitePagerAdapter; 223 | if (!mIsInfinitePagerAdapter) { 224 | throw new IllegalArgumentException("Currently, only InfinitePagerAdapter is supported"); 225 | } 226 | setFakeCurrentItem(FakePositionHelper.getRealPositon(InfiniteViewPager.this, 0), false); 227 | } 228 | 229 | @Override 230 | public boolean onTouchEvent(MotionEvent ev) { 231 | if (this.mAutoScroll || this.mTouchedWhenAutoScroll) { 232 | int action = ev.getAction(); 233 | switch (action) { 234 | case MotionEvent.ACTION_UP: 235 | this.mTouchedWhenAutoScroll = false; 236 | startAutoScroll(); 237 | break; 238 | } 239 | } 240 | return super.onTouchEvent(ev); 241 | } 242 | 243 | public static class FakePositionHelper { 244 | /** 245 | * Can not be less than 3 246 | */ 247 | public final static int MULTIPLIER = 5; 248 | 249 | public static int getRealFromFake(InfiniteViewPager viewPager, int fake) { 250 | int realAdapterSize = viewPager.getAdapterSize() / MULTIPLIER; 251 | if (realAdapterSize == 0) 252 | return 0; 253 | fake = fake % realAdapterSize;//ensure it 254 | int currentReal = viewPager.getFakeCurrentItem(); 255 | int real = fake + (currentReal - currentReal % realAdapterSize);//set to the target level 256 | return real; 257 | } 258 | 259 | public static int getFakeFromReal(InfiniteViewPager viewPager, int real) { 260 | int realAdapterSize = viewPager.getAdapterSize() / MULTIPLIER; 261 | if (realAdapterSize == 0) 262 | return 0; 263 | return real % realAdapterSize; 264 | } 265 | 266 | public static int getStartPosition(InfiniteViewPager viewPager) { 267 | int realAdapterSize = viewPager.getAdapterSize() / MULTIPLIER; 268 | return realAdapterSize; 269 | } 270 | 271 | public static int getEndPosition(InfiniteViewPager viewPager) { 272 | int realAdapterSize = viewPager.getAdapterSize() / MULTIPLIER; 273 | return realAdapterSize * (MULTIPLIER - 1) - 1; 274 | } 275 | 276 | public static int getRealAdapterSize(InfiniteViewPager viewPager) { 277 | return viewPager.isInfinitePagerAdapter() ? viewPager.getAdapterSize() / MULTIPLIER : viewPager.getAdapterSize(); 278 | } 279 | 280 | public static int getAdapterSize(ViewPager viewPager) { 281 | if (viewPager instanceof InfiniteViewPager) { 282 | InfiniteViewPager infiniteViewPager = (InfiniteViewPager) viewPager; 283 | return getRealAdapterSize(infiniteViewPager); 284 | } 285 | PagerAdapter adapter = viewPager.getAdapter(); 286 | if (adapter instanceof InfinitePagerAdapter) { 287 | InfinitePagerAdapter infinitePagerAdapter = (InfinitePagerAdapter) viewPager.getAdapter(); 288 | return infinitePagerAdapter.getItemCount(); 289 | } 290 | return adapter == null ? 0 : adapter.getCount(); 291 | } 292 | 293 | public static int getRealPositon(InfiniteViewPager viewPager, int position) { 294 | int realAdapterSize = getRealAdapterSize(viewPager); 295 | if (realAdapterSize == 0) 296 | return 0; 297 | int startPostion = getStartPosition(viewPager); 298 | int endPosition = getEndPosition(viewPager); 299 | if (position < startPostion) { 300 | return endPosition + 1 - realAdapterSize + position % realAdapterSize; 301 | } 302 | if (position > endPosition) { 303 | return startPostion + position % realAdapterSize; 304 | } 305 | return position; 306 | } 307 | 308 | public static boolean isOutOfRange(InfiniteViewPager viewPager, int position) { 309 | return position < getStartPosition(viewPager) || position > getEndPosition(viewPager); 310 | } 311 | 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/java/com/zanlabs/widget/infiniteviewpager/RecycleBin.java: -------------------------------------------------------------------------------- 1 | package com.zanlabs.widget.infiniteviewpager; 2 | 3 | import android.os.Build; 4 | import android.util.SparseArray; 5 | import android.view.View; 6 | 7 | /** 8 | * 9 | * https://github.com/JakeWharton/salvage 10 | * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of 11 | * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the 12 | * start of a layout. By construction, they are displaying current information. At the end of 13 | * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that 14 | * could potentially be used by the adapter to avoid allocating views unnecessarily. 15 | *

16 | * This class was taken from Android's implementation of {@link android.widget.AbsListView} which 17 | * is copyrighted 2006 The Android Open Source Project. 18 | */ 19 | public class RecycleBin { 20 | /** 21 | * Views that were on screen at the start of layout. This array is populated at the start of 22 | * layout, and at the end of layout all view in activeViews are moved to scrapViews. 23 | * Views in activeViews represent a contiguous range of Views, with position of the first 24 | * view store in mFirstActivePosition. 25 | */ 26 | private View[] activeViews = new View[0]; 27 | private int[] activeViewTypes = new int[0]; 28 | 29 | /** Unsorted views that can be used by the adapter as a convert view. */ 30 | private SparseArray[] scrapViews; 31 | 32 | private int viewTypeCount; 33 | 34 | private SparseArray currentScrapViews; 35 | 36 | public void setViewTypeCount(int viewTypeCount) { 37 | if (viewTypeCount < 1) { 38 | throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); 39 | } 40 | //noinspection unchecked 41 | SparseArray[] scrapViews = new SparseArray[viewTypeCount]; 42 | for (int i = 0; i < viewTypeCount; i++) { 43 | scrapViews[i] = new SparseArray(); 44 | } 45 | this.viewTypeCount = viewTypeCount; 46 | currentScrapViews = scrapViews[0]; 47 | this.scrapViews = scrapViews; 48 | } 49 | 50 | protected boolean shouldRecycleViewType(int viewType) { 51 | return viewType >= 0; 52 | } 53 | 54 | /** @return A view from the ScrapViews collection. These are unordered. */ 55 | View getScrapView(int position, int viewType) { 56 | if (viewTypeCount == 1) { 57 | return retrieveFromScrap(currentScrapViews, position); 58 | } else if (viewType >= 0 && viewType < scrapViews.length) { 59 | return retrieveFromScrap(scrapViews[viewType], position); 60 | } 61 | return null; 62 | } 63 | 64 | /** 65 | * Put a view into the ScrapViews list. These views are unordered. 66 | * 67 | * @param scrap The view to add 68 | */ 69 | void addScrapView(View scrap, int position, int viewType) { 70 | if (viewTypeCount == 1) { 71 | currentScrapViews.put(position, scrap); 72 | } else { 73 | scrapViews[viewType].put(position, scrap); 74 | } 75 | 76 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 77 | scrap.setAccessibilityDelegate(null); 78 | } 79 | } 80 | 81 | /** Move all views remaining in activeViews to scrapViews. */ 82 | void scrapActiveViews() { 83 | final View[] activeViews = this.activeViews; 84 | final int[] activeViewTypes = this.activeViewTypes; 85 | final boolean multipleScraps = viewTypeCount > 1; 86 | 87 | SparseArray scrapViews = currentScrapViews; 88 | final int count = activeViews.length; 89 | for (int i = count - 1; i >= 0; i--) { 90 | final View victim = activeViews[i]; 91 | if (victim != null) { 92 | int whichScrap = activeViewTypes[i]; 93 | 94 | activeViews[i] = null; 95 | activeViewTypes[i] = -1; 96 | 97 | if (!shouldRecycleViewType(whichScrap)) { 98 | continue; 99 | } 100 | 101 | if (multipleScraps) { 102 | scrapViews = this.scrapViews[whichScrap]; 103 | } 104 | scrapViews.put(i, victim); 105 | 106 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 107 | victim.setAccessibilityDelegate(null); 108 | } 109 | } 110 | } 111 | 112 | pruneScrapViews(); 113 | } 114 | 115 | /** 116 | * Makes sure that the size of scrapViews does not exceed the size of activeViews. 117 | * (This can happen if an adapter does not recycle its views). 118 | */ 119 | private void pruneScrapViews() { 120 | final int maxViews = activeViews.length; 121 | final int viewTypeCount = this.viewTypeCount; 122 | final SparseArray[] scrapViews = this.scrapViews; 123 | for (int i = 0; i < viewTypeCount; ++i) { 124 | final SparseArray scrapPile = scrapViews[i]; 125 | int size = scrapPile.size(); 126 | final int extras = size - maxViews; 127 | size--; 128 | for (int j = 0; j < extras; j++) { 129 | scrapPile.remove(scrapPile.keyAt(size--)); 130 | } 131 | } 132 | } 133 | 134 | static View retrieveFromScrap(SparseArray scrapViews, int position) { 135 | int size = scrapViews.size(); 136 | if (size > 0) { 137 | // See if we still have a view for this position. 138 | for (int i = 0; i < size; i++) { 139 | int fromPosition = scrapViews.keyAt(i); 140 | View view = scrapViews.get(fromPosition); 141 | if (fromPosition == position) { 142 | scrapViews.remove(fromPosition); 143 | return view; 144 | } 145 | } 146 | int index = size - 1; 147 | View r = scrapViews.valueAt(index); 148 | scrapViews.remove(scrapViews.keyAt(index)); 149 | return r; 150 | } else { 151 | return null; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/java/com/zanlabs/widget/infiniteviewpager/RecyclingPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.zanlabs.widget.infiniteviewpager; 2 | 3 | import android.support.v4.view.PagerAdapter; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.AdapterView; 7 | 8 | /** 9 | * https://github.com/JakeWharton/salvage 10 | * A {@link android.support.v4.view.PagerAdapter} which behaves like an {@link android.widget.Adapter} with view types and 11 | * view recycling. 12 | */ 13 | public abstract class RecyclingPagerAdapter extends PagerAdapter { 14 | static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE; 15 | 16 | private final RecycleBin recycleBin; 17 | 18 | public RecyclingPagerAdapter() { 19 | this(new RecycleBin()); 20 | } 21 | 22 | RecyclingPagerAdapter(RecycleBin recycleBin) { 23 | this.recycleBin = recycleBin; 24 | recycleBin.setViewTypeCount(getViewTypeCount()); 25 | } 26 | 27 | @Override 28 | public void notifyDataSetChanged() { 29 | recycleBin.scrapActiveViews(); 30 | super.notifyDataSetChanged(); 31 | } 32 | 33 | @Override 34 | public final Object instantiateItem(ViewGroup container, int position) { 35 | int viewType = getItemViewType(position); 36 | View view = null; 37 | if (viewType != IGNORE_ITEM_VIEW_TYPE) { 38 | view = recycleBin.getScrapView(position, viewType); 39 | } 40 | view = getViewInternal(position, view, container); 41 | container.addView(view); 42 | return view; 43 | } 44 | 45 | @Override 46 | public final void destroyItem(ViewGroup container, int position, Object object) { 47 | View view = (View) object; 48 | container.removeView(view); 49 | int viewType = getItemViewType(position); 50 | if (viewType != IGNORE_ITEM_VIEW_TYPE) { 51 | recycleBin.addScrapView(view, position, viewType); 52 | } 53 | } 54 | 55 | protected View getViewInternal(int position, View convertView, ViewGroup container){ 56 | return getView(position,convertView,container); 57 | } 58 | 59 | @Override 60 | public final boolean isViewFromObject(View view, Object object) { 61 | return view == object; 62 | } 63 | 64 | /** 65 | *

66 | * Returns the number of types of Views that will be created by 67 | * {@link #getView}. Each type represents a set of views that can be 68 | * converted in {@link #getView}. If the adapter always returns the same 69 | * type of View for all items, this method should return 1. 70 | *

71 | *

72 | * This method will only be called when when the adapter is set on the 73 | * the {@link android.widget.AdapterView}. 74 | *

75 | * 76 | * @return The number of types of Views that will be created by this adapter 77 | */ 78 | public int getViewTypeCount() { 79 | return 1; 80 | } 81 | 82 | /** 83 | * Get the type of View that will be created by {@link #getView} for the specified item. 84 | * 85 | * @param position The position of the item within the adapter's data set whose view type we 86 | * want. 87 | * @return An integer representing the type of View. Two views should share the same type if one 88 | * can be converted to the other in {@link #getView}. Note: Integers must be in the 89 | * range 0 to {@link #getViewTypeCount} - 1. {@link #IGNORE_ITEM_VIEW_TYPE} can 90 | * also be returned. 91 | * @see #IGNORE_ITEM_VIEW_TYPE 92 | */ 93 | @SuppressWarnings("UnusedParameters") // Argument potentially used by subclasses. 94 | public int getItemViewType(int position) { 95 | return 0; 96 | } 97 | 98 | /** 99 | * Get a View that displays the data at the specified position in the data set. You can either 100 | * create a View manually or inflate it from an XML layout file. When the View is inflated, the 101 | * parent View (GridView, ListView...) will apply default layout parameters unless you use 102 | * {@link android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)} 103 | * to specify a root view and to prevent attachment to the root. 104 | * 105 | * @param position The position of the item within the adapter's data set of the item whose view 106 | * we want. 107 | * @param convertView The old view to reuse, if possible. Note: You should check that this view 108 | * is non-null and of an appropriate type before using. If it is not possible to convert 109 | * this view to display the correct data, this method can create a new view. 110 | * Heterogeneous lists can specify their number of view types, so that this View is 111 | * always of the right type (see {@link #getViewTypeCount()} and 112 | * {@link #getItemViewType(int)}). 113 | * @param container The parent that this view will eventually be attached to 114 | * @return A View corresponding to the data at the specified position. 115 | */ 116 | public abstract View getView(int position, View convertView, ViewGroup container); 117 | } 118 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/java/com/zanlabs/widget/infiniteviewpager/indicator/PageIndicator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2011 Patrik Akerfeldt 3 | * Copyright (C) 2011 Jake Wharton 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package com.zanlabs.widget.infiniteviewpager.indicator; 19 | 20 | import android.support.v4.view.ViewPager; 21 | 22 | /** 23 | * A PageIndicator is responsible to show an visual indicator on the total views 24 | * number and the current visible view. 25 | */ 26 | public interface PageIndicator extends ViewPager.OnPageChangeListener { 27 | /** 28 | * Bind the indicator to a ViewPager. 29 | * 30 | * @param view 31 | */ 32 | void setViewPager(ViewPager view); 33 | 34 | /** 35 | * Bind the indicator to a ViewPager. 36 | * 37 | * @param view 38 | * @param initialPosition 39 | */ 40 | void setViewPager(ViewPager view, int initialPosition); 41 | 42 | /** 43 | *

Set the current page of both the ViewPager and indicator.

44 | * 45 | *

This must be used if you need to set the page before 46 | * the views are drawn on screen (e.g., default start page).

47 | * 48 | * @param item 49 | */ 50 | void setCurrentItem(int item); 51 | 52 | /** 53 | * Set a page change listener which will receive forwarded events. 54 | * 55 | * @param listener 56 | */ 57 | void setOnPageChangeListener(ViewPager.OnPageChangeListener listener); 58 | 59 | /** 60 | * Notify the indicator that the fragment list has changed. 61 | */ 62 | void notifyDataSetChanged(); 63 | } 64 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/java/com/zanlabs/widget/infiniteviewpager/indicator/UnderlinePageIndicator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 Jake Wharton 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.zanlabs.widget.infiniteviewpager.indicator; 17 | 18 | import android.content.Context; 19 | import android.content.res.Resources; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Canvas; 22 | import android.graphics.Paint; 23 | import android.graphics.drawable.Drawable; 24 | import android.os.Parcel; 25 | import android.os.Parcelable; 26 | import android.support.v4.view.MotionEventCompat; 27 | import android.support.v4.view.ViewConfigurationCompat; 28 | import android.support.v4.view.ViewPager; 29 | import android.util.AttributeSet; 30 | import android.view.MotionEvent; 31 | import android.view.View; 32 | import android.view.ViewConfiguration; 33 | 34 | import com.zanlabs.widget.infiniteviewpager.InfiniteViewPager; 35 | import com.zanlabs.widget.infiniteviewpager.R; 36 | 37 | /** 38 | * Draws a line for each page. The current page line is colored differently 39 | * than the unselected page lines. 40 | */ 41 | public class UnderlinePageIndicator extends View implements PageIndicator { 42 | private static final int INVALID_POINTER = -1; 43 | private static final int FADE_FRAME_MS = 30; 44 | 45 | private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 46 | 47 | private boolean mFades; 48 | private int mFadeDelay; 49 | private int mFadeLength; 50 | private int mFadeBy; 51 | 52 | private ViewPager mViewPager; 53 | private ViewPager.OnPageChangeListener mListener; 54 | private int mScrollState; 55 | private int mCurrentPage; 56 | private float mPositionOffset; 57 | 58 | private int mTouchSlop; 59 | private float mLastMotionX = -1; 60 | private int mActivePointerId = INVALID_POINTER; 61 | private boolean mIsDragging; 62 | 63 | private final Runnable mFadeRunnable = new Runnable() { 64 | @Override public void run() { 65 | if (!mFades) return; 66 | 67 | final int alpha = Math.max(mPaint.getAlpha() - mFadeBy, 0); 68 | mPaint.setAlpha(alpha); 69 | invalidate(); 70 | if (alpha > 0) { 71 | postDelayed(this, FADE_FRAME_MS); 72 | } 73 | } 74 | }; 75 | 76 | public UnderlinePageIndicator(Context context) { 77 | this(context, null); 78 | } 79 | 80 | public UnderlinePageIndicator(Context context, AttributeSet attrs) { 81 | this(context, attrs, R.attr.vpiUnderlinePageIndicatorStyle); 82 | } 83 | 84 | public UnderlinePageIndicator(Context context, AttributeSet attrs, int defStyle) { 85 | super(context, attrs, defStyle); 86 | if (isInEditMode()) return; 87 | 88 | final Resources res = getResources(); 89 | 90 | //Load defaults from resources 91 | final boolean defaultFades = res.getBoolean(R.bool.default_underline_indicator_fades); 92 | final int defaultFadeDelay = res.getInteger(R.integer.default_underline_indicator_fade_delay); 93 | final int defaultFadeLength = res.getInteger(R.integer.default_underline_indicator_fade_length); 94 | final int defaultSelectedColor = res.getColor(R.color.default_underline_indicator_selected_color); 95 | 96 | //Retrieve styles attributes 97 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.UnderlinePageIndicator, defStyle, 0); 98 | 99 | setFades(a.getBoolean(R.styleable.UnderlinePageIndicator_fades, defaultFades)); 100 | setSelectedColor(a.getColor(R.styleable.UnderlinePageIndicator_selectedColor, defaultSelectedColor)); 101 | setFadeDelay(a.getInteger(R.styleable.UnderlinePageIndicator_fadeDelay, defaultFadeDelay)); 102 | setFadeLength(a.getInteger(R.styleable.UnderlinePageIndicator_fadeLength, defaultFadeLength)); 103 | 104 | Drawable background = a.getDrawable(R.styleable.UnderlinePageIndicator_android_background); 105 | if (background != null) { 106 | setBackgroundDrawable(background); 107 | } 108 | 109 | a.recycle(); 110 | 111 | final ViewConfiguration configuration = ViewConfiguration.get(context); 112 | mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); 113 | } 114 | 115 | public boolean getFades() { 116 | return mFades; 117 | } 118 | 119 | public void setFades(boolean fades) { 120 | if (fades != mFades) { 121 | mFades = fades; 122 | if (fades) { 123 | post(mFadeRunnable); 124 | } else { 125 | removeCallbacks(mFadeRunnable); 126 | mPaint.setAlpha(0xFF); 127 | invalidate(); 128 | } 129 | } 130 | } 131 | 132 | public int getFadeDelay() { 133 | return mFadeDelay; 134 | } 135 | 136 | public void setFadeDelay(int fadeDelay) { 137 | mFadeDelay = fadeDelay; 138 | } 139 | 140 | public int getFadeLength() { 141 | return mFadeLength; 142 | } 143 | 144 | public void setFadeLength(int fadeLength) { 145 | mFadeLength = fadeLength; 146 | mFadeBy = 0xFF / (mFadeLength / FADE_FRAME_MS); 147 | } 148 | 149 | public int getSelectedColor() { 150 | return mPaint.getColor(); 151 | } 152 | 153 | public void setSelectedColor(int selectedColor) { 154 | mPaint.setColor(selectedColor); 155 | invalidate(); 156 | } 157 | 158 | @Override 159 | protected void onDraw(Canvas canvas) { 160 | super.onDraw(canvas); 161 | 162 | if (mViewPager == null) { 163 | return; 164 | } 165 | final int count = InfiniteViewPager.FakePositionHelper.getAdapterSize(mViewPager); 166 | if (count == 0) { 167 | return; 168 | } 169 | 170 | if (mCurrentPage >= count) { 171 | setCurrentItem(count - 1); 172 | return; 173 | } 174 | 175 | final int paddingLeft = getPaddingLeft(); 176 | final float pageWidth = (getWidth() - paddingLeft - getPaddingRight()) / (1f * count); 177 | final float left = paddingLeft + pageWidth * (mCurrentPage + mPositionOffset); 178 | final float right = left + pageWidth; 179 | final float top = getPaddingTop(); 180 | final float bottom = getHeight() - getPaddingBottom(); 181 | canvas.drawRect(left, top, right, bottom, mPaint); 182 | } 183 | 184 | public boolean onTouchEvent(MotionEvent ev) { 185 | if (super.onTouchEvent(ev)) { 186 | return true; 187 | } 188 | if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) { 189 | return false; 190 | } 191 | 192 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 193 | switch (action) { 194 | case MotionEvent.ACTION_DOWN: 195 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 196 | mLastMotionX = ev.getX(); 197 | break; 198 | 199 | case MotionEvent.ACTION_MOVE: { 200 | final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 201 | final float x = MotionEventCompat.getX(ev, activePointerIndex); 202 | final float deltaX = x - mLastMotionX; 203 | 204 | if (!mIsDragging) { 205 | if (Math.abs(deltaX) > mTouchSlop) { 206 | mIsDragging = true; 207 | } 208 | } 209 | 210 | if (mIsDragging) { 211 | mLastMotionX = x; 212 | if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) { 213 | mViewPager.fakeDragBy(deltaX); 214 | } 215 | } 216 | 217 | break; 218 | } 219 | 220 | case MotionEvent.ACTION_CANCEL: 221 | case MotionEvent.ACTION_UP: 222 | if (!mIsDragging) { 223 | final int count = InfiniteViewPager.FakePositionHelper.getAdapterSize(mViewPager); 224 | final int width = getWidth(); 225 | final float halfWidth = width / 2f; 226 | final float sixthWidth = width / 6f; 227 | 228 | if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) { 229 | if (action != MotionEvent.ACTION_CANCEL) { 230 | mViewPager.setCurrentItem(mCurrentPage - 1); 231 | } 232 | return true; 233 | } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) { 234 | if (action != MotionEvent.ACTION_CANCEL) { 235 | mViewPager.setCurrentItem(mCurrentPage + 1); 236 | } 237 | return true; 238 | } 239 | } 240 | 241 | mIsDragging = false; 242 | mActivePointerId = INVALID_POINTER; 243 | if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); 244 | break; 245 | 246 | case MotionEventCompat.ACTION_POINTER_DOWN: { 247 | final int index = MotionEventCompat.getActionIndex(ev); 248 | mLastMotionX = MotionEventCompat.getX(ev, index); 249 | mActivePointerId = MotionEventCompat.getPointerId(ev, index); 250 | break; 251 | } 252 | 253 | case MotionEventCompat.ACTION_POINTER_UP: 254 | final int pointerIndex = MotionEventCompat.getActionIndex(ev); 255 | final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 256 | if (pointerId == mActivePointerId) { 257 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 258 | mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 259 | } 260 | mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); 261 | break; 262 | } 263 | 264 | return true; 265 | } 266 | 267 | @Override 268 | public void setViewPager(ViewPager viewPager) { 269 | if (mViewPager == viewPager) { 270 | return; 271 | } 272 | if (mViewPager != null) { 273 | //Clear us from the old pager. 274 | mViewPager.setOnPageChangeListener(null); 275 | } 276 | if (viewPager.getAdapter() == null) { 277 | throw new IllegalStateException("ViewPager does not have adapter instance."); 278 | } 279 | mViewPager = viewPager; 280 | mViewPager.setOnPageChangeListener(this); 281 | invalidate(); 282 | post(new Runnable() { 283 | @Override public void run() { 284 | if (mFades) { 285 | post(mFadeRunnable); 286 | } 287 | } 288 | }); 289 | } 290 | 291 | @Override 292 | public void setViewPager(ViewPager view, int initialPosition) { 293 | setViewPager(view); 294 | setCurrentItem(initialPosition); 295 | } 296 | 297 | @Override 298 | public void setCurrentItem(int item) { 299 | if (mViewPager == null) { 300 | throw new IllegalStateException("ViewPager has not been bound."); 301 | } 302 | mViewPager.setCurrentItem(item); 303 | mCurrentPage = item; 304 | invalidate(); 305 | } 306 | 307 | @Override 308 | public void notifyDataSetChanged() { 309 | invalidate(); 310 | } 311 | 312 | @Override 313 | public void onPageScrollStateChanged(int state) { 314 | mScrollState = state; 315 | 316 | if (mListener != null) { 317 | mListener.onPageScrollStateChanged(state); 318 | } 319 | } 320 | 321 | @Override 322 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 323 | mCurrentPage = position; 324 | mPositionOffset = positionOffset; 325 | if (mFades) { 326 | if (positionOffsetPixels > 0) { 327 | removeCallbacks(mFadeRunnable); 328 | mPaint.setAlpha(0xFF); 329 | } else if (mScrollState != ViewPager.SCROLL_STATE_DRAGGING) { 330 | postDelayed(mFadeRunnable, mFadeDelay); 331 | } 332 | } 333 | invalidate(); 334 | 335 | if (mListener != null) { 336 | mListener.onPageScrolled(position, positionOffset, positionOffsetPixels); 337 | } 338 | } 339 | 340 | @Override 341 | public void onPageSelected(int position) { 342 | if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { 343 | mCurrentPage = position; 344 | mPositionOffset = 0; 345 | invalidate(); 346 | mFadeRunnable.run(); 347 | } 348 | if (mListener != null) { 349 | mListener.onPageSelected(position); 350 | } 351 | } 352 | 353 | @Override 354 | public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { 355 | mListener = listener; 356 | } 357 | 358 | @Override 359 | public void onRestoreInstanceState(Parcelable state) { 360 | SavedState savedState = (SavedState)state; 361 | super.onRestoreInstanceState(savedState.getSuperState()); 362 | mCurrentPage = savedState.currentPage; 363 | requestLayout(); 364 | } 365 | 366 | @Override 367 | public Parcelable onSaveInstanceState() { 368 | Parcelable superState = super.onSaveInstanceState(); 369 | SavedState savedState = new SavedState(superState); 370 | savedState.currentPage = mCurrentPage; 371 | return savedState; 372 | } 373 | 374 | static class SavedState extends BaseSavedState { 375 | int currentPage; 376 | 377 | public SavedState(Parcelable superState) { 378 | super(superState); 379 | } 380 | 381 | private SavedState(Parcel in) { 382 | super(in); 383 | currentPage = in.readInt(); 384 | } 385 | 386 | @Override 387 | public void writeToParcel(Parcel dest, int flags) { 388 | super.writeToParcel(dest, flags); 389 | dest.writeInt(currentPage); 390 | } 391 | 392 | @SuppressWarnings("UnusedDeclaration") 393 | public static final Creator CREATOR = new Creator() { 394 | @Override 395 | public SavedState createFromParcel(Parcel in) { 396 | return new SavedState(in); 397 | } 398 | 399 | @Override 400 | public SavedState[] newArray(int size) { 401 | return new SavedState[size]; 402 | } 403 | }; 404 | } 405 | } -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/color/vpi__dark_theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/color/vpi__light_theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_selected_focused_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_selected_focused_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_selected_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_selected_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_selected_pressed_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_selected_pressed_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_unselected_focused_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_unselected_focused_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_unselected_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_unselected_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_unselected_pressed_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-hdpi/vpi__tab_unselected_pressed_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_selected_focused_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_selected_focused_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_selected_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_selected_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_selected_pressed_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_selected_pressed_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_unselected_focused_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_unselected_focused_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_unselected_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_unselected_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_unselected_pressed_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-mdpi/vpi__tab_unselected_pressed_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_selected_focused_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_selected_focused_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_selected_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_selected_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_selected_pressed_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_selected_pressed_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_unselected_focused_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_unselected_focused_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_unselected_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_unselected_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_unselected_pressed_holo.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HanderWei/ZhihuDaily/91aa686af04355915d52be968a870de5b6e37480/infiniteviewpager/src/main/res/drawable-xhdpi/vpi__tab_unselected_pressed_holo.9.png -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/drawable/vpi__tab_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | InfiniteViewPager 3 | 4 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/values/vpi__attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/values/vpi__colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | #ff000000 19 | #fff3f3f3 20 | @color/vpi__background_holo_light 21 | @color/vpi__background_holo_dark 22 | #ff4c4c4c 23 | #ffb2b2b2 24 | @color/vpi__bright_foreground_holo_light 25 | @color/vpi__bright_foreground_holo_dark 26 | 27 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/values/vpi__defaults.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | true 19 | #FFFFFFFF 20 | #00000000 21 | 0 22 | 3dp 23 | false 24 | #FFDDDDDD 25 | 1dp 26 | 27 | 12dp 28 | 4dp 29 | 1dp 30 | #FF33B5E5 31 | #FFBBBBBB 32 | true 33 | 34 | 4dp 35 | #FF33B5E5 36 | 2dp 37 | 2 38 | 4dp 39 | 20dp 40 | 7dp 41 | 0 42 | #FFFFFFFF 43 | true 44 | #BBFFFFFF 45 | 15dp 46 | 5dp 47 | 7dp 48 | 49 | true 50 | 300 51 | 400 52 | #FF33B5E5 53 | -------------------------------------------------------------------------------- /infiniteviewpager/src/main/res/values/vpi__styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | 22 | 23 | 25 | 26 | 37 | 38 | 42 | 43 | 47 | 48 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':infiniteviewpager' 2 | --------------------------------------------------------------------------------