├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── mzule │ │ └── androidweekly │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── materialdesignicons-webfont.ttf │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── mzule │ │ │ └── androidweekly │ │ │ ├── App.java │ │ │ ├── api │ │ │ ├── ApiCallback.java │ │ │ ├── ArticleApi.java │ │ │ ├── DictionaryApi.java │ │ │ └── parser │ │ │ │ ├── ArticleParser.java │ │ │ │ ├── ArticleParsers.java │ │ │ │ ├── DocumentProvider.java │ │ │ │ ├── FresherArticlesParser.java │ │ │ │ └── OlderArticlesParser.java │ │ │ ├── dao │ │ │ ├── ArticleDao.java │ │ │ ├── FavoriteDao.java │ │ │ ├── IssueListKeeper.java │ │ │ ├── SearchHistoryKeeper.java │ │ │ └── TextZoomKeeper.java │ │ │ ├── entity │ │ │ ├── Article.java │ │ │ ├── Dict.java │ │ │ ├── Favorite.java │ │ │ ├── Issue.java │ │ │ └── TranslateResult.java │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ ├── AboutActivity.java │ │ │ │ ├── ArticleActivity.java │ │ │ │ ├── BaseActivity.java │ │ │ │ ├── FavoriteActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── SearchActivity.java │ │ │ │ └── SearchResultActivity.java │ │ │ ├── adapter │ │ │ │ ├── ArticleAdapter.java │ │ │ │ ├── SearchHistoryAdapter.java │ │ │ │ └── SlideAdapter.java │ │ │ ├── view │ │ │ │ ├── IconButton.java │ │ │ │ ├── NaviBar.java │ │ │ │ ├── PinnedSectionListView.java │ │ │ │ ├── PopupView.java │ │ │ │ ├── ProgressView.java │ │ │ │ ├── TintStatusBar.java │ │ │ │ ├── TranslateView.java │ │ │ │ └── base │ │ │ │ │ ├── BaseLinearLayout.java │ │ │ │ │ └── BaseRelativeLayout.java │ │ │ └── viewtype │ │ │ │ ├── ArticleViewType.java │ │ │ │ ├── BaseViewType.java │ │ │ │ ├── SearchHistoryViewType.java │ │ │ │ ├── SectionViewType.java │ │ │ │ └── SlideIssueViewType.java │ │ │ └── util │ │ │ ├── DateUtil.java │ │ │ ├── DensityUtil.java │ │ │ ├── DictParser.java │ │ │ ├── IOUtil.java │ │ │ ├── JsonUtil.java │ │ │ ├── Keyboard.java │ │ │ ├── MD5.java │ │ │ ├── StemUtil.java │ │ │ ├── ThreadUtil.java │ │ │ └── Tinter.java │ └── res │ │ ├── anim │ │ ├── slide_in_left.xml │ │ ├── slide_in_right.xml │ │ ├── slide_out_left.xml │ │ └── slide_out_right.xml │ │ ├── color │ │ ├── text_color_slide_date.xml │ │ └── text_color_slide_issue.xml │ │ ├── drawable-hdpi │ │ └── ic_logo.png │ │ ├── drawable-xxhdpi │ │ ├── ic_slide_menu_n.png │ │ └── ic_slide_menu_s.png │ │ ├── drawable │ │ ├── bg_button.xml │ │ ├── bg_button_n.xml │ │ ├── bg_button_s.xml │ │ ├── bg_dialog.xml │ │ └── ic_slide_menu.xml │ │ ├── layout │ │ ├── activity_about.xml │ │ ├── activity_article.xml │ │ ├── activity_favorite.xml │ │ ├── activity_main.xml │ │ ├── activity_search.xml │ │ ├── activity_search_result.xml │ │ ├── item_article.xml │ │ ├── item_search_history.xml │ │ ├── item_section.xml │ │ ├── item_slide_issue.xml │ │ ├── view_navi_bar.xml │ │ └── view_translate.xml │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── github │ └── mzule │ └── androidweekly │ └── ExampleUnitTest.java ├── art ├── article.jpg ├── favorite.jpg ├── home.jpg ├── issue.jpg ├── search.jpg └── translation.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── snowball ├── .gitignore ├── build.gradle └── src └── main └── java └── org └── tartarus └── snowball ├── Among.java ├── SnowballProgram.java ├── SnowballStemmer.java ├── TestApp.java └── ext └── EnglishStem.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidWeekly 2 | 3 | 一个第三方 androidweekly.net 的客户端,主要用 jsoup 抓取了 androidweekly.net 的数据进行展示,很适合在空暇时间阅读浏览。 4 | 5 | ## 目前实现的功能 6 | 7 | 1. 文章列表 8 | 2. 文章页面 9 | 3. 收藏文章 10 | 4. 调整文章字体大小 11 | 5. 文章页面内建词典 12 | 6. 文章分享 13 | 7. 已读文章全文搜索 14 | 8. 收藏导入导出 15 | 16 | 欢迎提 pull request 或 issue 添加新功能,修 bug. 17 | 18 | ## 下载 19 | 20 | [https://github.com/mzule/AndroidWeekly/releases](https://github.com/mzule/AndroidWeekly/releases) 21 | 22 | ## 运行效果 23 | 24 | ![](https://raw.githubusercontent.com/mzule/AndroidWeekly/master/art/translation.gif) 25 | ![](https://raw.githubusercontent.com/mzule/AndroidWeekly/master/art/home.jpg) 26 | ![](https://raw.githubusercontent.com/mzule/AndroidWeekly/master/art/favorite.jpg) 27 | ![](https://raw.githubusercontent.com/mzule/AndroidWeekly/master/art/issue.jpg) 28 | ![](https://raw.githubusercontent.com/mzule/AndroidWeekly/master/art/article.jpg) 29 | ![](https://raw.githubusercontent.com/mzule/AndroidWeekly/master/art/search.jpg) 30 | 31 | ## 感谢 32 | 33 | 本项目用了部分开源项目,在此感谢。 34 | 35 | * [ButterKnife](https://github.com/JakeWharton/butterknife) 36 | * [EasyAdapter](https://github.com/mzule/EasyAdapter) 37 | * [glide](https://github.com/bumptech/glide) 38 | * [gson](https://github.com/google/gson) 39 | * [jsoup](http://jsoup.org/) 40 | * [LayoutAnnotation](https://github.com/mzule/LayoutAnnotation) 41 | * [material-icon-lib](https://github.com/code-mc/material-icon-lib) 42 | * [materialish-progress](https://github.com/pnikosis/materialish-progress) 43 | * [Snowball](http://snowballstem.org/) 44 | 45 | ## 许可 46 | 47 | Apache License 2.0 48 | 49 | *不可发布到应用市场* 50 | 51 | ## 联系我 52 | 53 | 任何相关问题都可以通过以下方式联系我。 54 | 55 | 1. 提 issue 56 | 1. 新浪微博 [http://weibo.com/mzule](http://weibo.com/mzule) 57 | 1. 个人博客 [http://caodongping.me](http://caodongping.me) 58 | 1. 邮件 "mzule".concat("4j").concat("@").concat("gmail.com") 59 | 60 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'android-apt' 3 | 4 | android { 5 | compileSdkVersion 23 6 | buildToolsVersion "23.0.2" 7 | 8 | defaultConfig { 9 | applicationId "com.github.mzule.androidweekly" 10 | minSdkVersion 15 11 | targetSdkVersion 23 12 | versionCode 1 13 | versionName "1.0.1" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | compile fileTree(include: ['*.jar'], dir: 'libs') 25 | testCompile 'junit:junit:4.12' 26 | compile 'com.android.support:appcompat-v7:23.1.1' 27 | compile 'com.github.mzule.easyadapter:easyadapter:1.1.1' 28 | compile 'com.jakewharton:butterknife:7.0.1' 29 | compile 'org.jsoup:jsoup:1.8.3' 30 | compile 'com.github.bumptech.glide:glide:3.7.0' 31 | compile 'com.google.code.gson:gson:2.5' 32 | compile 'net.steamcrafted:materialiconlib:1.0.9' 33 | compile 'com.pnikosis:materialish-progress:1.7' 34 | compile project(':snowball') 35 | compile 'com.github.mzule.layoutannotation:library:1.0.4' 36 | apt 'com.github.mzule.layoutannotation:compiler:1.0.3' 37 | } 38 | -------------------------------------------------------------------------------- /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/baidu/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/com/github/mzule/androidweekly/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly; 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 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/assets/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/app/src/main/assets/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/App.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly; 2 | 3 | import android.app.Application; 4 | 5 | /** 6 | * Created by CaoDongping on 3/24/16. 7 | */ 8 | public class App extends Application { 9 | private static App instance; 10 | 11 | public static App getInstance() { 12 | return instance; 13 | } 14 | 15 | @Override 16 | public void onCreate() { 17 | super.onCreate(); 18 | instance = this; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/api/ApiCallback.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.api; 2 | 3 | /** 4 | * Created by CaoDongping on 3/24/16. 5 | */ 6 | public interface ApiCallback { 7 | 8 | void onSuccess(T data, boolean fromCache); 9 | 10 | void onFailure(Exception e); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/api/ArticleApi.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.api; 2 | 3 | 4 | import android.os.Handler; 5 | import android.text.TextUtils; 6 | 7 | import com.github.mzule.androidweekly.api.parser.ArticleParsers; 8 | import com.github.mzule.androidweekly.dao.ArticleDao; 9 | import com.github.mzule.androidweekly.dao.IssueListKeeper; 10 | import com.github.mzule.androidweekly.entity.Article; 11 | import com.github.mzule.androidweekly.entity.Issue; 12 | 13 | import org.jsoup.Jsoup; 14 | import org.jsoup.nodes.Document; 15 | import org.jsoup.nodes.Element; 16 | import org.jsoup.select.Elements; 17 | 18 | import java.net.URL; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by mzule on 3/16/16. 24 | */ 25 | public class ArticleApi { 26 | //TODO new thread to threadhandler 27 | private Handler handler = new Handler(); 28 | private ArticleDao articleDao; 29 | 30 | public ArticleApi() { 31 | articleDao = new ArticleDao(); 32 | } 33 | 34 | public void getPage(final String issue, final ApiCallback> callback) { 35 | new Thread() { 36 | @Override 37 | public void run() { 38 | try { 39 | if (!readCache()) { 40 | postSuccess(doGetPage(issue), callback); 41 | } 42 | } catch (final Exception e) { 43 | postError(e, callback); 44 | } 45 | 46 | } 47 | 48 | private boolean readCache() { 49 | if (!TextUtils.isEmpty(issue)) { 50 | List
articles = articleDao.read(issue); 51 | if (!articles.isEmpty()) { 52 | postSuccess(new Response<>(make(articles), true), callback); 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | private List make(List
articles) { 60 | List ret = new ArrayList<>(); 61 | for (Article article : articles) { 62 | if (!ret.contains(article.getSection())) { 63 | ret.add(article.getSection()); 64 | } 65 | ret.add(article); 66 | } 67 | return ret; 68 | } 69 | }.start(); 70 | } 71 | 72 | public void getArchive(final ApiCallback> callback) { 73 | new Thread() { 74 | @Override 75 | public void run() { 76 | try { 77 | List issues = IssueListKeeper.read(); 78 | if (issues != null && !issues.isEmpty()) { 79 | postSuccess(new Response<>(issues, true), callback); 80 | } 81 | postSuccess(doGetArchive(), callback); 82 | } catch (Exception e) { 83 | postError(e, callback); 84 | } 85 | } 86 | }.start(); 87 | } 88 | 89 | private Response> doGetArchive() throws Exception { 90 | Document doc = Jsoup.parse(new URL("http://androidweekly.net/archive"), 30000); 91 | Elements lis = doc.getElementsByClass("archive-list").get(0).getElementsByTag("li"); 92 | List issues = new ArrayList<>(); 93 | for (Element li : lis) { 94 | String date = li.getElementsByTag("span").get(0).text(); 95 | String name = li.getElementsByTag("h3").text().split("\\|")[0]; 96 | String url = li.getElementsByTag("a").attr("href"); 97 | issues.add(new Issue(name, url, date)); 98 | } 99 | IssueListKeeper.save(issues); 100 | return new Response<>(issues, false); 101 | } 102 | 103 | private Response> doGetPage(String issue) throws Exception { 104 | List result = ArticleParsers.get(issue).parse(issue); 105 | for (Object obj : result) { 106 | if (obj instanceof Article) { 107 | articleDao.save((Article) obj); 108 | } 109 | } 110 | return new Response<>(result, false); 111 | } 112 | 113 | private void postSuccess(final Response result, final ApiCallback callback) { 114 | handler.post(new Runnable() { 115 | @Override 116 | public void run() { 117 | callback.onSuccess(result.data, result.fromCache); 118 | } 119 | }); 120 | } 121 | 122 | private void postError(final Exception e, final ApiCallback callback) { 123 | e.printStackTrace(); 124 | handler.post(new Runnable() { 125 | @Override 126 | public void run() { 127 | callback.onFailure(e); 128 | } 129 | }); 130 | } 131 | 132 | static class Response { 133 | public T data; 134 | public boolean fromCache; 135 | 136 | public Response(T data, boolean fromCache) { 137 | this.data = data; 138 | this.fromCache = fromCache; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/api/DictionaryApi.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.api; 2 | 3 | import android.os.Handler; 4 | 5 | import com.github.mzule.androidweekly.entity.Dict; 6 | import com.github.mzule.androidweekly.util.DictParser; 7 | 8 | import java.net.URLEncoder; 9 | 10 | /** 11 | * Created by CaoDongping on 3/31/16. 12 | */ 13 | public class DictionaryApi { 14 | private Handler handler = new Handler(); 15 | 16 | public void look(String q, final ApiCallback callback) { 17 | final String url = "http://dict-co.iciba.com/api/dictionary.php?w=" + URLEncoder.encode(q) + "&key=A3950A86AC38B86FCDEE6EAC68931C25&type=xml"; 18 | 19 | new Thread(new Runnable() { 20 | @Override 21 | public void run() { 22 | try { 23 | final Dict result = new DictParser(url).parse(); 24 | handler.post(new Runnable() { 25 | @Override 26 | public void run() { 27 | callback.onSuccess(result, false); 28 | } 29 | }); 30 | } catch (final Exception e) { 31 | e.printStackTrace(); 32 | handler.post(new Runnable() { 33 | @Override 34 | public void run() { 35 | callback.onFailure(e); 36 | } 37 | }); 38 | } 39 | } 40 | }).start(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/api/parser/ArticleParser.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.api.parser; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | /** 7 | * Created by CaoDongping on 4/15/16. 8 | */ 9 | public interface ArticleParser { 10 | List parse(String issue) throws IOException; 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/api/parser/ArticleParsers.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.api.parser; 2 | 3 | import android.support.annotation.WorkerThread; 4 | 5 | /** 6 | * Created by CaoDongping on 4/15/16. 7 | */ 8 | public class ArticleParsers { 9 | @WorkerThread 10 | public static ArticleParser get(String issue) { 11 | if (issue == null || Integer.parseInt(issue.split("-")[1]) > 102) { 12 | return new FresherArticlesParser(); 13 | } else { 14 | return new OlderArticlesParser(); 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/api/parser/DocumentProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.api.parser; 2 | 3 | import org.jsoup.Jsoup; 4 | import org.jsoup.nodes.Document; 5 | 6 | import java.io.IOException; 7 | import java.net.URL; 8 | 9 | /** 10 | * Created by CaoDongping on 4/15/16. 11 | */ 12 | public class DocumentProvider { 13 | public static Document get(String issue) throws IOException { 14 | String url = "http://androidweekly.net/"; 15 | if (issue != null) { 16 | url += issue; 17 | } 18 | return Jsoup.parse(new URL(url), 30000); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/api/parser/FresherArticlesParser.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.api.parser; 2 | 3 | import com.github.mzule.androidweekly.entity.Article; 4 | 5 | import org.jsoup.nodes.Document; 6 | import org.jsoup.nodes.Element; 7 | import org.jsoup.select.Elements; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * Created by CaoDongping on 4/15/16. 15 | */ 16 | public class FresherArticlesParser implements ArticleParser { 17 | 18 | @Override 19 | public List parse(String issue) throws IOException { 20 | Document doc = DocumentProvider.get(issue); 21 | List articles = new ArrayList<>(); 22 | Elements tables = doc.getElementsByTag("table"); 23 | String currentSection = null; 24 | for (Element e : tables) { 25 | Elements h2 = e.getElementsByTag("h2"); 26 | Elements h5 = e.getElementsByTag("h5");// 兼容issue-226 SPONSORED 在 h5 标签里面 27 | if (!h2.isEmpty() || !h5.isEmpty()) { 28 | currentSection = h2.size() > 0 ? h2.get(0).text() : h5.get(0).text(); 29 | if (!articles.contains(currentSection)) { 30 | articles.add(currentSection); 31 | } 32 | } else { 33 | Elements tds = e.getElementsByTag("td"); 34 | Element td = tds.get(tds.size() - 2); 35 | String imageUrl = null; 36 | if (tds.size() == 4) { 37 | imageUrl = tds.get(0).getElementsByTag("img").get(0).attr("src"); 38 | } 39 | String title = td.getElementsByClass("article-headline").get(0).text(); 40 | String brief = td.getElementsByTag("p").get(0).text(); 41 | String link = td.getElementsByClass("article-headline").get(0).attr("href"); 42 | String domain = td.getElementsByTag("span").get(0).text().replace("(", "").replace(")", ""); 43 | if (issue == null) { 44 | String number = doc.getElementsByClass("issue-header").get(0).getElementsByTag("span").get(0).text(); 45 | issue = "/issues/issue-" + number.replace("#", ""); 46 | } 47 | Article article = new Article(); 48 | article.setTitle(title); 49 | article.setBrief(brief); 50 | article.setLink(link); 51 | article.setDomain(domain); 52 | article.setIssue(issue); 53 | article.setImageUrl(imageUrl); 54 | article.setSection(currentSection); 55 | articles.add(article); 56 | } 57 | } 58 | return articles; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/api/parser/OlderArticlesParser.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.api.parser; 2 | 3 | import com.github.mzule.androidweekly.entity.Article; 4 | 5 | import org.jsoup.nodes.Document; 6 | import org.jsoup.nodes.Element; 7 | import org.jsoup.select.Elements; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | /** 14 | * Created by CaoDongping on 4/15/16. 15 | */ 16 | public class OlderArticlesParser implements ArticleParser { 17 | 18 | @Override 19 | public List parse(String issue) throws IOException { 20 | Document doc = DocumentProvider.get(issue); 21 | List articles = new ArrayList<>(); 22 | Element root = doc.getElementsByClass("issue").get(0); 23 | while (root.children().size() == 1) { 24 | root = root.child(0); 25 | } 26 | String currentSection = null; 27 | for (Element e : root.children()) { 28 | if (e.tagName().equals("h2")) { 29 | currentSection = e.text(); 30 | articles.add(currentSection); 31 | continue; 32 | } 33 | if (e.tagName().equals("div")) { 34 | Elements img = e.getElementsByTag("img"); 35 | if (!img.isEmpty()) { 36 | Article article = new Article(); 37 | article.setImageUrl(img.get(0).attr("src")); 38 | article.setTitle(e.getElementsByTag("a").get(1).text()); 39 | article.setLink(e.getElementsByTag("a").get(1).attr("href")); 40 | article.setBrief(e.getElementsByTag("p").get(0).text()); 41 | Elements span = e.getElementsByTag("span"); 42 | if (!span.isEmpty()) { 43 | article.setDomain(span.get(0).text().replace("(", "").replace(")", "")); 44 | } 45 | article.setIssue(issue); 46 | article.setSection(currentSection); 47 | articles.add(article); 48 | } 49 | } else { 50 | Article article = new Article(); 51 | Elements title = e.getElementsByTag("a"); 52 | if (title.isEmpty()) { 53 | continue; 54 | } 55 | article.setTitle(title.get(0).text()); 56 | Elements span = e.getElementsByTag("span"); 57 | if (!span.isEmpty()) { 58 | article.setDomain(span.get(0).text().replace("(", "").replace(")", "")); 59 | } 60 | article.setLink(e.getElementsByTag("a").get(0).attr("href")); 61 | article.setBrief(e.text()); 62 | article.setIssue(issue); 63 | article.setSection(currentSection); 64 | articles.add(article); 65 | } 66 | } 67 | return articles; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/dao/ArticleDao.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.dao; 2 | 3 | import android.content.ContentValues; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteDatabase; 6 | import android.database.sqlite.SQLiteOpenHelper; 7 | import android.support.annotation.NonNull; 8 | 9 | import com.github.mzule.androidweekly.App; 10 | import com.github.mzule.androidweekly.entity.Article; 11 | import com.github.mzule.androidweekly.util.StemUtil; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * Created by CaoDongping on 4/2/16. 18 | */ 19 | public class ArticleDao extends SQLiteOpenHelper { 20 | private static final String NAME = "article"; 21 | private static final int VERSION = 1; 22 | private static final int INDEX_TITLE = 1; 23 | private static final int INDEX_BRIEF = 2; 24 | private static final int INDEX_LINK = 3; 25 | private static final int INDEX_IMAGE_URL = 4; 26 | private static final int INDEX_DOMAIN = 5; 27 | private static final int INDEX_ISSUE = 6; 28 | private static final int INDEX_SECTION = 7; 29 | 30 | public ArticleDao() { 31 | super(App.getInstance(), NAME, null, VERSION); 32 | } 33 | 34 | public List
read(String issue) { 35 | List
articles = new ArrayList<>(); 36 | Cursor cursor = getReadableDatabase().rawQuery("SELECT * FROM ARTICLE WHERE ISSUE=?", new String[]{issue}); 37 | while (cursor.moveToNext()) { 38 | articles.add(read(cursor)); 39 | } 40 | cursor.close(); 41 | return articles; 42 | } 43 | 44 | public List
search(String q) { 45 | q = StemUtil.stem(q); 46 | List
articles = new ArrayList<>(); 47 | Cursor cursor = getReadableDatabase().rawQuery("SELECT * FROM ARTICLE WHERE FTS MATCH ?", new String[]{q}); 48 | while (cursor.moveToNext()) { 49 | articles.add(read(cursor)); 50 | } 51 | cursor.close(); 52 | return articles; 53 | } 54 | 55 | @NonNull 56 | private Article read(Cursor cursor) { 57 | Article article = new Article(); 58 | article.setTitle(cursor.getString(INDEX_TITLE)); 59 | article.setBrief(cursor.getString(INDEX_BRIEF)); 60 | article.setLink(cursor.getString(INDEX_LINK)); 61 | article.setImageUrl(cursor.getString(INDEX_IMAGE_URL)); 62 | article.setDomain(cursor.getString(INDEX_DOMAIN)); 63 | article.setIssue(cursor.getString(INDEX_ISSUE)); 64 | article.setSection(cursor.getString(INDEX_SECTION)); 65 | return article; 66 | } 67 | 68 | public void save(Article article) { 69 | if (!checkExist(article)) { 70 | ContentValues cv = new ContentValues(); 71 | cv.put("TITLE", article.getTitle()); 72 | cv.put("BRIEF", article.getBrief()); 73 | cv.put("LINK", article.getLink()); 74 | cv.put("IMAGE_URL", article.getImageUrl()); 75 | cv.put("DOMAIN", article.getDomain()); 76 | cv.put("ISSUE", article.getIssue()); 77 | cv.put("SECTION", article.getSection()); 78 | cv.put("TYPE", 0);// TODO add type for article 79 | cv.put("FTS", StemUtil.stem(article.getFTS())); 80 | getWritableDatabase().insert("ARTICLE", null, cv); 81 | } 82 | } 83 | 84 | private boolean checkExist(Article article) { 85 | Cursor cursor = getReadableDatabase().rawQuery("SELECT COUNT(*) FROM ARTICLE WHERE LINK=?", new String[]{article.getLink()}); 86 | cursor.moveToNext(); 87 | int count = cursor.getInt(0); 88 | cursor.close(); 89 | return count > 0; 90 | } 91 | 92 | @Override 93 | public void onCreate(SQLiteDatabase db) { 94 | db.execSQL("CREATE VIRTUAL TABLE ARTICLE USING fts3(" + 95 | "ID INTEGER PRIMARY KEY AUTOINCREMENT, " + 96 | "TITLE VARCHAR(255), " + 97 | "BRIEF TEXT, " + 98 | "LINK VARCHAR(255), " + 99 | "IMAGE_URL VARCHAR(255), " + 100 | "DOMAIN VARCHAR(255), " + 101 | "ISSUE VARCHAR(255), " + 102 | "SECTION VARCHAR(255), " + 103 | "TYPE INTEGER, " + 104 | "FTS TEXT)"); 105 | } 106 | 107 | @Override 108 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/dao/FavoriteDao.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.dao; 2 | 3 | import android.content.ContentValues; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteDatabase; 6 | import android.database.sqlite.SQLiteOpenHelper; 7 | import android.os.Environment; 8 | 9 | import com.github.mzule.androidweekly.App; 10 | import com.github.mzule.androidweekly.entity.Article; 11 | import com.github.mzule.androidweekly.entity.Favorite; 12 | import com.github.mzule.androidweekly.util.IOUtil; 13 | import com.github.mzule.androidweekly.util.JsonUtil; 14 | import com.google.gson.reflect.TypeToken; 15 | 16 | import java.io.File; 17 | import java.io.FileWriter; 18 | import java.io.IOException; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by CaoDongping on 4/2/16. 24 | */ 25 | public class FavoriteDao extends SQLiteOpenHelper { 26 | private static final String NAME = "favorite"; 27 | private static final int VERSION = 1; 28 | private static final int INDEX_TITLE = 1; 29 | private static final int INDEX_BRIEF = 2; 30 | private static final int INDEX_LINK = 3; 31 | private static final int INDEX_IMAGE_URL = 4; 32 | private static final int INDEX_DOMAIN = 5; 33 | private static final int INDEX_ISSUE = 6; 34 | private static final int INDEX_SECTION = 7; 35 | private static final int INDEX_TYPE = 8; 36 | private static final int INDEX_TIME = 9; 37 | 38 | public FavoriteDao() { 39 | super(App.getInstance(), NAME, null, VERSION); 40 | } 41 | 42 | public List read() { 43 | List favorites = new ArrayList<>(); 44 | Cursor cursor = getReadableDatabase().rawQuery("SELECT * FROM FAVORITE ORDER BY TIME DESC", null); 45 | while (cursor.moveToNext()) { 46 | Article article = new Article(); 47 | article.setTitle(cursor.getString(INDEX_TITLE)); 48 | article.setBrief(cursor.getString(INDEX_BRIEF)); 49 | article.setLink(cursor.getString(INDEX_LINK)); 50 | article.setImageUrl(cursor.getString(INDEX_IMAGE_URL)); 51 | article.setDomain(cursor.getString(INDEX_DOMAIN)); 52 | article.setIssue(cursor.getString(INDEX_ISSUE)); 53 | article.setSection(cursor.getString(INDEX_SECTION)); 54 | Favorite favorite = new Favorite(article); 55 | favorite.setTime(cursor.getLong(INDEX_TIME)); 56 | favorites.add(favorite); 57 | } 58 | cursor.close(); 59 | return favorites; 60 | } 61 | 62 | public boolean contains(Article article) { 63 | Cursor cursor = getReadableDatabase().rawQuery("SELECT COUNT(*) FROM FAVORITE WHERE LINK=?", new String[]{article.getLink()}); 64 | cursor.moveToNext(); 65 | int count = cursor.getInt(0); 66 | cursor.close(); 67 | return count > 0; 68 | } 69 | 70 | public void save(Article article) { 71 | if (!contains(article)) { 72 | ContentValues cv = new ContentValues(); 73 | cv.put("TITLE", article.getTitle()); 74 | cv.put("BRIEF", article.getBrief()); 75 | cv.put("LINK", article.getLink()); 76 | cv.put("IMAGE_URL", article.getImageUrl()); 77 | cv.put("DOMAIN", article.getDomain()); 78 | cv.put("ISSUE", article.getIssue()); 79 | cv.put("SECTION", article.getSection()); 80 | cv.put("TYPE", 0);// TODO add type for article 81 | cv.put("TIME", System.currentTimeMillis()); 82 | getWritableDatabase().insert("FAVORITE", null, cv); 83 | } 84 | } 85 | 86 | public void save(Favorite favorite) { 87 | Article article = favorite.getArticle(); 88 | if (!contains(article)) { 89 | ContentValues cv = new ContentValues(); 90 | cv.put("TITLE", article.getTitle()); 91 | cv.put("BRIEF", article.getBrief()); 92 | cv.put("LINK", article.getLink()); 93 | cv.put("IMAGE_URL", article.getImageUrl()); 94 | cv.put("DOMAIN", article.getDomain()); 95 | cv.put("ISSUE", article.getIssue()); 96 | cv.put("SECTION", article.getSection()); 97 | cv.put("TYPE", 0);// TODO add type for article 98 | cv.put("TIME", favorite.getTime()); 99 | getWritableDatabase().insert("FAVORITE", null, cv); 100 | } 101 | } 102 | 103 | public void delete(Article article) { 104 | getWritableDatabase().delete("FAVORITE", "LINK=?", new String[]{article.getLink()}); 105 | } 106 | 107 | public void exportToFile() throws IOException { 108 | List all = read(); 109 | String json = JsonUtil.toJson(all); 110 | File file = new File(Environment.getExternalStorageDirectory(), "androidweeklyfavorite.json"); 111 | if (!file.exists()) { 112 | file.createNewFile(); 113 | } 114 | FileWriter fileWriter = null; 115 | try { 116 | fileWriter = new FileWriter(file); 117 | fileWriter.write(json); 118 | fileWriter.flush(); 119 | } finally { 120 | IOUtil.close(fileWriter); 121 | } 122 | } 123 | 124 | public void importFromFile() throws IOException { 125 | File file = new File(Environment.getExternalStorageDirectory(), "androidweeklyfavorite.json"); 126 | String json = IOUtil.read(file); 127 | List favorites = JsonUtil.fromJson(json, new TypeToken>() { 128 | }.getType()); 129 | if (favorites != null) { 130 | for (Favorite favorite : favorites) { 131 | save(favorite); 132 | } 133 | } 134 | } 135 | 136 | @Override 137 | public void onCreate(SQLiteDatabase db) { 138 | // 不创建一个与Article的外键引用是因为Article表是cache,可能会被清除。 139 | db.execSQL("CREATE TABLE IF NOT EXISTS FAVORITE(" + 140 | "ID INTEGER PRIMARY KEY AUTOINCREMENT, " + 141 | "TITLE VARCHAR(255), " + 142 | "BRIEF TEXT, " + 143 | "LINK VARCHAR(255), " + 144 | "IMAGE_URL VARCHAR(255), " + 145 | "DOMAIN VARCHAR(255), " + 146 | "ISSUE VARCHAR(255), " + 147 | "SECTION VARCHAR(255), " + 148 | "TYPE INTEGER, " + 149 | "TIME INTEGER)"); 150 | } 151 | 152 | @Override 153 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 154 | 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/dao/IssueListKeeper.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.dao; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.github.mzule.androidweekly.App; 7 | import com.github.mzule.androidweekly.entity.Issue; 8 | import com.github.mzule.androidweekly.util.JsonUtil; 9 | import com.google.gson.reflect.TypeToken; 10 | 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | /** 15 | * Created by CaoDongping on 4/2/16. 16 | */ 17 | public class IssueListKeeper { 18 | 19 | public static void save(List issues) { 20 | getSharedPreferences().edit().putString("issues", JsonUtil.toJson(issues)).apply(); 21 | } 22 | 23 | public static List read() { 24 | String json = getSharedPreferences().getString("issues", null); 25 | List issues = JsonUtil.fromJson(json, new TypeToken>() { 26 | }.getType()); 27 | return issues == null ? Collections.emptyList() : issues; 28 | } 29 | 30 | private static SharedPreferences getSharedPreferences() { 31 | return App.getInstance().getSharedPreferences("IssueList", Context.MODE_PRIVATE); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/dao/SearchHistoryKeeper.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.dao; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.text.TextUtils; 6 | 7 | import com.github.mzule.androidweekly.App; 8 | import com.github.mzule.androidweekly.util.JsonUtil; 9 | import com.google.gson.reflect.TypeToken; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /** 15 | * Created by CaoDongping on 4/5/16. 16 | */ 17 | public class SearchHistoryKeeper { 18 | 19 | private static final int MAX_SIZE = 5; 20 | 21 | public static void save(String q) { 22 | List exist = read(); 23 | if (exist.contains(q)) { 24 | exist.remove(q); 25 | } 26 | exist.add(0, q); 27 | if (exist.size() > MAX_SIZE) { 28 | exist = exist.subList(0, MAX_SIZE); 29 | } 30 | getSharedPreferences().edit().putString("q", JsonUtil.toJson(exist)).apply(); 31 | } 32 | 33 | public static List read() { 34 | String json = getSharedPreferences().getString("q", null); 35 | if (TextUtils.isEmpty(json)) { 36 | return new ArrayList<>(); 37 | } else { 38 | return JsonUtil.fromJson(json, new TypeToken>() { 39 | }.getType()); 40 | } 41 | } 42 | 43 | private static SharedPreferences getSharedPreferences() { 44 | return App.getInstance().getSharedPreferences("SearchHistory", Context.MODE_PRIVATE); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/dao/TextZoomKeeper.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.dao; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.github.mzule.androidweekly.App; 7 | 8 | /** 9 | * Created by CaoDongping on 3/26/16. 10 | */ 11 | public class TextZoomKeeper { 12 | public static void save(int zoom) { 13 | getSharedPreferences().edit().putInt("zoom", zoom).apply(); 14 | } 15 | 16 | public static int read(int def) { 17 | return getSharedPreferences().getInt("zoom", def); 18 | } 19 | 20 | private static SharedPreferences getSharedPreferences() { 21 | return App.getInstance().getSharedPreferences("TextZoom", Context.MODE_PRIVATE); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/entity/Article.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.entity; 2 | 3 | import java.io.Serializable; 4 | import java.util.Arrays; 5 | 6 | /** 7 | * Created by CaoDongping on 3/24/16. 8 | */ 9 | public class Article implements Serializable { 10 | private String title; 11 | private String brief; 12 | private String link; 13 | private String imageUrl; 14 | private String domain; 15 | private String issue; 16 | private String section; 17 | 18 | public String getSection() { 19 | return section; 20 | } 21 | 22 | public void setSection(String section) { 23 | this.section = section; 24 | } 25 | 26 | public String getIssue() { 27 | return issue; 28 | } 29 | 30 | public void setIssue(String issue) { 31 | this.issue = issue; 32 | } 33 | 34 | public String getTitle() { 35 | return title; 36 | } 37 | 38 | public void setTitle(String title) { 39 | this.title = title; 40 | } 41 | 42 | public String getBrief() { 43 | return brief; 44 | } 45 | 46 | public void setBrief(String brief) { 47 | this.brief = brief; 48 | } 49 | 50 | public String getLink() { 51 | return link; 52 | } 53 | 54 | public void setLink(String link) { 55 | this.link = link; 56 | } 57 | 58 | public String getImageUrl() { 59 | return imageUrl; 60 | } 61 | 62 | public void setImageUrl(String imageUrl) { 63 | this.imageUrl = imageUrl; 64 | } 65 | 66 | public String getDomain() { 67 | return domain; 68 | } 69 | 70 | public void setDomain(String domain) { 71 | this.domain = domain; 72 | } 73 | 74 | public String getFTS() { 75 | return title + "\n" + brief; 76 | } 77 | 78 | @Override 79 | public String toString() { 80 | return Arrays.toString(new String[]{title, "\n", brief, "\n", link, "\n", imageUrl, "\n", domain, "\n", "\n"}); 81 | } 82 | 83 | @Override 84 | public int hashCode() { 85 | return link == null ? 0 : link.hashCode(); 86 | } 87 | 88 | @Override 89 | public boolean equals(Object o) { 90 | if (o instanceof Article) { 91 | Article other = (Article) o; 92 | return link == null ? other.link == null : link.equals(other.link); 93 | } 94 | return false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/entity/Dict.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.entity; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * Created by CaoDongping on 3/31/16. 10 | */ 11 | public class Dict { 12 | private String key; 13 | private List ps; 14 | private List pos; 15 | private List pron; 16 | private List acceptation; 17 | private List sent; 18 | private String fy; 19 | 20 | public Dict() { 21 | ps = new ArrayList<>(); 22 | pos = new ArrayList<>(); 23 | pron = new ArrayList<>(); 24 | acceptation = new ArrayList<>(); 25 | sent = new ArrayList<>(); 26 | } 27 | 28 | public String getContent() { 29 | StringBuilder sb = new StringBuilder(); 30 | for (int i = 0; i < getPs().size(); i++) { 31 | sb.append(getPs().get(i)).append("\n"); 32 | } 33 | if (!TextUtils.isEmpty(getFy())) { 34 | sb.append("\n"); 35 | sb.append(getFy()).append("\n"); 36 | } 37 | for (int i = 0; i < getPos().size(); i++) { 38 | sb.append("\n"); 39 | sb.append(getPos().get(i)).append("\n"); 40 | sb.append(getAcceptation().get(i)).append("\n"); 41 | } 42 | for (int i = 0; i < getSent().size(); i++) { 43 | sb.append("\n"); 44 | sb.append(getSent().get(i).getOrig()).append("\n"); 45 | sb.append(getSent().get(i).getTrans()).append("\n"); 46 | } 47 | return sb.toString().trim(); 48 | } 49 | 50 | public void addPos(String pos) { 51 | this.pos.add(pos); 52 | } 53 | 54 | public void addAcceptation(String acceptation) { 55 | this.acceptation.add(acceptation); 56 | } 57 | 58 | public List getPos() { 59 | return pos; 60 | } 61 | 62 | public void setPos(List pos) { 63 | this.pos = pos; 64 | } 65 | 66 | public List getAcceptation() { 67 | return acceptation; 68 | } 69 | 70 | public void setAcceptation(List acceptation) { 71 | this.acceptation = acceptation; 72 | } 73 | 74 | public void addPs(String ps) { 75 | this.ps.add(ps); 76 | } 77 | 78 | public void addPron(String pron) { 79 | this.pron.add(pron); 80 | } 81 | 82 | public void addSent(Sent sent) { 83 | this.sent.add(sent); 84 | } 85 | 86 | public String getKey() { 87 | return key; 88 | } 89 | 90 | public void setKey(String key) { 91 | this.key = key; 92 | } 93 | 94 | public List getPs() { 95 | return ps; 96 | } 97 | 98 | public void setPs(List ps) { 99 | this.ps = ps; 100 | } 101 | 102 | public List getPron() { 103 | return pron; 104 | } 105 | 106 | public void setPron(List pron) { 107 | this.pron = pron; 108 | } 109 | 110 | public List getSent() { 111 | return sent; 112 | } 113 | 114 | public void setSent(List sent) { 115 | this.sent = sent; 116 | } 117 | 118 | public String getFy() { 119 | return fy; 120 | } 121 | 122 | public void setFy(String fy) { 123 | this.fy = fy; 124 | } 125 | 126 | public static class Sent { 127 | private String orig; 128 | private String trans; 129 | 130 | public String getOrig() { 131 | return orig; 132 | } 133 | 134 | public void setOrig(String orig) { 135 | this.orig = orig; 136 | } 137 | 138 | public String getTrans() { 139 | return trans; 140 | } 141 | 142 | public void setTrans(String trans) { 143 | this.trans = trans; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/entity/Favorite.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created by CaoDongping on 3/28/16. 7 | */ 8 | public class Favorite implements Serializable { 9 | private Article article; 10 | private long time; 11 | 12 | public Favorite(Article article) { 13 | this.article = article; 14 | this.time = System.currentTimeMillis(); 15 | } 16 | 17 | public Article getArticle() { 18 | return article; 19 | } 20 | 21 | public void setArticle(Article article) { 22 | this.article = article; 23 | } 24 | 25 | public long getTime() { 26 | return time; 27 | } 28 | 29 | public void setTime(long time) { 30 | this.time = time; 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | return o instanceof Favorite && article.equals(((Favorite) o).article); 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return article.hashCode(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/entity/Issue.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Created by CaoDongping on 3/25/16. 7 | */ 8 | public class Issue implements Serializable { 9 | private String date; 10 | private String name; 11 | private String url; 12 | private boolean active; 13 | 14 | public Issue(String name) { 15 | this.name = name; 16 | } 17 | 18 | public Issue(String name, boolean active) { 19 | this.name = name; 20 | this.active = active; 21 | } 22 | 23 | public Issue(String name, String url, String date) { 24 | this.name = name; 25 | this.url = url; 26 | this.date = date; 27 | } 28 | 29 | public String getUrl() { 30 | return url; 31 | } 32 | 33 | public void setUrl(String url) { 34 | this.url = url; 35 | } 36 | 37 | public String getDate() { 38 | return date; 39 | } 40 | 41 | public void setDate(String date) { 42 | this.date = date; 43 | } 44 | 45 | public String getName() { 46 | return name; 47 | } 48 | 49 | public void setName(String name) { 50 | this.name = name; 51 | } 52 | 53 | public boolean isActive() { 54 | return active; 55 | } 56 | 57 | public void setActive(boolean active) { 58 | this.active = active; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/entity/TranslateResult.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.entity; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Created by CaoDongping on 3/30/16. 9 | */ 10 | public class TranslateResult { 11 | private String from; 12 | private String to; 13 | @SerializedName("trans_result") 14 | private List result; 15 | 16 | public String getFrom() { 17 | return from; 18 | } 19 | 20 | public void setFrom(String from) { 21 | this.from = from; 22 | } 23 | 24 | public String getTo() { 25 | return to; 26 | } 27 | 28 | public void setTo(String to) { 29 | this.to = to; 30 | } 31 | 32 | public List getResult() { 33 | return result; 34 | } 35 | 36 | public void setResult(List result) { 37 | this.result = result; 38 | } 39 | 40 | public String getDst() { 41 | if (getResult() != null && !getResult().isEmpty()) { 42 | return getResult().get(0).getDst(); 43 | } 44 | return null; 45 | } 46 | 47 | public static class Result { 48 | private String src; 49 | private String dst; 50 | 51 | public Result(String src, String dst) { 52 | this.src = src; 53 | this.dst = dst; 54 | } 55 | 56 | public String getSrc() { 57 | return src; 58 | } 59 | 60 | public void setSrc(String src) { 61 | this.src = src; 62 | } 63 | 64 | public String getDst() { 65 | return dst; 66 | } 67 | 68 | public void setDst(String dst) { 69 | this.dst = dst; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/activity/AboutActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.text.Html; 6 | import android.text.method.LinkMovementMethod; 7 | import android.widget.TextView; 8 | 9 | import com.github.mzule.androidweekly.R; 10 | import com.github.mzule.layoutannotation.Layout; 11 | 12 | import butterknife.Bind; 13 | 14 | /** 15 | * Created by CaoDongping on 4/12/16. 16 | */ 17 | @Layout(R.layout.activity_about) 18 | public class AboutActivity extends BaseActivity { 19 | @Bind(R.id.textView) 20 | TextView textView; 21 | 22 | public static Intent makeIntent(Context context) { 23 | return new Intent(context, AboutActivity.class); 24 | } 25 | 26 | @Override 27 | protected void afterBind() { 28 | textView.setMovementMethod(LinkMovementMethod.getInstance()); 29 | textView.setText(Html.fromHtml("Copyright:
http://androidweekly.net/
" + 30 | "Github Host:
https://github.com/mzule/AndroidWeekly
" + 31 | "Contributor:
mzule / maoruibin ")); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/activity/ArticleActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.activity; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.ClipData; 5 | import android.content.ClipboardManager; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.support.v4.view.GravityCompat; 9 | import android.support.v4.widget.DrawerLayout; 10 | import android.view.View; 11 | import android.webkit.WebSettings; 12 | import android.webkit.WebView; 13 | import android.webkit.WebViewClient; 14 | import android.widget.Toast; 15 | 16 | import com.github.mzule.androidweekly.R; 17 | import com.github.mzule.androidweekly.dao.FavoriteDao; 18 | import com.github.mzule.androidweekly.dao.TextZoomKeeper; 19 | import com.github.mzule.androidweekly.entity.Article; 20 | import com.github.mzule.androidweekly.ui.view.ProgressView; 21 | import com.github.mzule.androidweekly.ui.view.TranslateView; 22 | import com.github.mzule.layoutannotation.Layout; 23 | 24 | import butterknife.Bind; 25 | import butterknife.OnClick; 26 | 27 | /** 28 | * Created by CaoDongping on 3/24/16. 29 | */ 30 | @Layout(R.layout.activity_article) 31 | public class ArticleActivity extends BaseActivity { 32 | @Bind(R.id.webView) 33 | WebView webView; 34 | @Bind(R.id.progressView) 35 | ProgressView progressView; 36 | @Bind(R.id.drawerLayout) 37 | DrawerLayout drawerLayout; 38 | @Bind(R.id.favoriteButton) 39 | View favoriteButton; 40 | private Article article; 41 | private WebSettings settings; 42 | private boolean changed; 43 | private FavoriteDao favoriteDao; 44 | 45 | public static Intent makeIntent(Context context, Article article) { 46 | Intent intent = new Intent(context, ArticleActivity.class); 47 | intent.putExtra("article", article); 48 | return intent; 49 | } 50 | 51 | @OnClick(R.id.favoriteButton) 52 | void favorite(View v) { 53 | changed = true; 54 | v.setSelected(!v.isSelected()); 55 | if (v.isSelected()) { 56 | favoriteDao.save(article); 57 | } else { 58 | favoriteDao.delete(article); 59 | } 60 | drawerLayout.closeDrawers(); 61 | } 62 | 63 | @OnClick(R.id.increaseButton) 64 | void increate() { 65 | settings.setTextZoom(settings.getTextZoom() + 5); 66 | } 67 | 68 | @OnClick(R.id.decreaseButton) 69 | void decrease() { 70 | settings.setTextZoom(settings.getTextZoom() - 5); 71 | } 72 | 73 | @OnClick(R.id.translateButton) 74 | void translateAndPaste(View v) { 75 | new TranslateView(this).attachTo(this, null); 76 | drawerLayout.closeDrawers(); 77 | } 78 | 79 | @OnClick(R.id.shareButton) 80 | void share() { 81 | Intent intent = new Intent(Intent.ACTION_SEND); 82 | intent.putExtra(Intent.EXTRA_TEXT, article.getTitle() + " " + article.getLink()); 83 | intent.setType("text/plain"); 84 | startActivity(Intent.createChooser(intent, "SHARE")); 85 | 86 | drawerLayout.closeDrawers(); 87 | } 88 | 89 | @OnClick(R.id.copyUrlButton) 90 | void copyUrl() { 91 | ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); 92 | ClipData clip = ClipData.newPlainText("label",article.getLink()); 93 | clipboard.setPrimaryClip(clip); 94 | drawerLayout.closeDrawers(); 95 | Toast.makeText(ArticleActivity.this, "已复制文章链接到粘贴板", Toast.LENGTH_SHORT).show(); 96 | } 97 | 98 | @SuppressLint("SetJavaScriptEnabled") 99 | @Override 100 | protected void afterBind() { 101 | favoriteDao = new FavoriteDao(); 102 | article = (Article) getIntent().getSerializableExtra("article"); 103 | settings = webView.getSettings(); 104 | webView.loadUrl(article.getLink()); 105 | webView.setWebViewClient(new WebViewClient() { 106 | @Override 107 | public void onPageFinished(WebView view, String url) { 108 | progressView.stop(); 109 | } 110 | }); 111 | settings.setTextZoom(TextZoomKeeper.read(settings.getTextZoom())); 112 | settings.setJavaScriptEnabled(true); 113 | settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK); 114 | settings.setDomStorageEnabled(true); 115 | favoriteButton.setSelected(favoriteDao.contains(article)); 116 | } 117 | 118 | @Override 119 | protected void onStop() { 120 | super.onStop(); 121 | TextZoomKeeper.save(settings.getTextZoom()); 122 | } 123 | 124 | @Override 125 | public void finish() { 126 | Intent intent = new Intent(); 127 | intent.putExtra("changed", changed); 128 | setResult(RESULT_OK, intent); 129 | super.finish(); 130 | } 131 | 132 | @Override 133 | public void onBackPressed() { 134 | if (drawerLayout.isDrawerOpen(GravityCompat.START)) { 135 | drawerLayout.closeDrawers(); 136 | } else if (webView.canGoBack()) { 137 | webView.goBack(); 138 | } else { 139 | super.onBackPressed(); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/activity/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.FragmentActivity; 5 | import android.view.View; 6 | 7 | import com.github.mzule.androidweekly.R; 8 | import com.github.mzule.androidweekly.ui.view.PopupView; 9 | import com.github.mzule.androidweekly.util.Tinter; 10 | import com.github.mzule.layoutannotation.LayoutBinder; 11 | 12 | import butterknife.ButterKnife; 13 | 14 | /** 15 | * Created by CaoDongping on 3/24/16. 16 | */ 17 | public abstract class BaseActivity extends FragmentActivity { 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | LayoutBinder.bind(this); 22 | ButterKnife.bind(this); 23 | Tinter.enableIfSupport(this); 24 | afterBind(); 25 | } 26 | 27 | public void back(View v) { 28 | onBackPressed(); 29 | } 30 | 31 | @Override 32 | public void onBackPressed() { 33 | PopupView popupView = (PopupView) findViewById(R.id.popup_view_id); 34 | if (popupView == null) { 35 | super.onBackPressed(); 36 | } else { 37 | popupView.finish(); 38 | } 39 | } 40 | 41 | protected abstract void afterBind(); 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/activity/FavoriteActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.support.v4.widget.DrawerLayout; 6 | import android.view.View; 7 | import android.widget.AdapterView; 8 | import android.widget.ListView; 9 | import android.widget.Toast; 10 | 11 | import com.github.mzule.androidweekly.R; 12 | import com.github.mzule.androidweekly.dao.FavoriteDao; 13 | import com.github.mzule.androidweekly.entity.Article; 14 | import com.github.mzule.androidweekly.entity.Favorite; 15 | import com.github.mzule.androidweekly.ui.adapter.ArticleAdapter; 16 | import com.github.mzule.androidweekly.util.DateUtil; 17 | import com.github.mzule.layoutannotation.Layout; 18 | 19 | import java.io.IOException; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | import butterknife.Bind; 24 | import butterknife.OnClick; 25 | import butterknife.OnItemClick; 26 | 27 | /** 28 | * Created by CaoDongping on 3/28/16. 29 | */ 30 | @Layout(R.layout.activity_favorite) 31 | public class FavoriteActivity extends BaseActivity { 32 | private static final int REQUEST_CODE_OPEN_ARTICLE = 0x1; 33 | @Bind(R.id.drawerLayout) 34 | DrawerLayout drawerLayout; 35 | @Bind(R.id.listView) 36 | ListView listView; 37 | private FavoriteDao favoriteDao; 38 | private ArticleAdapter adapter; 39 | 40 | public static Intent makeIntent(Context context) { 41 | return new Intent(context, FavoriteActivity.class); 42 | } 43 | 44 | @OnItemClick(R.id.listView) 45 | void onItemClick(AdapterView parent, View view, int position, long id) { 46 | Object item = parent.getAdapter().getItem(position); 47 | if (item instanceof Article) { 48 | startActivityForResult(ArticleActivity.makeIntent(this, (Article) item), REQUEST_CODE_OPEN_ARTICLE); 49 | } 50 | } 51 | 52 | @OnClick(R.id.exportButton) 53 | void exportToFile() { 54 | try { 55 | favoriteDao.exportToFile(); 56 | } catch (IOException e) { 57 | e.printStackTrace(); 58 | } 59 | Toast.makeText(this, "Export complete", Toast.LENGTH_SHORT).show(); 60 | drawerLayout.closeDrawers(); 61 | } 62 | 63 | @OnClick(R.id.importButton) 64 | void importFromFile() { 65 | try { 66 | favoriteDao.importFromFile(); 67 | } catch (IOException e) { 68 | e.printStackTrace(); 69 | } 70 | adapter.clear(); 71 | adapter.addAndNotify(extract(favoriteDao.read())); 72 | Toast.makeText(this, "Import complete", Toast.LENGTH_SHORT).show(); 73 | drawerLayout.closeDrawers(); 74 | } 75 | 76 | @Override 77 | protected void afterBind() { 78 | favoriteDao = new FavoriteDao(); 79 | adapter = new ArticleAdapter(this); 80 | listView.setAdapter(adapter); 81 | renderFavorites(); 82 | } 83 | 84 | private void renderFavorites() { 85 | List favorites = favoriteDao.read(); 86 | adapter.clear(); 87 | adapter.addAndNotify(extract(favorites)); 88 | } 89 | 90 | private List extract(List favorites) { 91 | List ret = new ArrayList<>(); 92 | for (Favorite favorite : favorites) { 93 | String time = DateUtil.format(favorite.getTime()); 94 | if (!ret.contains(time)) { 95 | ret.add(time); 96 | } 97 | ret.add(favorite.getArticle()); 98 | } 99 | return ret; 100 | } 101 | 102 | @Override 103 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 104 | super.onActivityResult(requestCode, resultCode, data); 105 | if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_OPEN_ARTICLE) { 106 | if (data.getBooleanExtra("changed", false)) { 107 | renderFavorites(); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/activity/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.activity; 2 | 3 | import android.support.v4.view.GravityCompat; 4 | import android.support.v4.widget.DrawerLayout; 5 | import android.view.View; 6 | import android.widget.AdapterView; 7 | import android.widget.ListView; 8 | 9 | import com.github.mzule.androidweekly.R; 10 | import com.github.mzule.androidweekly.api.ApiCallback; 11 | import com.github.mzule.androidweekly.api.ArticleApi; 12 | import com.github.mzule.androidweekly.entity.Article; 13 | import com.github.mzule.androidweekly.entity.Issue; 14 | import com.github.mzule.androidweekly.ui.adapter.ArticleAdapter; 15 | import com.github.mzule.androidweekly.ui.adapter.SlideAdapter; 16 | import com.github.mzule.androidweekly.ui.view.ProgressView; 17 | import com.github.mzule.layoutannotation.Layout; 18 | 19 | import java.util.List; 20 | 21 | import butterknife.Bind; 22 | import butterknife.OnClick; 23 | import butterknife.OnItemClick; 24 | 25 | @Layout(R.layout.activity_main) 26 | public class MainActivity extends BaseActivity { 27 | @Bind(R.id.drawerLayout) 28 | DrawerLayout drawerLayout; 29 | @Bind(R.id.listView) 30 | ListView listView; 31 | @Bind(R.id.progressView) 32 | ProgressView progressView; 33 | @Bind(R.id.slideListView) 34 | ListView slideListView; 35 | private ArticleAdapter adapter; 36 | private SlideAdapter slideAdapter; 37 | private List issues; 38 | private ArticleApi articleApi; 39 | 40 | @OnItemClick(R.id.slideListView) 41 | void onSlideItemClick(AdapterView parent, View view, int position, long id) { 42 | Issue issue = (Issue) parent.getAdapter().getItem(position); 43 | active(issue); 44 | slideAdapter.notifyDataSetChanged(); 45 | 46 | sendArticleListRequest(issue.getUrl()); 47 | 48 | drawerLayout.closeDrawers(); 49 | progressView.start(); 50 | } 51 | 52 | @OnItemClick(R.id.listView) 53 | void onItemClick(AdapterView parent, View view, int position, long id) { 54 | Object item = parent.getAdapter().getItem(position); 55 | if (item instanceof Article) { 56 | startActivity(ArticleActivity.makeIntent(this, (Article) item)); 57 | } 58 | } 59 | 60 | @OnClick(R.id.slideMenuButton) 61 | void onSlideMenuClick() { 62 | drawerLayout.openDrawer(GravityCompat.START); 63 | } 64 | 65 | @OnClick(R.id.favoriteButton) 66 | void onFavoriteClick() { 67 | startActivity(FavoriteActivity.makeIntent(this)); 68 | drawerLayout.closeDrawers(); 69 | } 70 | 71 | @OnClick(R.id.searchButton) 72 | void onSearchClicK() { 73 | startActivity(SearchActivity.makeIntent(this)); 74 | drawerLayout.closeDrawers(); 75 | } 76 | 77 | @OnClick(R.id.aboutButton) 78 | void onAboutClick() { 79 | startActivity(AboutActivity.makeIntent(this)); 80 | drawerLayout.closeDrawers(); 81 | } 82 | 83 | @Override 84 | protected void afterBind() { 85 | articleApi = new ArticleApi(); 86 | 87 | adapter = new ArticleAdapter(this); 88 | listView.setAdapter(adapter); 89 | 90 | slideAdapter = new SlideAdapter(this); 91 | slideListView.setAdapter(slideAdapter); 92 | 93 | sendArticleListRequest(null); 94 | sendIssueListRequest(); 95 | } 96 | 97 | private void active(Issue issue) { 98 | for (Issue i : issues) { 99 | i.setActive(i == issue); 100 | } 101 | } 102 | 103 | private void sendArticleListRequest(String issue) { 104 | articleApi.getPage(issue, new ApiCallback>() { 105 | @Override 106 | public void onSuccess(List data, boolean fromCache) { 107 | adapter.clear(); 108 | adapter.addAndNotify(data); 109 | listView.setSelection(0); 110 | progressView.stop(); 111 | } 112 | 113 | @Override 114 | public void onFailure(Exception e) { 115 | progressView.stop(); 116 | } 117 | }); 118 | } 119 | 120 | private void sendIssueListRequest() { 121 | articleApi.getArchive(new ApiCallback>() { 122 | @Override 123 | public void onSuccess(List data, boolean fromCache) { 124 | issues = data; 125 | slideAdapter.clear(); 126 | slideAdapter.addAndNotify(issues); 127 | active(issues.get(0)); 128 | } 129 | 130 | @Override 131 | public void onFailure(Exception e) { 132 | } 133 | }); 134 | } 135 | 136 | @Override 137 | public void onBackPressed() { 138 | if (drawerLayout.isDrawerOpen(GravityCompat.START)) { 139 | drawerLayout.closeDrawers(); 140 | } else { 141 | super.onBackPressed(); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/activity/SearchActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.view.KeyEvent; 6 | import android.view.View; 7 | import android.widget.AdapterView; 8 | import android.widget.EditText; 9 | import android.widget.ListView; 10 | import android.widget.TextView; 11 | 12 | import com.github.mzule.androidweekly.R; 13 | import com.github.mzule.androidweekly.dao.SearchHistoryKeeper; 14 | import com.github.mzule.androidweekly.ui.adapter.SearchHistoryAdapter; 15 | import com.github.mzule.layoutannotation.Layout; 16 | 17 | import butterknife.Bind; 18 | import butterknife.OnItemClick; 19 | 20 | /** 21 | * Created by CaoDongping on 4/5/16. 22 | */ 23 | @Layout(R.layout.activity_search) 24 | public class SearchActivity extends BaseActivity { 25 | @Bind(R.id.queryInput) 26 | EditText queryInput; 27 | @Bind(R.id.listView) 28 | ListView listView; 29 | private SearchHistoryAdapter adapter; 30 | 31 | public static Intent makeIntent(Context context) { 32 | return new Intent(context, SearchActivity.class); 33 | } 34 | 35 | @OnItemClick(R.id.listView) 36 | void onItemClick(AdapterView parent, View view, int position, long id) { 37 | String q = (String) parent.getAdapter().getItem(position); 38 | SearchHistoryKeeper.save(q); 39 | startActivity(SearchResultActivity.makeIntent(this, q)); 40 | } 41 | 42 | @Override 43 | protected void afterBind() { 44 | adapter = new SearchHistoryAdapter(this); 45 | listView.setAdapter(adapter); 46 | queryInput.setOnEditorActionListener(new TextView.OnEditorActionListener() { 47 | @Override 48 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 49 | String q = queryInput.getText().toString().trim(); 50 | startActivity(SearchResultActivity.makeIntent(SearchActivity.this, q)); 51 | SearchHistoryKeeper.save(q); 52 | queryInput.setText(""); 53 | return true; 54 | } 55 | }); 56 | } 57 | 58 | @Override 59 | protected void onStart() { 60 | super.onStart(); 61 | adapter.clear(); 62 | adapter.addAndNotify(SearchHistoryKeeper.read()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/activity/SearchResultActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.view.View; 6 | import android.widget.AdapterView; 7 | import android.widget.ListView; 8 | 9 | import com.github.mzule.androidweekly.R; 10 | import com.github.mzule.androidweekly.dao.ArticleDao; 11 | import com.github.mzule.androidweekly.entity.Article; 12 | import com.github.mzule.androidweekly.ui.adapter.ArticleAdapter; 13 | import com.github.mzule.androidweekly.ui.view.NaviBar; 14 | import com.github.mzule.layoutannotation.Layout; 15 | 16 | import java.util.List; 17 | 18 | import butterknife.Bind; 19 | import butterknife.OnItemClick; 20 | 21 | /** 22 | * Created by CaoDongping on 4/5/16. 23 | */ 24 | @Layout(R.layout.activity_search_result) 25 | public class SearchResultActivity extends BaseActivity { 26 | @Bind(R.id.naviBar) 27 | NaviBar naviBar; 28 | @Bind(R.id.listView) 29 | ListView listView; 30 | 31 | public static Intent makeIntent(Context context, String q) { 32 | Intent intent = new Intent(context, SearchResultActivity.class); 33 | intent.putExtra("q", q); 34 | return intent; 35 | } 36 | 37 | @OnItemClick(R.id.listView) 38 | void onItemClick(AdapterView parent, View view, int position, long id) { 39 | Object item = parent.getAdapter().getItem(position); 40 | if (item instanceof Article) { 41 | startActivity(ArticleActivity.makeIntent(this, (Article) item)); 42 | } 43 | } 44 | 45 | @Override 46 | protected void afterBind() { 47 | String q = getIntent().getStringExtra("q"); 48 | naviBar.setLeftText(q.toUpperCase()); 49 | 50 | ArticleDao articleDao = new ArticleDao(); 51 | List
result = articleDao.search(q); 52 | ArticleAdapter adapter = new ArticleAdapter(this); 53 | listView.setAdapter(adapter); 54 | adapter.addAndNotify(result); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/adapter/ArticleAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.adapter; 2 | 3 | import android.content.Context; 4 | 5 | import com.github.mzule.androidweekly.entity.Article; 6 | import com.github.mzule.androidweekly.ui.view.PinnedSectionListView; 7 | import com.github.mzule.androidweekly.ui.viewtype.ArticleViewType; 8 | import com.github.mzule.androidweekly.ui.viewtype.SectionViewType; 9 | import com.github.mzule.easyadapter.TypePerEntityAdapter; 10 | 11 | /** 12 | * Created by CaoDongping on 3/24/16. 13 | */ 14 | public class ArticleAdapter extends TypePerEntityAdapter implements PinnedSectionListView.PinnedSectionListAdapter { 15 | 16 | public ArticleAdapter(Context context) { 17 | super(context); 18 | } 19 | 20 | @Override 21 | protected void mapEntityViewTypes() { 22 | mapEntityViewType(String.class, SectionViewType.class); 23 | mapEntityViewType(Article.class, ArticleViewType.class); 24 | } 25 | 26 | @Override 27 | public boolean isItemViewTypePinned(int viewType) { 28 | return getRawViewType(SectionViewType.class) == viewType; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/adapter/SearchHistoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.adapter; 2 | 3 | import android.content.Context; 4 | 5 | import com.github.mzule.androidweekly.ui.viewtype.SearchHistoryViewType; 6 | import com.github.mzule.easyadapter.SingleTypeAdapter; 7 | import com.github.mzule.easyadapter.ViewType; 8 | 9 | /** 10 | * Created by CaoDongping on 4/5/16. 11 | */ 12 | public class SearchHistoryAdapter extends SingleTypeAdapter { 13 | public SearchHistoryAdapter(Context context) { 14 | super(context); 15 | } 16 | 17 | @Override 18 | protected Class singleViewType() { 19 | return SearchHistoryViewType.class; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/adapter/SlideAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.adapter; 2 | 3 | import android.content.Context; 4 | 5 | import com.github.mzule.androidweekly.entity.Issue; 6 | import com.github.mzule.androidweekly.ui.viewtype.SlideIssueViewType; 7 | import com.github.mzule.easyadapter.SingleTypeAdapter; 8 | import com.github.mzule.easyadapter.ViewType; 9 | 10 | /** 11 | * Created by CaoDongping on 3/25/16. 12 | */ 13 | public class SlideAdapter extends SingleTypeAdapter { 14 | 15 | public SlideAdapter(Context context) { 16 | super(context); 17 | } 18 | 19 | @Override 20 | protected Class singleViewType() { 21 | return SlideIssueViewType.class; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/IconButton.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | import com.github.mzule.androidweekly.R; 7 | 8 | import net.steamcrafted.materialiconlib.MaterialIconView; 9 | 10 | /** 11 | * Created by CaoDongping on 3/27/16. 12 | */ 13 | public class IconButton extends MaterialIconView { 14 | public IconButton(Context context) { 15 | super(context); 16 | } 17 | 18 | public IconButton(Context context, AttributeSet attrs) { 19 | super(context, attrs); 20 | } 21 | 22 | public IconButton(Context context, AttributeSet attrs, int defStyle) { 23 | super(context, attrs, defStyle); 24 | } 25 | 26 | @Override 27 | public void setPressed(boolean pressed) { 28 | super.setPressed(pressed); 29 | if (pressed) { 30 | setColor(getResources().getColor(R.color.white)); 31 | setBackgroundColor(getResources().getColor(R.color.colorPrimary)); 32 | } else if (!isSelected()) { 33 | setColor(getResources().getColor(R.color.colorPrimary)); 34 | setBackgroundColor(getResources().getColor(R.color.transparent)); 35 | } 36 | } 37 | 38 | @Override 39 | public void setSelected(boolean selected) { 40 | super.setSelected(selected); 41 | if (selected) { 42 | setColor(getResources().getColor(R.color.white)); 43 | setBackgroundResource(R.color.colorPrimary); 44 | } else { 45 | setColor(getResources().getColor(R.color.colorPrimary)); 46 | setBackgroundResource(R.color.transparent); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/NaviBar.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.view; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.util.AttributeSet; 7 | import android.widget.TextView; 8 | 9 | import com.github.mzule.androidweekly.R; 10 | import com.github.mzule.androidweekly.ui.view.base.BaseLinearLayout; 11 | import com.github.mzule.layoutannotation.Layout; 12 | 13 | import butterknife.Bind; 14 | import butterknife.OnClick; 15 | 16 | /** 17 | * Created by CaoDongping on 4/5/16. 18 | */ 19 | @Layout(R.layout.view_navi_bar) 20 | public class NaviBar extends BaseLinearLayout { 21 | 22 | @Bind(R.id.leftTextView) 23 | TextView leftTextView; 24 | @Bind(R.id.rightTextView) 25 | TextView rightTextView; 26 | 27 | public NaviBar(Context context) { 28 | super(context); 29 | } 30 | 31 | public NaviBar(Context context, AttributeSet attrs) { 32 | super(context, attrs); 33 | } 34 | 35 | public NaviBar(Context context, AttributeSet attrs, int defStyleAttr) { 36 | super(context, attrs, defStyleAttr); 37 | } 38 | 39 | @Override 40 | protected void init(Context context, AttributeSet attrs) { 41 | super.init(context, attrs); 42 | if (attrs != null) { 43 | TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.NaviBar); 44 | leftTextView.setText(a.getString(R.styleable.NaviBar_nb_left_text)); 45 | rightTextView.setText(a.getString(R.styleable.NaviBar_nb_right_text)); 46 | a.recycle(); 47 | } 48 | } 49 | 50 | @OnClick(R.id.backButton) 51 | void back() { 52 | if (getContext() instanceof Activity) { 53 | ((Activity) getContext()).finish(); 54 | } 55 | } 56 | 57 | public void setLeftText(String text) { 58 | leftTextView.setText(text); 59 | } 60 | 61 | public void setRightText(String text) { 62 | rightTextView.setText(text); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/PinnedSectionListView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Sergej Shafarenka, halfbit.de 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file kt 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 | 17 | package com.github.mzule.androidweekly.ui.view; 18 | 19 | import android.content.Context; 20 | import android.database.DataSetObserver; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.PointF; 24 | import android.graphics.Rect; 25 | import android.graphics.drawable.GradientDrawable; 26 | import android.graphics.drawable.GradientDrawable.Orientation; 27 | import android.os.Parcelable; 28 | import android.util.AttributeSet; 29 | import android.view.MotionEvent; 30 | import android.view.SoundEffectConstants; 31 | import android.view.View; 32 | import android.view.ViewConfiguration; 33 | import android.view.accessibility.AccessibilityEvent; 34 | import android.widget.AbsListView; 35 | import android.widget.HeaderViewListAdapter; 36 | import android.widget.ListAdapter; 37 | import android.widget.ListView; 38 | import android.widget.SectionIndexer; 39 | 40 | import com.github.mzule.androidweekly.BuildConfig; 41 | 42 | 43 | /** 44 | * ListView, which is capable to pin section views at its top while the rest is still scrolled. 45 | */ 46 | public class PinnedSectionListView extends ListView { 47 | 48 | //-- inner classes 49 | 50 | // fields used for handling touch events 51 | private final Rect mTouchRect = new Rect(); 52 | private final PointF mTouchPoint = new PointF(); 53 | 54 | //-- class fields 55 | /** 56 | * Delegating listener, can be null. 57 | */ 58 | OnScrollListener mDelegateOnScrollListener; 59 | /** 60 | * Shadow for being recycled, can be null. 61 | */ 62 | PinnedSection mRecycleSection; 63 | /** 64 | * shadow instance with a pinned view, can be null. 65 | */ 66 | PinnedSection mPinnedSection; 67 | /** 68 | * Pinned view Y-translation. We use it to stick pinned view to the next section. 69 | */ 70 | int mTranslateY; 71 | private int mTouchSlop; 72 | private View mTouchTarget; 73 | private MotionEvent mDownEvent; 74 | // fields used for drawing shadow under a pinned section 75 | private GradientDrawable mShadowDrawable; 76 | private int mSectionsDistanceY; 77 | /** 78 | * Scroll listener which does the magic 79 | */ 80 | private final OnScrollListener mOnScrollListener = new OnScrollListener() { 81 | 82 | @Override 83 | public void onScrollStateChanged(AbsListView view, int scrollState) { 84 | if (mDelegateOnScrollListener != null) { // delegate 85 | mDelegateOnScrollListener.onScrollStateChanged(view, scrollState); 86 | } 87 | } 88 | 89 | @Override 90 | public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 91 | 92 | if (mDelegateOnScrollListener != null) { // delegate 93 | mDelegateOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); 94 | } 95 | 96 | // get expected adapter or fail fast 97 | ListAdapter adapter = getAdapter(); 98 | if (adapter == null || visibleItemCount == 0) return; // nothing to do 99 | 100 | final boolean isFirstVisibleItemSection = 101 | isItemViewTypePinned(adapter, adapter.getItemViewType(firstVisibleItem)); 102 | 103 | if (isFirstVisibleItemSection) { 104 | View sectionView = getChildAt(0); 105 | if (sectionView.getTop() == getPaddingTop()) { // view sticks to the top, no need for pinned shadow 106 | destroyPinnedShadow(); 107 | } else { // section doesn't stick to the top, make sure we have a pinned shadow 108 | ensureShadowForPosition(firstVisibleItem, firstVisibleItem, visibleItemCount); 109 | } 110 | 111 | } else { // section is not at the first visible position 112 | int sectionPosition = findCurrentSectionPosition(firstVisibleItem); 113 | if (sectionPosition > -1) { // we have section position 114 | ensureShadowForPosition(sectionPosition, firstVisibleItem, visibleItemCount); 115 | } else { // there is no section for the first visible item, destroy shadow 116 | destroyPinnedShadow(); 117 | } 118 | } 119 | } 120 | 121 | ; 122 | 123 | }; 124 | /** 125 | * Default change observer. 126 | */ 127 | private final DataSetObserver mDataSetObserver = new DataSetObserver() { 128 | @Override 129 | public void onChanged() { 130 | recreatePinnedShadow(); 131 | } 132 | 133 | ; 134 | 135 | @Override 136 | public void onInvalidated() { 137 | recreatePinnedShadow(); 138 | } 139 | }; 140 | private int mShadowHeight; 141 | 142 | public PinnedSectionListView(Context context, AttributeSet attrs) { 143 | super(context, attrs); 144 | initView(); 145 | } 146 | 147 | public PinnedSectionListView(Context context, AttributeSet attrs, int defStyle) { 148 | super(context, attrs, defStyle); 149 | initView(); 150 | } 151 | 152 | //-- constructors 153 | 154 | public static boolean isItemViewTypePinned(ListAdapter adapter, int viewType) { 155 | if (adapter instanceof HeaderViewListAdapter) { 156 | adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter(); 157 | } 158 | return ((PinnedSectionListAdapter) adapter).isItemViewTypePinned(viewType); 159 | } 160 | 161 | private void initView() { 162 | setOnScrollListener(mOnScrollListener); 163 | mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 164 | initShadow(false); 165 | } 166 | 167 | public void setShadowVisible(boolean visible) { 168 | initShadow(visible); 169 | if (mPinnedSection != null) { 170 | View v = mPinnedSection.view; 171 | invalidate(v.getLeft(), v.getTop(), v.getRight(), v.getBottom() + mShadowHeight); 172 | } 173 | } 174 | 175 | //-- public API methods 176 | 177 | public void initShadow(boolean visible) { 178 | if (visible) { 179 | if (mShadowDrawable == null) { 180 | mShadowDrawable = new GradientDrawable(Orientation.TOP_BOTTOM, 181 | new int[]{Color.parseColor("#ffa0a0a0"), Color.parseColor("#50a0a0a0"), Color.parseColor("#00a0a0a0")}); 182 | mShadowHeight = (int) (8 * getResources().getDisplayMetrics().density); 183 | } 184 | } else { 185 | if (mShadowDrawable != null) { 186 | mShadowDrawable = null; 187 | mShadowHeight = 0; 188 | } 189 | } 190 | } 191 | 192 | //-- pinned section drawing methods 193 | 194 | /** 195 | * Create shadow wrapper with a pinned view for a view at given position 196 | */ 197 | void createPinnedShadow(int position) { 198 | 199 | // try to recycle shadow 200 | PinnedSection pinnedShadow = mRecycleSection; 201 | mRecycleSection = null; 202 | 203 | // create new shadow, if needed 204 | if (pinnedShadow == null) pinnedShadow = new PinnedSection(); 205 | // request new view using recycled view, if such 206 | View pinnedView = getAdapter().getView(position, pinnedShadow.view, PinnedSectionListView.this); 207 | 208 | // read layout parameters 209 | LayoutParams layoutParams = (LayoutParams) pinnedView.getLayoutParams(); 210 | if (layoutParams == null) { 211 | layoutParams = (LayoutParams) generateDefaultLayoutParams(); 212 | pinnedView.setLayoutParams(layoutParams); 213 | } 214 | 215 | int heightMode = MeasureSpec.getMode(layoutParams.height); 216 | int heightSize = MeasureSpec.getSize(layoutParams.height); 217 | 218 | if (heightMode == MeasureSpec.UNSPECIFIED) heightMode = MeasureSpec.EXACTLY; 219 | 220 | int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom(); 221 | if (heightSize > maxHeight) heightSize = maxHeight; 222 | 223 | // measure & layout 224 | int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(), MeasureSpec.EXACTLY); 225 | int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode); 226 | pinnedView.measure(ws, hs); 227 | pinnedView.layout(0, 0, pinnedView.getMeasuredWidth(), pinnedView.getMeasuredHeight()); 228 | mTranslateY = 0; 229 | 230 | // initialize pinned shadow 231 | pinnedShadow.view = pinnedView; 232 | pinnedShadow.position = position; 233 | pinnedShadow.id = getAdapter().getItemId(position); 234 | 235 | // store pinned shadow 236 | mPinnedSection = pinnedShadow; 237 | } 238 | 239 | /** 240 | * Destroy shadow wrapper for currently pinned view 241 | */ 242 | void destroyPinnedShadow() { 243 | if (mPinnedSection != null) { 244 | // keep shadow for being recycled later 245 | mRecycleSection = mPinnedSection; 246 | mPinnedSection = null; 247 | } 248 | } 249 | 250 | /** 251 | * Makes sure we have an actual pinned shadow for given position. 252 | */ 253 | void ensureShadowForPosition(int sectionPosition, int firstVisibleItem, int visibleItemCount) { 254 | if (visibleItemCount < 2) { // no need for creating shadow at all, we have a single visible item 255 | destroyPinnedShadow(); 256 | return; 257 | } 258 | 259 | if (mPinnedSection != null 260 | && mPinnedSection.position != sectionPosition) { // invalidate shadow, if required 261 | destroyPinnedShadow(); 262 | } 263 | 264 | if (mPinnedSection == null) { // create shadow, if empty 265 | createPinnedShadow(sectionPosition); 266 | } 267 | 268 | // align shadow according to next section position, if needed 269 | int nextPosition = sectionPosition + 1; 270 | if (nextPosition < getCount()) { 271 | int nextSectionPosition = findFirstVisibleSectionPosition(nextPosition, 272 | visibleItemCount - (nextPosition - firstVisibleItem)); 273 | if (nextSectionPosition > -1) { 274 | View nextSectionView = getChildAt(nextSectionPosition - firstVisibleItem); 275 | final int bottom = mPinnedSection.view.getBottom() + getPaddingTop(); 276 | mSectionsDistanceY = nextSectionView.getTop() - bottom; 277 | if (mSectionsDistanceY < 0) { 278 | // next section overlaps pinned shadow, move it up 279 | mTranslateY = mSectionsDistanceY; 280 | } else { 281 | // next section does not overlap with pinned, stick to top 282 | mTranslateY = 0; 283 | } 284 | } else { 285 | // no other sections are visible, stick to top 286 | mTranslateY = 0; 287 | mSectionsDistanceY = Integer.MAX_VALUE; 288 | } 289 | } 290 | 291 | } 292 | 293 | int findFirstVisibleSectionPosition(int firstVisibleItem, int visibleItemCount) { 294 | ListAdapter adapter = getAdapter(); 295 | 296 | int adapterDataCount = adapter.getCount(); 297 | if (getLastVisiblePosition() >= adapterDataCount) 298 | return -1; // dataset has changed, no candidate 299 | 300 | if (firstVisibleItem + visibleItemCount >= adapterDataCount) {//added to prevent index Outofbound (in case) 301 | visibleItemCount = adapterDataCount - firstVisibleItem; 302 | } 303 | 304 | for (int childIndex = 0; childIndex < visibleItemCount; childIndex++) { 305 | int position = firstVisibleItem + childIndex; 306 | int viewType = adapter.getItemViewType(position); 307 | if (isItemViewTypePinned(adapter, viewType)) return position; 308 | } 309 | return -1; 310 | } 311 | 312 | int findCurrentSectionPosition(int fromPosition) { 313 | ListAdapter adapter = getAdapter(); 314 | 315 | if (fromPosition >= adapter.getCount()) return -1; // dataset has changed, no candidate 316 | 317 | if (adapter instanceof SectionIndexer) { 318 | // try fast way by asking section indexer 319 | SectionIndexer indexer = (SectionIndexer) adapter; 320 | int sectionPosition = indexer.getSectionForPosition(fromPosition); 321 | int itemPosition = indexer.getPositionForSection(sectionPosition); 322 | int typeView = adapter.getItemViewType(itemPosition); 323 | if (isItemViewTypePinned(adapter, typeView)) { 324 | return itemPosition; 325 | } // else, no luck 326 | } 327 | 328 | // try slow way by looking through to the next section item above 329 | for (int position = fromPosition; position >= 0; position--) { 330 | int viewType = adapter.getItemViewType(position); 331 | if (isItemViewTypePinned(adapter, viewType)) return position; 332 | } 333 | return -1; // no candidate found 334 | } 335 | 336 | void recreatePinnedShadow() { 337 | destroyPinnedShadow(); 338 | ListAdapter adapter = getAdapter(); 339 | if (adapter != null && adapter.getCount() > 0) { 340 | int firstVisiblePosition = getFirstVisiblePosition(); 341 | int sectionPosition = findCurrentSectionPosition(firstVisiblePosition); 342 | if (sectionPosition == -1) return; // no views to pin, exit 343 | ensureShadowForPosition(sectionPosition, 344 | firstVisiblePosition, getLastVisiblePosition() - firstVisiblePosition); 345 | } 346 | } 347 | 348 | @Override 349 | public void setOnScrollListener(OnScrollListener listener) { 350 | if (listener == mOnScrollListener) { 351 | super.setOnScrollListener(listener); 352 | } else { 353 | mDelegateOnScrollListener = listener; 354 | } 355 | } 356 | 357 | @Override 358 | public void onRestoreInstanceState(Parcelable state) { 359 | super.onRestoreInstanceState(state); 360 | post(new Runnable() { 361 | @Override 362 | public void run() { // restore pinned view after configuration change 363 | recreatePinnedShadow(); 364 | } 365 | }); 366 | } 367 | 368 | @Override 369 | public void setAdapter(ListAdapter adapter) { 370 | 371 | // assert adapter in debug mode 372 | if (BuildConfig.DEBUG && adapter != null) { 373 | if (!(adapter instanceof PinnedSectionListAdapter)) 374 | throw new IllegalArgumentException("Does your adapter implement PinnedSectionListAdapter?"); 375 | if (adapter.getViewTypeCount() < 2) 376 | throw new IllegalArgumentException("Does your adapter handle at least two types" + 377 | " of views in getViewTypeCount() method: items and sections?"); 378 | } 379 | 380 | // unregister observer at old adapter and register on new one 381 | ListAdapter oldAdapter = getAdapter(); 382 | if (oldAdapter != null) oldAdapter.unregisterDataSetObserver(mDataSetObserver); 383 | if (adapter != null) adapter.registerDataSetObserver(mDataSetObserver); 384 | 385 | // destroy pinned shadow, if new adapter is not same as old one 386 | if (oldAdapter != adapter) destroyPinnedShadow(); 387 | 388 | super.setAdapter(adapter); 389 | } 390 | 391 | @Override 392 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 393 | super.onLayout(changed, l, t, r, b); 394 | if (mPinnedSection != null) { 395 | int parentWidth = r - l - getPaddingLeft() - getPaddingRight(); 396 | int shadowWidth = mPinnedSection.view.getWidth(); 397 | if (parentWidth != shadowWidth) { 398 | recreatePinnedShadow(); 399 | } 400 | } 401 | } 402 | 403 | @Override 404 | protected void dispatchDraw(Canvas canvas) { 405 | super.dispatchDraw(canvas); 406 | 407 | if (mPinnedSection != null) { 408 | 409 | // prepare variables 410 | int pLeft = getListPaddingLeft(); 411 | int pTop = getListPaddingTop(); 412 | View view = mPinnedSection.view; 413 | 414 | // draw child 415 | canvas.save(); 416 | 417 | int clipHeight = view.getHeight() + 418 | (mShadowDrawable == null ? 0 : Math.min(mShadowHeight, mSectionsDistanceY)); 419 | canvas.clipRect(pLeft, pTop, pLeft + view.getWidth(), pTop + clipHeight); 420 | 421 | canvas.translate(pLeft, pTop + mTranslateY); 422 | drawChild(canvas, mPinnedSection.view, getDrawingTime()); 423 | 424 | if (mShadowDrawable != null && mSectionsDistanceY > 0) { 425 | mShadowDrawable.setBounds(mPinnedSection.view.getLeft(), 426 | mPinnedSection.view.getBottom(), 427 | mPinnedSection.view.getRight(), 428 | mPinnedSection.view.getBottom() + mShadowHeight); 429 | mShadowDrawable.draw(canvas); 430 | } 431 | 432 | canvas.restore(); 433 | } 434 | } 435 | 436 | @Override 437 | public boolean dispatchTouchEvent(MotionEvent ev) { 438 | 439 | final float x = ev.getX(); 440 | final float y = ev.getY(); 441 | final int action = ev.getAction(); 442 | 443 | if (action == MotionEvent.ACTION_DOWN 444 | && mTouchTarget == null 445 | && mPinnedSection != null 446 | && isPinnedViewTouched(mPinnedSection.view, x, y)) { // create touch target 447 | 448 | // user touched pinned view 449 | mTouchTarget = mPinnedSection.view; 450 | mTouchPoint.x = x; 451 | mTouchPoint.y = y; 452 | 453 | // copy down event for eventually be used later 454 | mDownEvent = MotionEvent.obtain(ev); 455 | } 456 | 457 | if (mTouchTarget != null) { 458 | if (isPinnedViewTouched(mTouchTarget, x, y)) { // forward event to pinned view 459 | mTouchTarget.dispatchTouchEvent(ev); 460 | } 461 | 462 | if (action == MotionEvent.ACTION_UP) { // perform onClick on pinned view 463 | super.dispatchTouchEvent(ev); 464 | performPinnedItemClick(); 465 | clearTouchTarget(); 466 | 467 | } else if (action == MotionEvent.ACTION_CANCEL) { // cancel 468 | clearTouchTarget(); 469 | 470 | } else if (action == MotionEvent.ACTION_MOVE) { 471 | if (Math.abs(y - mTouchPoint.y) > mTouchSlop) { 472 | 473 | // cancel sequence on touch target 474 | MotionEvent event = MotionEvent.obtain(ev); 475 | event.setAction(MotionEvent.ACTION_CANCEL); 476 | mTouchTarget.dispatchTouchEvent(event); 477 | event.recycle(); 478 | 479 | // provide correct sequence to super class for further handling 480 | super.dispatchTouchEvent(mDownEvent); 481 | super.dispatchTouchEvent(ev); 482 | clearTouchTarget(); 483 | 484 | } 485 | } 486 | 487 | return true; 488 | } 489 | 490 | // call super if this was not our pinned view 491 | return super.dispatchTouchEvent(ev); 492 | } 493 | 494 | //-- touch handling methods 495 | 496 | private boolean isPinnedViewTouched(View view, float x, float y) { 497 | view.getHitRect(mTouchRect); 498 | 499 | // by taping top or bottom padding, the list performs on click on a border item. 500 | // we don't add top padding here to keep behavior consistent. 501 | mTouchRect.top += mTranslateY; 502 | 503 | mTouchRect.bottom += mTranslateY + getPaddingTop(); 504 | mTouchRect.left += getPaddingLeft(); 505 | mTouchRect.right -= getPaddingRight(); 506 | return mTouchRect.contains((int) x, (int) y); 507 | } 508 | 509 | private void clearTouchTarget() { 510 | mTouchTarget = null; 511 | if (mDownEvent != null) { 512 | mDownEvent.recycle(); 513 | mDownEvent = null; 514 | } 515 | } 516 | 517 | private boolean performPinnedItemClick() { 518 | if (mPinnedSection == null) return false; 519 | 520 | OnItemClickListener listener = getOnItemClickListener(); 521 | if (listener != null && getAdapter().isEnabled(mPinnedSection.position)) { 522 | View view = mPinnedSection.view; 523 | playSoundEffect(SoundEffectConstants.CLICK); 524 | if (view != null) { 525 | view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 526 | } 527 | listener.onItemClick(this, view, mPinnedSection.position, mPinnedSection.id); 528 | return true; 529 | } 530 | return false; 531 | } 532 | 533 | /** 534 | * List adapter to be implemented for being used with PinnedSectionListView adapter. 535 | */ 536 | public static interface PinnedSectionListAdapter extends ListAdapter { 537 | /** 538 | * This method shall return 'true' if views of given type has to be pinned. 539 | */ 540 | boolean isItemViewTypePinned(int viewType); 541 | } 542 | 543 | /** 544 | * Wrapper class for pinned section view and its position in the list. 545 | */ 546 | static class PinnedSection { 547 | public View view; 548 | public int position; 549 | public long id; 550 | } 551 | 552 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/PopupView.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.view; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorListenerAdapter; 5 | import android.animation.ObjectAnimator; 6 | import android.app.Activity; 7 | import android.content.Context; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.view.animation.AccelerateInterpolator; 11 | import android.widget.FrameLayout; 12 | 13 | import com.github.mzule.androidweekly.R; 14 | import com.github.mzule.androidweekly.ui.view.base.BaseRelativeLayout; 15 | 16 | 17 | /** 18 | * Created by CaoDongping on 11/30/15. 19 | */ 20 | public abstract class PopupView extends BaseRelativeLayout { 21 | public static final int ANIMATION_DIRECTION_BOTTOM_TO_TOP = 0; 22 | public static final int ANIMATION_DIRECTION_TOP_TO_BOTTOM = 1; 23 | public static final int ANIMATION_DIRECTION_LEFT_TO_RIGHT = 2; 24 | public static final int ANIMATION_DIRECTION_RIGHT_TO_LEFT = 3; 25 | public static final int ANIMATE_DURATION = 200; 26 | private boolean inAnimation; 27 | 28 | public PopupView(Context context) { 29 | super(context, null, 0); 30 | } 31 | 32 | /** 33 | * 显示到Activity上面去 34 | * 35 | * @param activity 36 | * @param data 37 | * @return 38 | */ 39 | public PopupView attachTo(Activity activity, T data) { 40 | FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 41 | return attachTo(activity, data, lp); 42 | } 43 | 44 | /** 45 | * 显示到Activity上面去 46 | * 47 | * @param activity 48 | * @param data 49 | * @param layoutParams 50 | * @return 51 | */ 52 | public PopupView attachTo(Activity activity, T data, FrameLayout.LayoutParams layoutParams) { 53 | setId(R.id.popup_view_id); 54 | ViewGroup contentView = (ViewGroup) activity.findViewById(android.R.id.content); 55 | if (null != contentView.findViewById(R.id.popup_view_id)) { 56 | return this; 57 | } 58 | render(data); 59 | contentView.addView(this, layoutParams); 60 | animateIn(); 61 | return this; 62 | } 63 | 64 | protected int getAnimationDirection() { 65 | return ANIMATION_DIRECTION_BOTTOM_TO_TOP; 66 | } 67 | 68 | /** 69 | * 动画显示 70 | */ 71 | protected void animateIn() { 72 | if (inAnimation) { 73 | return; 74 | } 75 | setVisibility(INVISIBLE); 76 | inAnimation = true; 77 | post(new Runnable() { 78 | @Override 79 | public void run() { 80 | setVisibility(VISIBLE); 81 | ObjectAnimator alpha = ObjectAnimator.ofFloat(getMaskView(), "alpha", 0, 1); 82 | alpha.setDuration(ANIMATE_DURATION); 83 | alpha.start(); 84 | int measuredHeight = getMainView().getMeasuredHeight(); 85 | int measuredWidth = getMainView().getMeasuredWidth(); 86 | ObjectAnimator translate; 87 | switch (getAnimationDirection()) { 88 | case ANIMATION_DIRECTION_BOTTOM_TO_TOP: 89 | translate = ObjectAnimator.ofFloat(getMainView(), "translationY", measuredHeight, 0); 90 | break; 91 | case ANIMATION_DIRECTION_TOP_TO_BOTTOM: 92 | translate = ObjectAnimator.ofFloat(getMainView(), "translationY", -measuredHeight, 0); 93 | break; 94 | case ANIMATION_DIRECTION_LEFT_TO_RIGHT: 95 | translate = ObjectAnimator.ofFloat(getMainView(), "translationX", -measuredWidth, 0); 96 | break; 97 | case ANIMATION_DIRECTION_RIGHT_TO_LEFT: 98 | translate = ObjectAnimator.ofFloat(getMainView(), "translationX", measuredWidth, 0); 99 | break; 100 | default: 101 | throw new RuntimeException("unknown animation direction"); 102 | } 103 | translate.setInterpolator(new AccelerateInterpolator()); 104 | translate.setDuration(ANIMATE_DURATION); 105 | translate.start(); 106 | translate.addListener(new AnimatorListenerAdapter() { 107 | @Override 108 | public void onAnimationEnd(Animator animation) { 109 | inAnimation = false; 110 | } 111 | }); 112 | } 113 | }); 114 | } 115 | 116 | public void finish() { 117 | finish(null); 118 | } 119 | 120 | /** 121 | * 从当前Activity上移除 122 | */ 123 | public void finish(final FinishCallback callback) { 124 | if (!inAnimation) { 125 | inAnimation = true; 126 | ObjectAnimator alpha = ObjectAnimator.ofFloat(getMainView(), "alpha", 1, 0); 127 | alpha.setDuration(ANIMATE_DURATION); 128 | alpha.start(); 129 | int measuredHeight = getMainView().getMeasuredHeight(); 130 | int measuredWidth = getMainView().getMeasuredWidth(); 131 | ObjectAnimator translate; 132 | switch (getAnimationDirection()) { 133 | case ANIMATION_DIRECTION_BOTTOM_TO_TOP: 134 | translate = ObjectAnimator.ofFloat(getMainView(), "translationY", 0, measuredHeight); 135 | break; 136 | case ANIMATION_DIRECTION_TOP_TO_BOTTOM: 137 | translate = ObjectAnimator.ofFloat(getMainView(), "translationY", 0, -measuredHeight); 138 | break; 139 | case ANIMATION_DIRECTION_LEFT_TO_RIGHT: 140 | translate = ObjectAnimator.ofFloat(getMainView(), "translationX", 0, -measuredWidth); 141 | break; 142 | case ANIMATION_DIRECTION_RIGHT_TO_LEFT: 143 | translate = ObjectAnimator.ofFloat(getMainView(), "translationX", 0, measuredWidth); 144 | break; 145 | default: 146 | throw new RuntimeException("unknown animation direction"); 147 | } 148 | translate.setInterpolator(new AccelerateInterpolator()); 149 | translate.setDuration(ANIMATE_DURATION); 150 | translate.start(); 151 | translate.addListener(new AnimatorListenerAdapter() { 152 | @Override 153 | public void onAnimationEnd(Animator animation) { 154 | if (getParent() instanceof ViewGroup) { 155 | ((ViewGroup) getParent()).removeView(PopupView.this); 156 | } 157 | inAnimation = false; 158 | if (callback != null) { 159 | callback.afterFinish(); 160 | } 161 | } 162 | }); 163 | } 164 | } 165 | 166 | /** 167 | * 渲染数据到UI 168 | * 169 | * @param data 170 | */ 171 | protected abstract void render(T data); 172 | 173 | /** 174 | * 获取需要fadeIn显示的遮罩视图 175 | * 176 | * @return 177 | */ 178 | protected abstract View getMaskView(); 179 | 180 | 181 | /** 182 | * 获取需要translation的主UI视图 183 | * 184 | * @return 185 | */ 186 | protected abstract View getMainView(); 187 | 188 | public interface FinishCallback { 189 | void afterFinish(); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/ProgressView.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import com.github.mzule.androidweekly.R; 9 | import com.github.mzule.androidweekly.util.DensityUtil; 10 | import com.pnikosis.materialishprogress.ProgressWheel; 11 | 12 | 13 | /** 14 | * Created by CaoDongping on 1/5/16. 15 | */ 16 | public class ProgressView extends ProgressWheel { 17 | public ProgressView(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | init(); 20 | } 21 | 22 | public ProgressView(Context context) { 23 | super(context); 24 | init(); 25 | } 26 | 27 | private void init() { 28 | if (isInEditMode()) { 29 | return; 30 | } 31 | setBarColor(getResources().getColor(R.color.colorPrimary)); 32 | setBarWidth(DensityUtil.dp2px(2)); 33 | spin(); 34 | } 35 | 36 | public void start() { 37 | setVisibility(VISIBLE); 38 | } 39 | 40 | public void stop() { 41 | setVisibility(View.GONE); 42 | } 43 | 44 | @Override 45 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 46 | if (!isInEditMode() && getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) { 47 | super.onMeasure(DensityUtil.dp2px(40), DensityUtil.dp2px(40)); 48 | } else { 49 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/TintStatusBar.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | 8 | import com.github.mzule.androidweekly.util.Tinter; 9 | 10 | /** 11 | * 占位符View,在支持Tint的设备上,高度为信号栏的高度,在不支持Tint的设备上,高度为0,可以用来作为UI布局的占位符 12 | * Created by CaoDongping on 11/11/15. 13 | */ 14 | public class TintStatusBar extends View { 15 | private int statusBarHeight; 16 | 17 | public TintStatusBar(Context context) { 18 | super(context); 19 | init(); 20 | } 21 | 22 | public TintStatusBar(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | init(); 25 | } 26 | 27 | public TintStatusBar(Context context, AttributeSet attrs, int defStyleAttr) { 28 | super(context, attrs, defStyleAttr); 29 | init(); 30 | } 31 | 32 | public void init() { 33 | statusBarHeight = getStatusBarHeight(); 34 | } 35 | 36 | @Override 37 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 38 | if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) { 39 | int height = 0; 40 | if (Tinter.isSupport()) { 41 | height = statusBarHeight; 42 | } 43 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 44 | } 45 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 46 | } 47 | 48 | public int getStatusBarHeight() { 49 | int result = 0; 50 | int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); 51 | if (resourceId > 0) { 52 | result = getResources().getDimensionPixelSize(resourceId); 53 | } 54 | return result; 55 | } 56 | 57 | public void setHeightPercent(float percent) { 58 | ViewGroup.LayoutParams lp = getLayoutParams(); 59 | lp.height = (int) (statusBarHeight * percent); 60 | setLayoutParams(lp); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/TranslateView.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.view; 2 | 3 | import android.app.Activity; 4 | import android.content.ClipData; 5 | import android.content.ClipboardManager; 6 | import android.content.Context; 7 | import android.support.annotation.Nullable; 8 | import android.text.TextUtils; 9 | import android.util.Log; 10 | import android.view.View; 11 | import android.widget.EditText; 12 | 13 | import com.github.mzule.androidweekly.R; 14 | import com.github.mzule.androidweekly.api.ApiCallback; 15 | import com.github.mzule.androidweekly.api.DictionaryApi; 16 | import com.github.mzule.androidweekly.entity.Dict; 17 | import com.github.mzule.androidweekly.util.Keyboard; 18 | import com.github.mzule.layoutannotation.Layout; 19 | 20 | import butterknife.Bind; 21 | import butterknife.OnClick; 22 | 23 | /** 24 | * Created by CaoDongping on 3/30/16. 25 | */ 26 | @Layout(R.layout.view_translate) 27 | public class TranslateView extends PopupView { 28 | @Bind(R.id.queryInput) 29 | EditText queryInput; 30 | @Bind(R.id.resultView) 31 | EditText resultView; 32 | @Bind(R.id.buttonPanel) 33 | View buttonPanel; 34 | 35 | public TranslateView(Context context) { 36 | super(context); 37 | } 38 | 39 | @OnClick(R.id.maskView) 40 | @Override 41 | public void finish() { 42 | super.finish(); 43 | hideKeyboard(); 44 | } 45 | 46 | private void hideKeyboard() { 47 | if (getContext() instanceof Activity) { 48 | Keyboard.hide((Activity) getContext()); 49 | } 50 | } 51 | 52 | @OnClick(R.id.clearButton) 53 | void clear() { 54 | queryInput.setText(""); 55 | resultView.setText(""); 56 | queryInput.requestFocus(); 57 | Keyboard.show(getContext()); 58 | } 59 | 60 | @OnClick(R.id.translateButton) 61 | void translate() { 62 | new DictionaryApi().look(queryInput.getText().toString().toLowerCase(), new ApiCallback() { 63 | @Override 64 | public void onSuccess(Dict data, boolean fromCache) { 65 | resultView.setText(data.getContent()); 66 | resultView.setSelection(0); 67 | hideKeyboard(); 68 | updateResultViewMaxHeight(); 69 | } 70 | 71 | @Override 72 | public void onFailure(Exception e) { 73 | resultView.setText(Log.getStackTraceString(e)); 74 | } 75 | }); 76 | } 77 | 78 | @Override 79 | protected void render(Void data) { 80 | String paste = getPasteText(); 81 | queryInput.requestFocus(); 82 | if (!TextUtils.isEmpty(paste)) { 83 | queryInput.setText(paste); 84 | queryInput.setSelection(paste.length()); 85 | translate(); 86 | } 87 | } 88 | 89 | private void updateResultViewMaxHeight() { 90 | int height = getResources().getDisplayMetrics().heightPixels; 91 | resultView.setMaxHeight(height - queryInput.getBottom() - buttonPanel.getMeasuredHeight() - queryInput.getPaddingLeft()); 92 | } 93 | 94 | @Nullable 95 | private String getPasteText() { 96 | String paste = null; 97 | ClipboardManager manager = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE); 98 | ClipData clip = manager.getPrimaryClip(); 99 | if (clip != null && clip.getItemCount() > 0 && clip.getItemAt(0) != null) { 100 | CharSequence text = clip.getItemAt(0).getText(); 101 | paste = text == null ? "" : text.toString(); 102 | } 103 | return paste; 104 | } 105 | 106 | @Override 107 | protected View getMaskView() { 108 | return findViewById(R.id.maskView); 109 | } 110 | 111 | @Override 112 | protected View getMainView() { 113 | return findViewById(R.id.mainView); 114 | } 115 | 116 | @Override 117 | protected int getAnimationDirection() { 118 | return ANIMATION_DIRECTION_TOP_TO_BOTTOM; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/base/BaseLinearLayout.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.view.base; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.LinearLayout; 6 | 7 | import com.github.mzule.layoutannotation.LayoutBinder; 8 | 9 | import butterknife.ButterKnife; 10 | 11 | /** 12 | * Created by CaoDongping on 8/30/16. 13 | */ 14 | 15 | public class BaseLinearLayout extends LinearLayout { 16 | 17 | public BaseLinearLayout(Context context) { 18 | super(context); 19 | init(context, null); 20 | } 21 | 22 | public BaseLinearLayout(Context context, AttributeSet attrs) { 23 | super(context, attrs); 24 | init(context, attrs); 25 | } 26 | 27 | public BaseLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { 28 | super(context, attrs, defStyleAttr); 29 | init(context, attrs); 30 | } 31 | 32 | protected void init(Context context, AttributeSet attrs) { 33 | LayoutBinder.bind(this); 34 | ButterKnife.bind(this); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/view/base/BaseRelativeLayout.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.view.base; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.RelativeLayout; 6 | 7 | import com.github.mzule.layoutannotation.LayoutBinder; 8 | 9 | import butterknife.ButterKnife; 10 | 11 | /** 12 | * Created by CaoDongping on 8/30/16. 13 | */ 14 | 15 | public class BaseRelativeLayout extends RelativeLayout { 16 | 17 | public BaseRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { 18 | super(context, attrs, defStyleAttr); 19 | init(); 20 | } 21 | 22 | private void init() { 23 | LayoutBinder.bind(this); 24 | ButterKnife.bind(this); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/viewtype/ArticleViewType.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.viewtype; 2 | 3 | import android.text.TextUtils; 4 | import android.view.View; 5 | import android.widget.ImageView; 6 | import android.widget.TextView; 7 | 8 | import com.bumptech.glide.Glide; 9 | import com.github.mzule.androidweekly.R; 10 | import com.github.mzule.androidweekly.entity.Article; 11 | import com.github.mzule.layoutannotation.Layout; 12 | 13 | import butterknife.Bind; 14 | 15 | /** 16 | * Created by CaoDongping on 3/24/16. 17 | */ 18 | @Layout(R.layout.item_article) 19 | public class ArticleViewType extends BaseViewType
{ 20 | @Bind(R.id.nameView) 21 | TextView nameView; 22 | @Bind(R.id.briefView) 23 | TextView briefView; 24 | @Bind(R.id.domainView) 25 | TextView domainView; 26 | @Bind(R.id.iconView) 27 | ImageView iconView; 28 | 29 | @Override 30 | public void onRender(int position, Article data) { 31 | nameView.setText(data.getTitle()); 32 | briefView.setText(data.getBrief()); 33 | domainView.setText(data.getDomain()); 34 | Glide.with(getContext()).load(data.getImageUrl()).centerCrop().placeholder(R.color.placeholder).into(iconView); 35 | iconView.setVisibility(TextUtils.isEmpty(data.getImageUrl()) ? View.GONE : View.VISIBLE); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/viewtype/BaseViewType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Baidu, Inc. All Rights Reserved. 3 | */ 4 | package com.github.mzule.androidweekly.ui.viewtype; 5 | 6 | import com.github.mzule.easyadapter.ViewType; 7 | import com.github.mzule.layoutannotation.LayoutBinder; 8 | 9 | import butterknife.ButterKnife; 10 | 11 | /** 12 | * Created by CaoDongping on 8/29/16. 13 | */ 14 | 15 | public abstract class BaseViewType extends ViewType { 16 | @Override 17 | public void onCreate() { 18 | setContentView(LayoutBinder.inflate(getContext(), this)); 19 | ButterKnife.bind(this, getView()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/viewtype/SearchHistoryViewType.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.viewtype; 2 | 3 | import android.widget.TextView; 4 | 5 | import com.github.mzule.androidweekly.R; 6 | import com.github.mzule.layoutannotation.Layout; 7 | 8 | import butterknife.Bind; 9 | 10 | /** 11 | * Created by CaoDongping on 4/5/16. 12 | */ 13 | @Layout(R.layout.item_search_history) 14 | public class SearchHistoryViewType extends BaseViewType { 15 | @Bind(R.id.textView) 16 | TextView textView; 17 | 18 | @Override 19 | public void onRender(int position, String data) { 20 | textView.setText(data); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/viewtype/SectionViewType.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.viewtype; 2 | 3 | import android.widget.TextView; 4 | 5 | import com.github.mzule.androidweekly.R; 6 | import com.github.mzule.layoutannotation.Layout; 7 | 8 | import butterknife.Bind; 9 | 10 | /** 11 | * Created by CaoDongping on 3/24/16. 12 | */ 13 | @Layout(R.layout.item_section) 14 | public class SectionViewType extends BaseViewType { 15 | @Bind(R.id.sectionName) 16 | TextView sectionName; 17 | 18 | @Override 19 | public void onRender(int position, String data) { 20 | sectionName.setText(data); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/ui/viewtype/SlideIssueViewType.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.ui.viewtype; 2 | 3 | import android.view.View; 4 | import android.widget.TextView; 5 | 6 | import com.github.mzule.androidweekly.R; 7 | import com.github.mzule.androidweekly.entity.Issue; 8 | import com.github.mzule.layoutannotation.Layout; 9 | 10 | import butterknife.Bind; 11 | 12 | /** 13 | * Created by CaoDongping on 3/25/16. 14 | */ 15 | @Layout(R.layout.item_slide_issue) 16 | public class SlideIssueViewType extends BaseViewType { 17 | @Bind(R.id.nameView) 18 | TextView nameView; 19 | @Bind(R.id.dateView) 20 | TextView dateView; 21 | @Bind(R.id.rootLayout) 22 | View rootLayout; 23 | 24 | @Override 25 | public void onRender(int position, Issue data) { 26 | nameView.setText(data.getName()); 27 | nameView.setSelected(data.isActive()); 28 | dateView.setText(data.getDate()); 29 | dateView.setSelected(data.isActive()); 30 | int color = getContext().getResources().getColor(data.isActive() ? R.color.colorPrimary : R.color.transparent); 31 | rootLayout.setBackgroundColor(color); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/DateUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | import android.annotation.SuppressLint; 4 | 5 | import java.text.DateFormat; 6 | import java.text.SimpleDateFormat; 7 | 8 | /** 9 | * Created by CaoDongping on 3/28/16. 10 | */ 11 | public class DateUtil { 12 | @SuppressLint("SimpleDateFormat") 13 | private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); 14 | 15 | public static String format(long time) { 16 | return format.format(time); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/DensityUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | 4 | import com.github.mzule.androidweekly.App; 5 | 6 | /** 7 | * Created by CaoDongping on 11/4/15. 8 | */ 9 | public class DensityUtil { 10 | public static int dp2px(float dp) { 11 | float density = App.getInstance().getResources().getDisplayMetrics().density; 12 | return (int) (dp * density + 0.5f); 13 | } 14 | 15 | public static float px2dp(float px) { 16 | float density = App.getInstance().getResources().getDisplayMetrics().density; 17 | return px / density; 18 | } 19 | 20 | public static int screenWidth() { 21 | return App.getInstance().getResources().getDisplayMetrics().widthPixels; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/DictParser.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | import android.util.Xml; 4 | 5 | import com.github.mzule.androidweekly.entity.Dict; 6 | 7 | import org.xmlpull.v1.XmlPullParser; 8 | import org.xmlpull.v1.XmlPullParserException; 9 | 10 | import java.io.IOException; 11 | import java.net.URL; 12 | 13 | /** 14 | * Created by CaoDongping on 3/31/16. 15 | */ 16 | public class DictParser { 17 | private Dict dict; 18 | private String url; 19 | 20 | public DictParser(String url) { 21 | this.url = url; 22 | } 23 | 24 | public Dict parse() throws IOException, XmlPullParserException { 25 | XmlPullParser parser = Xml.newPullParser(); 26 | parser.setInput(new URL(url).openStream(), null); 27 | parser.nextTag(); 28 | dict = new Dict(); 29 | readDict(parser); 30 | return dict; 31 | } 32 | 33 | private void readDict(XmlPullParser parser) throws IOException, XmlPullParserException { 34 | parser.require(XmlPullParser.START_TAG, null, "dict"); 35 | while (parser.next() != XmlPullParser.END_TAG) { 36 | if (parser.getEventType() != XmlPullParser.START_TAG) { 37 | continue; 38 | } 39 | String name = parser.getName(); 40 | switch (name) { 41 | case "key": 42 | dict.setKey(readText(parser)); 43 | break; 44 | case "ps": 45 | dict.addPs(readText(parser)); 46 | break; 47 | case "pron": 48 | dict.addPron(readText(parser)); 49 | break; 50 | case "pos": 51 | dict.addPos(readText(parser)); 52 | break; 53 | case "acceptation": 54 | dict.addAcceptation(readText(parser)); 55 | break; 56 | case "fy": 57 | dict.setFy(readText(parser)); 58 | break; 59 | case "sent": 60 | parseSent(parser); 61 | break; 62 | 63 | } 64 | } 65 | } 66 | 67 | private void parseSent(XmlPullParser parser) throws IOException, XmlPullParserException { 68 | Dict.Sent sent = new Dict.Sent(); 69 | parser.require(XmlPullParser.START_TAG, null, "sent"); 70 | while (parser.next() != XmlPullParser.END_TAG) { 71 | if (parser.getEventType() != XmlPullParser.START_TAG) { 72 | continue; 73 | } 74 | String name = parser.getName(); 75 | switch (name) { 76 | case "orig": 77 | sent.setOrig(readText(parser)); 78 | break; 79 | case "trans": 80 | sent.setTrans(readText(parser)); 81 | break; 82 | } 83 | } 84 | dict.addSent(sent); 85 | } 86 | 87 | 88 | private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { 89 | String result = null; 90 | if (parser.next() == XmlPullParser.TEXT) { 91 | result = parser.getText(); 92 | parser.nextTag(); 93 | } 94 | return result == null ? null : result.trim(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/IOUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.Closeable; 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | 10 | /** 11 | * Created by mzule on 4/5/16. 12 | */ 13 | public class IOUtil { 14 | public static void close(Closeable... closeables) { 15 | if (closeables == null) { 16 | return; 17 | } 18 | for (Closeable closeable : closeables) { 19 | if (closeable != null) { 20 | try { 21 | closeable.close(); 22 | } catch (IOException e) { 23 | e.printStackTrace(); 24 | } 25 | } 26 | } 27 | } 28 | 29 | public static void close(AutoCloseable... closeables) { 30 | if (closeables == null) { 31 | return; 32 | } 33 | for (AutoCloseable closeable : closeables) { 34 | if (closeable != null) { 35 | try { 36 | closeable.close(); 37 | } catch (Exception e) { 38 | e.printStackTrace(); 39 | } 40 | } 41 | } 42 | } 43 | 44 | public static String read(File file) throws IOException { 45 | StringBuilder sb = new StringBuilder(); 46 | BufferedReader br = null; 47 | try { 48 | br = new BufferedReader(new InputStreamReader(new FileInputStream(file))); 49 | String line; 50 | while ((line = br.readLine()) != null) { 51 | sb.append(line); 52 | } 53 | } finally { 54 | close(br); 55 | } 56 | return sb.toString(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | import android.support.annotation.WorkerThread; 4 | 5 | import com.google.gson.Gson; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.InputStreamReader; 9 | import java.lang.reflect.Type; 10 | import java.net.URL; 11 | 12 | /** 13 | * Created by CaoDongping on 3/26/16. 14 | */ 15 | public class JsonUtil { 16 | private static final Gson gson = new Gson(); 17 | 18 | public static String toJson(Object obj) { 19 | try { 20 | return gson.toJson(obj); 21 | } catch (Throwable e) { 22 | return ""; 23 | } 24 | } 25 | 26 | public static T fromJson(String json, Class cls) { 27 | try { 28 | return gson.fromJson(json, cls); 29 | } catch (Throwable e) { 30 | return null; 31 | } 32 | } 33 | 34 | public static T fromJson(String json, Type type) { 35 | try { 36 | return gson.fromJson(json, type); 37 | } catch (Throwable e) { 38 | return null; 39 | } 40 | } 41 | 42 | @WorkerThread 43 | public static T fromJson(URL url, Class cls) { 44 | try { 45 | return gson.fromJson(new BufferedReader(new InputStreamReader(url.openConnection().getInputStream())), cls); 46 | } catch (Throwable e) { 47 | return null; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/Keyboard.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.view.View; 6 | import android.view.inputmethod.InputMethodManager; 7 | 8 | /** 9 | * Created by CaoDongping on 12/21/15. 10 | */ 11 | public class Keyboard { 12 | public static void show(Context context) { 13 | InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 14 | imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); 15 | } 16 | 17 | public static void hide(Activity activity) { 18 | View view = activity.getCurrentFocus(); 19 | if (view != null) { 20 | InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); 21 | imm.hideSoftInputFromWindow(view.getWindowToken(), 0); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/MD5.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | 6 | /** 7 | * Created by CaoDongping on 1/4/16. 8 | */ 9 | public class MD5 { 10 | 11 | public static String encode(final String s) { 12 | final String MD5 = "MD5"; 13 | try { 14 | // Create MD5 Hash 15 | MessageDigest digest = MessageDigest.getInstance(MD5); 16 | digest.update(s.getBytes()); 17 | byte messageDigest[] = digest.digest(); 18 | // Create Hex String 19 | StringBuilder hexString = new StringBuilder(); 20 | for (byte aMessageDigest : messageDigest) { 21 | String h = Integer.toHexString(0xFF & aMessageDigest); 22 | while (h.length() < 2) { 23 | h = "0" + h; 24 | } 25 | hexString.append(h); 26 | } 27 | return hexString.toString(); 28 | 29 | } catch (NoSuchAlgorithmException e) { 30 | e.printStackTrace(); 31 | } 32 | return ""; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/StemUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | import org.tartarus.snowball.ext.EnglishStem; 4 | 5 | /** 6 | * Created by CaoDongping on 4/2/16. 7 | */ 8 | public class StemUtil { 9 | public static String stem(String input) { 10 | String[] words = input.replaceAll("[^a-zA-Z ]", " ").toLowerCase().split("\\s+"); 11 | StringBuilder sb = new StringBuilder(); 12 | EnglishStem stem = new EnglishStem(); 13 | for (String w : words) { 14 | stem.setCurrent(w); 15 | stem.stem(); 16 | sb.append(stem.getCurrent()).append(" "); 17 | } 18 | return sb.toString(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/ThreadUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | /** 4 | * Created by CaoDongping on 1/12/16. 5 | */ 6 | public class ThreadUtil { 7 | public static void sleepUntilNextDraw(long startTime) { 8 | int cost = (int) (System.currentTimeMillis() - startTime); 9 | if (cost < 16) { 10 | try { 11 | Thread.sleep(16 - cost); 12 | } catch (InterruptedException e) { 13 | e.printStackTrace(); 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/mzule/androidweekly/util/Tinter.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly.util; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.os.Build; 6 | import android.view.Window; 7 | import android.view.WindowManager; 8 | 9 | import java.lang.reflect.Field; 10 | import java.lang.reflect.Method; 11 | 12 | /** 13 | *

14 | * 设置Activity的信号栏为透明风格的工具类 15 | *

16 | * Created by CaoDongping on 9/17/15. 17 | */ 18 | public class Tinter { 19 | 20 | /** 21 | * 如果当前系统版本支持Tint,则开启,否则,不作任何事情. 22 | * 23 | * @param activity 24 | */ 25 | public static void enableIfSupport(Activity activity) { 26 | if (isSupport()) { 27 | tint(activity, true); 28 | } 29 | } 30 | 31 | /** 32 | * 当前系统版本是否支持Tint 33 | * 34 | * @return 35 | */ 36 | public static boolean isSupport() { 37 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 38 | } 39 | 40 | @TargetApi(19) 41 | private static void tint(Activity activity, boolean on) { 42 | Window win = activity.getWindow(); 43 | WindowManager.LayoutParams winParams = win.getAttributes(); 44 | final int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; 45 | if (on) { 46 | winParams.flags |= bits; 47 | } else { 48 | winParams.flags &= ~bits; 49 | } 50 | win.setAttributes(winParams); 51 | 52 | Class clazz = win.getClass(); 53 | try { 54 | Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams"); 55 | Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE"); 56 | int darkModeFlag = field.getInt(layoutParams); 57 | Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class); 58 | extraFlagField.invoke(win, darkModeFlag, darkModeFlag); 59 | } catch (Throwable e) { 60 | } 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_in_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/anim/slide_out_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/color/text_color_slide_date.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/color/text_color_slide_issue.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/app/src/main/res/drawable-hdpi/ic_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_slide_menu_n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/app/src/main/res/drawable-xxhdpi/ic_slide_menu_n.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_slide_menu_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/app/src/main/res/drawable-xxhdpi/ic_slide_menu_s.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button_n.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_button_s.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_slide_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_article.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 17 | 21 | 22 | 26 | 27 | 30 | 31 | 36 | 37 | 38 | 39 | 44 | 45 | 48 | 49 | 50 | 57 | 58 | 59 | 60 | 64 | 65 | 66 | 67 | 71 | 72 | 73 | 74 | 78 | 79 | 80 | 81 | 85 | 86 | 87 | 88 | 92 | 93 | 94 | 95 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_favorite.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 18 | 19 | 25 | 26 | 27 | 34 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 17 | 18 | 25 | 26 | 33 | 34 | 42 | 43 | 44 | 49 | 50 | 56 | 57 | 64 | 65 | 70 | 71 | 72 | 79 | 80 | 83 | 84 | 88 | 89 | 97 | 98 | 102 | 103 | 107 | 108 | 112 | 113 | 117 | 118 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 16 | 17 | 21 | 22 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search_result.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_article.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 11 | 12 | 19 | 20 | 27 | 28 | 39 | 40 | 49 | 50 | 51 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_search_history.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_section.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_slide_issue.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_navi_bar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 16 | 17 | 23 | 24 | 32 | 33 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_translate.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 21 | 22 | 26 | 27 | 41 | 42 | 45 | 46 | 59 | 60 | 63 | 64 | 69 | 70 | 80 | 81 | 82 | 83 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #36bce8 4 | #303F9F 5 | #464646 6 | #e1e1e1 7 | #d9d9d9 8 | #00000000 9 | #fcfcfc 10 | #ffffff 11 | #ddffffff 12 | #666666 13 | #999999 14 | #CCCCCC 15 | #22000000 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 10sp 3 | 12sp 4 | 14sp 5 | 16sp 6 | 12dp 7 | 6dp 8 | 4dp 9 | 40dp 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Weekly 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 21 | 22 | 30 | 31 | 39 | 40 | 48 | 49 | 54 | 55 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/mzule/androidweekly/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.mzule.androidweekly; 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 | } -------------------------------------------------------------------------------- /art/article.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/art/article.jpg -------------------------------------------------------------------------------- /art/favorite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/art/favorite.jpg -------------------------------------------------------------------------------- /art/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/art/home.jpg -------------------------------------------------------------------------------- /art/issue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/art/issue.jpg -------------------------------------------------------------------------------- /art/search.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/art/search.jpg -------------------------------------------------------------------------------- /art/translation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/art/translation.gif -------------------------------------------------------------------------------- /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.0' 9 | classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | jcenter() 16 | } 17 | } 18 | 19 | task clean(type: Delete) { 20 | delete rootProject.buildDir 21 | } 22 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## Project-wide Gradle settings. 2 | # 3 | # For more details on how to configure your build environment visit 4 | # http://www.gradle.org/docs/current/userguide/build_environment.html 5 | # 6 | # Specifies the JVM arguments used for the daemon process. 7 | # The setting is particularly useful for tweaking memory settings. 8 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 10 | # 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | #Thu May 12 10:00:05 CST 2016 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mzule/AndroidWeekly/f4446585472d21224ea439e39a7d1362dcabd49b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Apr 09 16:59:39 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.10-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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':snowball' 2 | -------------------------------------------------------------------------------- /snowball/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /snowball/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | sourceCompatibility = 1.7 4 | targetCompatibility = 1.7 5 | 6 | dependencies { 7 | compile fileTree(dir: 'libs', include: ['*.jar']) 8 | } -------------------------------------------------------------------------------- /snowball/src/main/java/org/tartarus/snowball/Among.java: -------------------------------------------------------------------------------- 1 | package org.tartarus.snowball; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | public class Among { 6 | public final int s_size; /* search string */ 7 | public final char[] s; /* search string */ 8 | public final int substring_i; /* index to longest matching substring */ 9 | public final int result; /* result of the lookup */ 10 | public final Method method; /* method to use if substring matches */ 11 | public final SnowballProgram methodobject; /* object to invoke method on */ 12 | 13 | public Among(String s, int substring_i, int result, 14 | String methodname, SnowballProgram methodobject) { 15 | this.s_size = s.length(); 16 | this.s = s.toCharArray(); 17 | this.substring_i = substring_i; 18 | this.result = result; 19 | this.methodobject = methodobject; 20 | if (methodname.length() == 0) { 21 | this.method = null; 22 | } else { 23 | try { 24 | this.method = methodobject.getClass(). 25 | getDeclaredMethod(methodname, new Class[0]); 26 | } catch (NoSuchMethodException e) { 27 | throw new RuntimeException(e); 28 | } 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /snowball/src/main/java/org/tartarus/snowball/SnowballProgram.java: -------------------------------------------------------------------------------- 1 | 2 | package org.tartarus.snowball; 3 | 4 | import java.lang.reflect.InvocationTargetException; 5 | 6 | public class SnowballProgram { 7 | // current string 8 | protected StringBuffer current; 9 | protected int cursor; 10 | protected int limit; 11 | protected int limit_backward; 12 | protected int bra; 13 | protected int ket; 14 | 15 | protected SnowballProgram() { 16 | current = new StringBuffer(); 17 | setCurrent(""); 18 | } 19 | 20 | /** 21 | * Get the current string. 22 | */ 23 | public String getCurrent() { 24 | String result = current.toString(); 25 | // Make a new StringBuffer. If we reuse the old one, and a user of 26 | // the library keeps a reference to the buffer returned (for example, 27 | // by converting it to a String in a way which doesn't force a copy), 28 | // the buffer size will not decrease, and we will risk wasting a large 29 | // amount of memory. 30 | // Thanks to Wolfram Esser for spotting this problem. 31 | current = new StringBuffer(); 32 | return result; 33 | } 34 | 35 | /** 36 | * Set the current string. 37 | */ 38 | public void setCurrent(String value) { 39 | current.replace(0, current.length(), value); 40 | cursor = 0; 41 | limit = current.length(); 42 | limit_backward = 0; 43 | bra = cursor; 44 | ket = limit; 45 | } 46 | 47 | protected void copy_from(SnowballProgram other) { 48 | current = other.current; 49 | cursor = other.cursor; 50 | limit = other.limit; 51 | limit_backward = other.limit_backward; 52 | bra = other.bra; 53 | ket = other.ket; 54 | } 55 | 56 | protected boolean in_grouping(char[] s, int min, int max) { 57 | if (cursor >= limit) return false; 58 | char ch = current.charAt(cursor); 59 | if (ch > max || ch < min) return false; 60 | ch -= min; 61 | if ((s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) return false; 62 | cursor++; 63 | return true; 64 | } 65 | 66 | protected boolean in_grouping_b(char[] s, int min, int max) { 67 | if (cursor <= limit_backward) return false; 68 | char ch = current.charAt(cursor - 1); 69 | if (ch > max || ch < min) return false; 70 | ch -= min; 71 | if ((s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) return false; 72 | cursor--; 73 | return true; 74 | } 75 | 76 | protected boolean out_grouping(char[] s, int min, int max) { 77 | if (cursor >= limit) return false; 78 | char ch = current.charAt(cursor); 79 | if (ch > max || ch < min) { 80 | cursor++; 81 | return true; 82 | } 83 | ch -= min; 84 | if ((s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) { 85 | cursor++; 86 | return true; 87 | } 88 | return false; 89 | } 90 | 91 | protected boolean out_grouping_b(char[] s, int min, int max) { 92 | if (cursor <= limit_backward) return false; 93 | char ch = current.charAt(cursor - 1); 94 | if (ch > max || ch < min) { 95 | cursor--; 96 | return true; 97 | } 98 | ch -= min; 99 | if ((s[ch >> 3] & (0X1 << (ch & 0X7))) == 0) { 100 | cursor--; 101 | return true; 102 | } 103 | return false; 104 | } 105 | 106 | protected boolean in_range(int min, int max) { 107 | if (cursor >= limit) return false; 108 | char ch = current.charAt(cursor); 109 | if (ch > max || ch < min) return false; 110 | cursor++; 111 | return true; 112 | } 113 | 114 | protected boolean in_range_b(int min, int max) { 115 | if (cursor <= limit_backward) return false; 116 | char ch = current.charAt(cursor - 1); 117 | if (ch > max || ch < min) return false; 118 | cursor--; 119 | return true; 120 | } 121 | 122 | protected boolean out_range(int min, int max) { 123 | if (cursor >= limit) return false; 124 | char ch = current.charAt(cursor); 125 | if (!(ch > max || ch < min)) return false; 126 | cursor++; 127 | return true; 128 | } 129 | 130 | protected boolean out_range_b(int min, int max) { 131 | if (cursor <= limit_backward) return false; 132 | char ch = current.charAt(cursor - 1); 133 | if (!(ch > max || ch < min)) return false; 134 | cursor--; 135 | return true; 136 | } 137 | 138 | protected boolean eq_s(int s_size, String s) { 139 | if (limit - cursor < s_size) return false; 140 | int i; 141 | for (i = 0; i != s_size; i++) { 142 | if (current.charAt(cursor + i) != s.charAt(i)) return false; 143 | } 144 | cursor += s_size; 145 | return true; 146 | } 147 | 148 | protected boolean eq_s_b(int s_size, String s) { 149 | if (cursor - limit_backward < s_size) return false; 150 | int i; 151 | for (i = 0; i != s_size; i++) { 152 | if (current.charAt(cursor - s_size + i) != s.charAt(i)) return false; 153 | } 154 | cursor -= s_size; 155 | return true; 156 | } 157 | 158 | protected boolean eq_v(CharSequence s) { 159 | return eq_s(s.length(), s.toString()); 160 | } 161 | 162 | protected boolean eq_v_b(CharSequence s) { 163 | return eq_s_b(s.length(), s.toString()); 164 | } 165 | 166 | protected int find_among(Among v[], int v_size) { 167 | int i = 0; 168 | int j = v_size; 169 | 170 | int c = cursor; 171 | int l = limit; 172 | 173 | int common_i = 0; 174 | int common_j = 0; 175 | 176 | boolean first_key_inspected = false; 177 | 178 | while (true) { 179 | int k = i + ((j - i) >> 1); 180 | int diff = 0; 181 | int common = common_i < common_j ? common_i : common_j; // smaller 182 | Among w = v[k]; 183 | int i2; 184 | for (i2 = common; i2 < w.s_size; i2++) { 185 | if (c + common == l) { 186 | diff = -1; 187 | break; 188 | } 189 | diff = current.charAt(c + common) - w.s[i2]; 190 | if (diff != 0) break; 191 | common++; 192 | } 193 | if (diff < 0) { 194 | j = k; 195 | common_j = common; 196 | } else { 197 | i = k; 198 | common_i = common; 199 | } 200 | if (j - i <= 1) { 201 | if (i > 0) break; // v->s has been inspected 202 | if (j == i) break; // only one item in v 203 | 204 | // - but now we need to go round once more to get 205 | // v->s inspected. This looks messy, but is actually 206 | // the optimal approach. 207 | 208 | if (first_key_inspected) break; 209 | first_key_inspected = true; 210 | } 211 | } 212 | while (true) { 213 | Among w = v[i]; 214 | if (common_i >= w.s_size) { 215 | cursor = c + w.s_size; 216 | if (w.method == null) return w.result; 217 | boolean res; 218 | try { 219 | Object resobj = w.method.invoke(w.methodobject, 220 | new Object[0]); 221 | res = resobj.toString().equals("true"); 222 | } catch (InvocationTargetException e) { 223 | res = false; 224 | // FIXME - debug message 225 | } catch (IllegalAccessException e) { 226 | res = false; 227 | // FIXME - debug message 228 | } 229 | cursor = c + w.s_size; 230 | if (res) return w.result; 231 | } 232 | i = w.substring_i; 233 | if (i < 0) return 0; 234 | } 235 | } 236 | 237 | // find_among_b is for backwards processing. Same comments apply 238 | protected int find_among_b(Among v[], int v_size) { 239 | int i = 0; 240 | int j = v_size; 241 | 242 | int c = cursor; 243 | int lb = limit_backward; 244 | 245 | int common_i = 0; 246 | int common_j = 0; 247 | 248 | boolean first_key_inspected = false; 249 | 250 | while (true) { 251 | int k = i + ((j - i) >> 1); 252 | int diff = 0; 253 | int common = common_i < common_j ? common_i : common_j; 254 | Among w = v[k]; 255 | int i2; 256 | for (i2 = w.s_size - 1 - common; i2 >= 0; i2--) { 257 | if (c - common == lb) { 258 | diff = -1; 259 | break; 260 | } 261 | diff = current.charAt(c - 1 - common) - w.s[i2]; 262 | if (diff != 0) break; 263 | common++; 264 | } 265 | if (diff < 0) { 266 | j = k; 267 | common_j = common; 268 | } else { 269 | i = k; 270 | common_i = common; 271 | } 272 | if (j - i <= 1) { 273 | if (i > 0) break; 274 | if (j == i) break; 275 | if (first_key_inspected) break; 276 | first_key_inspected = true; 277 | } 278 | } 279 | while (true) { 280 | Among w = v[i]; 281 | if (common_i >= w.s_size) { 282 | cursor = c - w.s_size; 283 | if (w.method == null) return w.result; 284 | 285 | boolean res; 286 | try { 287 | Object resobj = w.method.invoke(w.methodobject, 288 | new Object[0]); 289 | res = resobj.toString().equals("true"); 290 | } catch (InvocationTargetException e) { 291 | res = false; 292 | // FIXME - debug message 293 | } catch (IllegalAccessException e) { 294 | res = false; 295 | // FIXME - debug message 296 | } 297 | cursor = c - w.s_size; 298 | if (res) return w.result; 299 | } 300 | i = w.substring_i; 301 | if (i < 0) return 0; 302 | } 303 | } 304 | 305 | /* to replace chars between c_bra and c_ket in current by the 306 | * chars in s. 307 | */ 308 | protected int replace_s(int c_bra, int c_ket, String s) { 309 | int adjustment = s.length() - (c_ket - c_bra); 310 | current.replace(c_bra, c_ket, s); 311 | limit += adjustment; 312 | if (cursor >= c_ket) cursor += adjustment; 313 | else if (cursor > c_bra) cursor = c_bra; 314 | return adjustment; 315 | } 316 | 317 | protected void slice_check() { 318 | if (bra < 0 || 319 | bra > ket || 320 | ket > limit || 321 | limit > current.length()) // this line could be removed 322 | { 323 | System.err.println("faulty slice operation"); 324 | // FIXME: report error somehow. 325 | /* 326 | fprintf(stderr, "faulty slice operation:\n"); 327 | debug(z, -1, 0); 328 | exit(1); 329 | */ 330 | } 331 | } 332 | 333 | protected void slice_from(String s) { 334 | slice_check(); 335 | replace_s(bra, ket, s); 336 | } 337 | 338 | protected void slice_from(CharSequence s) { 339 | slice_from(s.toString()); 340 | } 341 | 342 | protected void slice_del() { 343 | slice_from(""); 344 | } 345 | 346 | protected void insert(int c_bra, int c_ket, String s) { 347 | int adjustment = replace_s(c_bra, c_ket, s); 348 | if (c_bra <= bra) bra += adjustment; 349 | if (c_bra <= ket) ket += adjustment; 350 | } 351 | 352 | protected void insert(int c_bra, int c_ket, CharSequence s) { 353 | insert(c_bra, c_ket, s.toString()); 354 | } 355 | 356 | /* Copy the slice into the supplied StringBuffer */ 357 | protected StringBuffer slice_to(StringBuffer s) { 358 | slice_check(); 359 | int len = ket - bra; 360 | s.replace(0, s.length(), current.substring(bra, ket)); 361 | return s; 362 | } 363 | 364 | /* Copy the slice into the supplied StringBuilder */ 365 | protected StringBuilder slice_to(StringBuilder s) { 366 | slice_check(); 367 | int len = ket - bra; 368 | s.replace(0, s.length(), current.substring(bra, ket)); 369 | return s; 370 | } 371 | 372 | protected StringBuffer assign_to(StringBuffer s) { 373 | s.replace(0, s.length(), current.substring(0, limit)); 374 | return s; 375 | } 376 | 377 | protected StringBuilder assign_to(StringBuilder s) { 378 | s.replace(0, s.length(), current.substring(0, limit)); 379 | return s; 380 | } 381 | 382 | /* 383 | extern void debug(struct SN_env * z, int number, int line_count) 384 | { int i; 385 | int limit = SIZE(z->p); 386 | //if (number >= 0) printf("%3d (line %4d): '", number, line_count); 387 | if (number >= 0) printf("%3d (line %4d): [%d]'", number, line_count,limit); 388 | for (i = 0; i <= limit; i++) 389 | { if (z->lb == i) printf("{"); 390 | if (z->bra == i) printf("["); 391 | if (z->c == i) printf("|"); 392 | if (z->ket == i) printf("]"); 393 | if (z->l == i) printf("}"); 394 | if (i < limit) 395 | { int ch = z->p[i]; 396 | if (ch == 0) ch = '#'; 397 | printf("%c", ch); 398 | } 399 | } 400 | printf("'\n"); 401 | } 402 | */ 403 | 404 | }; 405 | -------------------------------------------------------------------------------- /snowball/src/main/java/org/tartarus/snowball/SnowballStemmer.java: -------------------------------------------------------------------------------- 1 | 2 | package org.tartarus.snowball; 3 | 4 | public abstract class SnowballStemmer extends SnowballProgram { 5 | public abstract boolean stem(); 6 | }; 7 | -------------------------------------------------------------------------------- /snowball/src/main/java/org/tartarus/snowball/TestApp.java: -------------------------------------------------------------------------------- 1 | 2 | package org.tartarus.snowball; 3 | 4 | import java.io.BufferedReader; 5 | import java.io.BufferedWriter; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.InputStreamReader; 9 | import java.io.OutputStream; 10 | import java.io.OutputStreamWriter; 11 | import java.io.Reader; 12 | import java.io.Writer; 13 | 14 | public class TestApp { 15 | private static void usage() { 16 | System.err.println("Usage: TestApp [-o ]"); 17 | } 18 | 19 | public static void main(String[] args) throws Throwable { 20 | if (args.length < 2) { 21 | usage(); 22 | return; 23 | } 24 | 25 | Class stemClass = Class.forName("org.tartarus.snowball.ext." + 26 | args[0] + "Stemmer"); 27 | SnowballStemmer stemmer = (SnowballStemmer) stemClass.newInstance(); 28 | 29 | Reader reader; 30 | reader = new InputStreamReader(new FileInputStream(args[1])); 31 | reader = new BufferedReader(reader); 32 | 33 | StringBuffer input = new StringBuffer(); 34 | 35 | OutputStream outstream; 36 | 37 | if (args.length > 2) { 38 | if (args.length >= 4 && args[2].equals("-o")) { 39 | outstream = new FileOutputStream(args[3]); 40 | } else { 41 | usage(); 42 | return; 43 | } 44 | } else { 45 | outstream = System.out; 46 | } 47 | Writer output = new OutputStreamWriter(outstream); 48 | output = new BufferedWriter(output); 49 | 50 | int repeat = 1; 51 | if (args.length > 4) { 52 | repeat = Integer.parseInt(args[4]); 53 | } 54 | 55 | Object[] emptyArgs = new Object[0]; 56 | int character; 57 | while ((character = reader.read()) != -1) { 58 | char ch = (char) character; 59 | if (Character.isWhitespace((char) ch)) { 60 | if (input.length() > 0) { 61 | stemmer.setCurrent(input.toString()); 62 | for (int i = repeat; i != 0; i--) { 63 | stemmer.stem(); 64 | } 65 | output.write(stemmer.getCurrent()); 66 | output.write('\n'); 67 | input.delete(0, input.length()); 68 | } 69 | } else { 70 | input.append(Character.toLowerCase(ch)); 71 | } 72 | } 73 | output.flush(); 74 | } 75 | } 76 | --------------------------------------------------------------------------------