├── .gitattributes ├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── V2EX.iml ├── app ├── .gitignore ├── app-debug.apk ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── zzhoujay │ │ └── v2ex │ │ ├── MainActivity.java │ │ ├── V2EX.java │ │ ├── data │ │ ├── DataManger.java │ │ ├── DataProvider.java │ │ ├── MemberProvider.java │ │ ├── NodesProvider.java │ │ ├── RepliesProvider.java │ │ ├── TopicProvider.java │ │ └── TopicsProvider.java │ │ ├── interfaces │ │ ├── ClickCallback.java │ │ ├── MemberService.java │ │ ├── NodeService.java │ │ ├── NodesService.java │ │ ├── Notifier.java │ │ ├── OnItemClickListener.java │ │ ├── OnLoadCompleteListener.java │ │ ├── RepliesService.java │ │ ├── TopicService.java │ │ └── TopicsService.java │ │ ├── model │ │ ├── Member.java │ │ ├── Node.java │ │ ├── Replies.java │ │ └── Topic.java │ │ ├── net │ │ ├── NetworkManager.java │ │ ├── PersistentCookieStore.java │ │ └── SerializableHttpCookie.java │ │ ├── ui │ │ ├── activity │ │ │ ├── LoginActivity.java │ │ │ ├── MemberActivity.java │ │ │ ├── NewTopicActivity.java │ │ │ ├── NodeActivity.java │ │ │ ├── NodesActivity.java │ │ │ └── TopicDetailActivity.java │ │ ├── adapter │ │ │ ├── NodesAdapter.java │ │ │ ├── RepliesAdapter.java │ │ │ └── TopicsAdapter.java │ │ ├── dialog │ │ │ └── ContentDialog.java │ │ ├── fragment │ │ │ ├── NodesFragment.java │ │ │ ├── ReplyFragment.java │ │ │ ├── TopicDetailFragment.java │ │ │ └── TopicsFragment.java │ │ └── view │ │ │ └── SwipeToRefreshLayout.java │ │ └── util │ │ ├── ContentUtils.java │ │ ├── FileComparator.java │ │ ├── FileNameFilter.java │ │ ├── FileUtils.java │ │ ├── TimeUtils.java │ │ └── UserUtils.java │ └── res │ ├── drawable-hdpi │ ├── ic_add_white.png │ ├── ic_dashboard_grey.png │ ├── ic_done_white.png │ ├── ic_drawer.png │ ├── ic_exit_to_app_white.png │ ├── ic_info_grey.png │ ├── ic_refresh_white.png │ ├── ic_send_grey.png │ ├── ic_settings_grey.png │ └── ic_view_agenda_grey.png │ ├── drawable-mdpi │ └── ic_drawer.png │ ├── drawable-xhdpi │ ├── ic_add_white.png │ ├── ic_done_white.png │ ├── ic_drawer.png │ ├── ic_exit_to_app_white.png │ ├── ic_refresh_white.png │ └── ic_send_grey.png │ ├── drawable-xxhdpi │ └── ic_drawer.png │ ├── drawable │ ├── default_image.xml │ └── image_btn_bg.xml │ ├── layout │ ├── activity_fragment.xml │ ├── activity_fragment_floatactionbar.xml │ ├── activity_login.xml │ ├── activity_main.xml │ ├── activity_member.xml │ ├── activity_new_topic.xml │ ├── drawer_header.xml │ ├── fragment_content.xml │ ├── fragment_recycler_view.xml │ ├── fragment_reply.xml │ ├── fragment_topic_detail.xml │ ├── item_node.xml │ ├── item_replies.xml │ └── item_topic.xml │ ├── menu │ └── drawer.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot ├── Screenshot_2015-07-28-19-06-28.png ├── Screenshot_2015-07-28-19-06-34.png ├── Screenshot_2015-07-28-19-06-40.png ├── Screenshot_2015-07-28-19-07-02.png ├── Screenshot_2015-07-28-19-07-32.png ├── Screenshot_2015-07-28-19-07-43.png ├── Screenshot_2015-07-28-19-07-55.png ├── Screenshot_2015-07-28-19-29-23.png ├── Screenshot_2015-07-28-19-30-44.png ├── Screenshot_2015-07-28-19-37-09.png └── Screenshot_2015-07-28-21-20-39.png └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | V2EX -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Class structure 15 | 16 | 17 | Code maturity issues 18 | 19 | 20 | General 21 | 22 | 23 | Java language level migration aids 24 | 25 | 26 | Javadoc issues 27 | 28 | 29 | Performance issues 30 | 31 | 32 | TestNG 33 | 34 | 35 | Threading issues 36 | 37 | 38 | 39 | 40 | Abstraction issues 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # V2EX 2 | 3 | V2EX的非官方Android客户端,极力遵循Material Design风格 4 | 5 | ### 实现功能 6 | * 查看最新和最热话题 7 | * 查看所有节点 8 | * 查看节点里的话题 9 | * 查看话题详情、评论 10 | * 评论话题 11 | * 发表新话题 12 | * 登录 13 | * 查看用户信息 14 | 15 | > 依赖Glide、okhttp、retrofit 16 | > 有些地方参考了[v2ex-android](https://github.com/greatyao/v2ex-android)的实现 17 | 18 | ### 运行效果 19 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203434_f003c526_141009.png "演示") 20 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203458_6290d1be_141009.png "演示") 21 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203515_d8d02651_141009.png "演示") 22 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203534_d667deed_141009.png "演示") 23 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203600_b6748df2_141009.png "演示") 24 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203618_2b6af7a6_141009.png "演示") 25 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203633_9792c3f5_141009.png "演示") 26 | ![演示](http://git.oschina.net/uploads/images/2015/0728/212513_627934bf_141009.png "演示") 27 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203718_74e3ea8a_141009.png "演示") 28 | ![演示](http://git.oschina.net/uploads/images/2015/0728/203734_2f3d668a_141009.png "演示") 29 | 30 | _by zzhoujay_ 31 | -------------------------------------------------------------------------------- /V2EX.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/app-debug.apk -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | //apply plugin: 'oneapm' 3 | 4 | android { 5 | compileSdkVersion 23 6 | buildToolsVersion "22.0.1" 7 | 8 | defaultConfig { 9 | applicationId "com.zzhoujay.v2ex" 10 | minSdkVersion 15 11 | targetSdkVersion 23 12 | versionCode 1 13 | versionName "1.0" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | repositories { 24 | maven { 25 | url "http://dl.bintray.com/zzhoujay/maven" 26 | } 27 | } 28 | 29 | 30 | dependencies { 31 | compile fileTree(include: ['*.jar'], dir: 'libs') 32 | // compile fileTree(dir:'E:\\Other SDK\\OneAPM_Android_Gradle_2.0.0\\agent',include:['*.jar']) 33 | compile 'com.android.support:appcompat-v7:23.4.0' 34 | compile 'com.android.support:design:23.4.0' 35 | compile 'com.android.support:cardview-v7:23.4.0' 36 | compile 'com.android.support:recyclerview-v7:23.4.0' 37 | compile 'com.squareup.retrofit:retrofit:1.9.0' 38 | compile 'com.squareup.okhttp:okhttp:2.4.0' 39 | compile 'com.zzhoujay.richtext:richtext:1.1.2' 40 | compile 'com.zzhoujay.advanceadapter:advanceadapter:1.0.2' 41 | // compile 'zhou.widget:advanceadapter:1.0' 42 | compile 'com.github.bumptech.glide:glide:3.7.0' 43 | } 44 | -------------------------------------------------------------------------------- /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 E:\android_studio\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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 45 | 49 | 53 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.os.Parcelable; 6 | import android.support.design.widget.NavigationView; 7 | import android.support.design.widget.TabLayout; 8 | import android.support.v4.app.Fragment; 9 | import android.support.v4.app.FragmentPagerAdapter; 10 | import android.support.v4.view.GravityCompat; 11 | import android.support.v4.view.PagerAdapter; 12 | import android.support.v4.view.ViewPager; 13 | import android.support.v4.widget.DrawerLayout; 14 | import android.support.v7.app.ActionBar; 15 | import android.support.v7.app.AppCompatActivity; 16 | import android.support.v7.widget.Toolbar; 17 | import android.view.MenuItem; 18 | import android.view.View; 19 | import android.widget.ImageView; 20 | import android.widget.TextView; 21 | 22 | import com.bumptech.glide.Glide; 23 | import com.zzhoujay.v2ex.data.DataManger; 24 | import com.zzhoujay.v2ex.data.TopicsProvider; 25 | import com.zzhoujay.v2ex.interfaces.Notifier; 26 | import com.zzhoujay.v2ex.model.Member; 27 | import com.zzhoujay.v2ex.ui.activity.LoginActivity; 28 | import com.zzhoujay.v2ex.ui.activity.MemberActivity; 29 | import com.zzhoujay.v2ex.ui.activity.NodesActivity; 30 | import com.zzhoujay.v2ex.ui.dialog.ContentDialog; 31 | import com.zzhoujay.v2ex.ui.fragment.TopicsFragment; 32 | 33 | 34 | public class MainActivity extends AppCompatActivity { 35 | 36 | private TabLayout tabLayout; 37 | private ViewPager viewPager; 38 | private NavigationView navigationView; 39 | private TopicsFragment[] fragments; 40 | private ImageView icon; 41 | private TextView name, bio; 42 | private View header; 43 | private DrawerLayout drawerLayout; 44 | private ContentDialog about; 45 | 46 | private int[] ids = {R.string.tab1, R.string.tab2}; 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | setContentView(R.layout.activity_main); 52 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 53 | setSupportActionBar(toolbar); 54 | ActionBar actionBar = getSupportActionBar(); 55 | if (actionBar != null) { 56 | actionBar.setDisplayUseLogoEnabled(true); 57 | actionBar.setDisplayShowTitleEnabled(true); 58 | actionBar.setDisplayShowHomeEnabled(true); 59 | toolbar.setNavigationIcon(R.drawable.ic_drawer); 60 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 61 | @Override 62 | public void onClick(View v) { 63 | drawerLayout.openDrawer(GravityCompat.START); 64 | } 65 | }); 66 | } 67 | 68 | fragments = new TopicsFragment[2]; 69 | fragments[0] = TopicsFragment.newInstance(TopicsProvider.TopicType.LATEST); 70 | fragments[1] = TopicsFragment.newInstance(TopicsProvider.TopicType.HOT); 71 | 72 | initView(); 73 | initData(); 74 | initEvent(); 75 | 76 | } 77 | 78 | private void initEvent() { 79 | navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { 80 | @Override 81 | public boolean onNavigationItemSelected(MenuItem menuItem) { 82 | drawerLayout.closeDrawers(); 83 | switch (menuItem.getItemId()) { 84 | case R.id.drawer_tab2: 85 | Intent intent = new Intent(MainActivity.this, NodesActivity.class); 86 | startActivity(intent); 87 | return true; 88 | case R.id.drawer_about: 89 | about = ContentDialog.newInstance(getString(R.string.about), getString(R.string.about_content)); 90 | about.show(getSupportFragmentManager(), "about"); 91 | return true; 92 | } 93 | return false; 94 | } 95 | }); 96 | } 97 | 98 | private void initData() { 99 | tabLayout.addTab(tabLayout.newTab().setText(ids[0])); 100 | tabLayout.addTab(tabLayout.newTab().setText(ids[1])); 101 | 102 | PagerAdapter adapter = new FragmentPagerAdapter(getSupportFragmentManager()) { 103 | @Override 104 | public Fragment getItem(int position) { 105 | return fragments[position]; 106 | } 107 | 108 | @Override 109 | public int getCount() { 110 | return fragments.length; 111 | } 112 | 113 | @Override 114 | public CharSequence getPageTitle(int position) { 115 | return getString(ids[position]); 116 | } 117 | }; 118 | 119 | viewPager.setAdapter(adapter); 120 | tabLayout.setupWithViewPager(viewPager); 121 | tabLayout.setTabsFromPagerAdapter(adapter); 122 | 123 | } 124 | 125 | private void initView() { 126 | drawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout); 127 | navigationView = (NavigationView) findViewById(R.id.navigationView); 128 | tabLayout = (TabLayout) findViewById(R.id.tabLayout); 129 | viewPager = (ViewPager) findViewById(R.id.viewPage); 130 | 131 | header=navigationView.inflateHeaderView(R.layout.drawer_header); 132 | 133 | icon = (ImageView) header.findViewById(R.id.header_icon); 134 | name = (TextView) header.findViewById(R.id.header_name); 135 | bio = (TextView) header.findViewById(R.id.header_bio); 136 | 137 | } 138 | 139 | private void setUserInfo(Member member) { 140 | if (member == null) { 141 | name.setText(R.string.login); 142 | icon.setImageResource(R.mipmap.ic_launcher); 143 | bio.setText(""); 144 | return; 145 | } 146 | name.setText(member.username); 147 | bio.setText(member.bio); 148 | Glide.with(this) 149 | .load("http:" + member.avatar_large) 150 | .placeholder(R.mipmap.ic_launcher) 151 | .error(R.mipmap.ic_launcher) 152 | .centerCrop() 153 | .crossFade() 154 | .into(icon); 155 | } 156 | 157 | @Override 158 | protected void onResume() { 159 | super.onResume(); 160 | Member user = V2EX.getInstance().getSelf(); 161 | if (user != null) { 162 | setUserInfo(user); 163 | name.setOnClickListener(userInfoListener); 164 | header.setOnClickListener(userInfoListener); 165 | icon.setOnClickListener(userInfoListener); 166 | } else { 167 | setUserInfo(null); 168 | icon.setOnClickListener(loginListener); 169 | name.setOnClickListener(loginListener); 170 | } 171 | } 172 | 173 | private View.OnClickListener loginListener = new View.OnClickListener() { 174 | @Override 175 | public void onClick(View v) { 176 | Intent intent = new Intent(MainActivity.this, LoginActivity.class); 177 | startActivity(intent); 178 | } 179 | }; 180 | 181 | private View.OnClickListener userInfoListener = new View.OnClickListener() { 182 | @Override 183 | public void onClick(View v) { 184 | Intent intent = new Intent(MainActivity.this, MemberActivity.class); 185 | intent.putExtra(Member.MEMBER, (Parcelable) V2EX.getInstance().getSelf()); 186 | startActivity(intent); 187 | } 188 | }; 189 | 190 | private Notifier notifier = new Notifier() { 191 | @Override 192 | public void notice() { 193 | Member user = V2EX.getInstance().getSelf(); 194 | if (user != null) { 195 | setUserInfo(user); 196 | name.setOnClickListener(null); 197 | header.setOnClickListener(userInfoListener); 198 | } else { 199 | name.setOnClickListener(loginListener); 200 | } 201 | } 202 | }; 203 | 204 | @Override 205 | protected void onDestroy() { 206 | super.onDestroy(); 207 | DataManger.getInstance().removeProvider(TopicsProvider.TopicType.FILE_NAME_HOT); 208 | DataManger.getInstance().removeProvider(TopicsProvider.TopicType.FILE_NAME_LATEST); 209 | V2EX.getInstance().removeSelfChangeNotifier(notifier); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/V2EX.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex; 2 | 3 | import android.app.Application; 4 | import android.net.ConnectivityManager; 5 | import android.net.NetworkInfo; 6 | import android.widget.Toast; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import com.zzhoujay.v2ex.data.DataManger; 12 | import com.zzhoujay.v2ex.data.MemberProvider; 13 | import com.zzhoujay.v2ex.interfaces.Notifier; 14 | import com.zzhoujay.v2ex.model.Member; 15 | 16 | /** 17 | * Created by 州 on 2015/7/18 0018. 18 | * Application 19 | */ 20 | public class V2EX extends Application { 21 | 22 | public static final String SINGIN_URL = "http://v2ex.com/signin"; 23 | public static final String SITE_URL = "http://v2ex.com"; 24 | 25 | 26 | private static V2EX v2EX; 27 | 28 | private Member self; 29 | private List selfStateChangeNotifier; 30 | 31 | @Override 32 | public void onCreate() { 33 | super.onCreate(); 34 | v2EX = this; 35 | selfStateChangeNotifier = new ArrayList<>(); 36 | setSelf(MemberProvider.getSelf()); 37 | if (self != null) { 38 | MemberProvider memberProvider = new MemberProvider(DataManger.getInstance().getRestAdapter(), self.username, true); 39 | DataManger.getInstance().addProvider(memberProvider.FILE_NAME, memberProvider); 40 | DataManger.getInstance().refresh(memberProvider.FILE_NAME, null); 41 | } 42 | } 43 | 44 | public static V2EX getInstance() { 45 | return v2EX; 46 | } 47 | 48 | public boolean isNetworkConnected() { 49 | ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); 50 | NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); 51 | return networkInfo != null && networkInfo.isAvailable(); 52 | } 53 | 54 | public void toast(String msg) { 55 | Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); 56 | } 57 | 58 | public void toast(int res) { 59 | Toast.makeText(this, res, Toast.LENGTH_SHORT).show(); 60 | } 61 | 62 | 63 | public boolean saveCache() { 64 | return false; 65 | } 66 | 67 | public boolean isSelf(String username) { 68 | return self != null && self.username.equals(username); 69 | } 70 | 71 | public void setSelf(Member member) { 72 | this.self = member; 73 | for (Notifier notifier : selfStateChangeNotifier) { 74 | notifier.notice(); 75 | } 76 | } 77 | 78 | public Member getSelf() { 79 | return self; 80 | } 81 | 82 | public boolean isLogin() { 83 | return self != null; 84 | } 85 | 86 | public boolean logout() { 87 | setSelf(null); 88 | for (Notifier notifier : selfStateChangeNotifier) { 89 | notifier.notice(); 90 | } 91 | return MemberProvider.clearSelf(); 92 | } 93 | 94 | @SuppressWarnings("unused") 95 | public void addSelfChangeNotifier(Notifier notifier) { 96 | selfStateChangeNotifier.add(notifier); 97 | } 98 | 99 | public void removeSelfChangeNotifier(Notifier notifier) { 100 | selfStateChangeNotifier.remove(notifier); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/data/DataManger.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.data; 2 | 3 | import android.support.annotation.Nullable; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import retrofit.RestAdapter; 9 | import com.zzhoujay.v2ex.R; 10 | import com.zzhoujay.v2ex.V2EX; 11 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 12 | 13 | /** 14 | * Created by 州 on 2015/7/20 0020. 15 | * 数据管理器(内存、本地缓存、联网加载) 16 | */ 17 | public class DataManger { 18 | 19 | private Map providerMap; 20 | private RestAdapter restAdapter; 21 | 22 | private DataManger() { 23 | providerMap = new HashMap<>(); 24 | restAdapter = new RestAdapter.Builder().setEndpoint("http://v2ex.com").build(); 25 | } 26 | 27 | private static DataManger dataManger; 28 | 29 | public static DataManger getInstance() { 30 | if (dataManger == null) { 31 | dataManger = new DataManger(); 32 | } 33 | return dataManger; 34 | } 35 | 36 | /** 37 | * 联网刷新数据 38 | * 39 | * @param key key 40 | * @param onLoadComplete 回调 41 | * @param type 42 | */ 43 | @SuppressWarnings("unchecked") 44 | public void refresh(String key, @Nullable final OnLoadCompleteListener onLoadComplete) { 45 | final DataProvider provider = providerMap.get(key); 46 | if (provider != null) { 47 | //进行联网加载 48 | provider.getFromNet(new OnLoadCompleteListener() { 49 | @Override 50 | public void loadComplete(T o) { 51 | if (o != null) { 52 | //联网加载成功 53 | provider.set(o); 54 | if (provider.needCache()) 55 | provider.persistence(); 56 | } else { 57 | //联网加载失败 58 | V2EX.getInstance().toast(R.string.load_error); 59 | } 60 | if (onLoadComplete != null) { 61 | onLoadComplete.loadComplete(o); 62 | } 63 | } 64 | }); 65 | } else { 66 | if (onLoadComplete != null) { 67 | onLoadComplete.loadComplete(null); 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * 按照 内存->本地缓存->网络数据 的顺序加载数据 74 | * 75 | * @param key key 76 | * @param onLoadComplete 回调 77 | * @param type 78 | */ 79 | @SuppressWarnings("unchecked") 80 | public void getData(String key, final OnLoadCompleteListener onLoadComplete) { 81 | final DataProvider provider = providerMap.get(key); 82 | if (provider == null) { 83 | if (onLoadComplete != null) { 84 | onLoadComplete.loadComplete(null); 85 | } 86 | return; 87 | } 88 | if (provider.hasLoad()) { 89 | //已加载至内存 90 | if (onLoadComplete != null) { 91 | onLoadComplete.loadComplete(provider.get()); 92 | } 93 | } else { 94 | //未加载到内存 95 | provider.getFromLocal(new OnLoadCompleteListener() { 96 | @Override 97 | public void loadComplete(T o) { 98 | if (o != null) { 99 | //从本地加载到了 100 | provider.set(o); 101 | if (onLoadComplete != null) { 102 | onLoadComplete.loadComplete(o); 103 | } 104 | } else { 105 | //本地没有缓存,联网加载 106 | provider.getFromNet(new OnLoadCompleteListener() { 107 | @Override 108 | public void loadComplete(T o) { 109 | if (o != null) { 110 | //联网加载成功 111 | provider.set(o); 112 | if (provider.needCache()) 113 | provider.persistence(); 114 | } else { 115 | //联网加载失败 116 | V2EX.getInstance().toast(R.string.load_error); 117 | } 118 | if (onLoadComplete != null) { 119 | onLoadComplete.loadComplete(o); 120 | } 121 | } 122 | }); 123 | } 124 | } 125 | }); 126 | } 127 | } 128 | 129 | /** 130 | * 添加数据提供器 131 | * 132 | * @param key key 133 | * @param provider 数据提供器 134 | * @param type 135 | */ 136 | public void addProvider(String key, DataProvider provider) { 137 | if (provider != null && key != null) 138 | if (!providerMap.containsKey(key)) { 139 | providerMap.put(key, provider); 140 | } 141 | } 142 | 143 | /** 144 | * 添加数据提供器 145 | * 146 | * @param key key 147 | * @param provider 数据提供器 148 | * @param flag 是否强制添加 149 | * @param type 150 | */ 151 | public void addProvider(String key, DataProvider provider, boolean flag) { 152 | if (provider != null && key != null) { 153 | if (flag) { 154 | providerMap.put(key, provider); 155 | } else { 156 | if (!providerMap.containsKey(key)) { 157 | providerMap.put(key, provider); 158 | } 159 | } 160 | } 161 | } 162 | 163 | /** 164 | * 是否已经添加 165 | * 166 | * @param key key 167 | * @return boolean 168 | */ 169 | @SuppressWarnings("unused") 170 | public boolean hasProvider(String key) { 171 | return providerMap.containsKey(key); 172 | } 173 | 174 | public RestAdapter getRestAdapter() { 175 | return restAdapter; 176 | } 177 | 178 | public void removeProvider(String key) { 179 | providerMap.remove(key); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/data/DataProvider.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.data; 2 | 3 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 4 | 5 | /** 6 | * Created by 州 on 2015/7/19 0019. 7 | * 数据提供器接口 8 | */ 9 | public interface DataProvider { 10 | 11 | /** 12 | * 数据持久化 13 | */ 14 | void persistence(); 15 | 16 | /** 17 | * 获取数据 18 | * 19 | * @return 数据 20 | */ 21 | T get(); 22 | 23 | /** 24 | * 设置数据 25 | * 26 | * @param t 数据 27 | */ 28 | void set(T t); 29 | 30 | /** 31 | * 从本地获取数据 32 | * 33 | * @param loadComplete 回调 34 | */ 35 | void getFromLocal(OnLoadCompleteListener loadComplete); 36 | 37 | /** 38 | * 从网络加载数据 39 | * 40 | * @param loadComplete 回调 41 | */ 42 | void getFromNet(OnLoadCompleteListener loadComplete); 43 | 44 | /** 45 | * 是否已经加载 46 | * 47 | * @return boolean 48 | */ 49 | boolean hasLoad(); 50 | 51 | /** 52 | * 是否需要缓存 53 | * 54 | * @return boolean 55 | */ 56 | boolean needCache(); 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/data/MemberProvider.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.data; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.File; 6 | 7 | import retrofit.Callback; 8 | import retrofit.RestAdapter; 9 | import retrofit.RetrofitError; 10 | import retrofit.client.Response; 11 | import com.zzhoujay.v2ex.R; 12 | import com.zzhoujay.v2ex.V2EX; 13 | import com.zzhoujay.v2ex.interfaces.MemberService; 14 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 15 | import com.zzhoujay.v2ex.model.Member; 16 | import com.zzhoujay.v2ex.util.FileUtils; 17 | 18 | /** 19 | * Created by zzhoujay on 2015/7/22 0022. 20 | * Member的数据提供者的实现 21 | */ 22 | public class MemberProvider implements DataProvider { 23 | 24 | public static final String SElF = "self.cache"; 25 | 26 | public String FILE_NAME = "member_"; 27 | 28 | private Member member; 29 | private MemberService memberService; 30 | private boolean isSelf; 31 | private String username; 32 | 33 | public MemberProvider(RestAdapter restAdapter, String username, boolean self) { 34 | this.isSelf = self; 35 | this.username = username; 36 | memberService = restAdapter.create(MemberService.class); 37 | FILE_NAME = isSelf ? SElF : (FILE_NAME + username); 38 | } 39 | 40 | @Override 41 | public void persistence() { 42 | if (hasLoad()) { 43 | new Thread() { 44 | @Override 45 | public void run() { 46 | File file = new File(V2EX.getInstance().getCacheDir(), FILE_NAME); 47 | FileUtils.writeObject(file, member); 48 | } 49 | }.start(); 50 | } 51 | } 52 | 53 | @Override 54 | public Member get() { 55 | return member; 56 | } 57 | 58 | @Override 59 | public void set(Member member) { 60 | this.member = member; 61 | } 62 | 63 | @Override 64 | public void getFromLocal(OnLoadCompleteListener loadComplete) { 65 | File file = new File(V2EX.getInstance().getCacheDir(), FILE_NAME); 66 | Member m = null; 67 | if (file.exists()) { 68 | try { 69 | m = (Member) FileUtils.readObject(file); 70 | } catch (Exception e) { 71 | Log.d("getFromLocal", "error", e); 72 | } 73 | } 74 | if (loadComplete != null) { 75 | loadComplete.loadComplete(m); 76 | } 77 | } 78 | 79 | @Override 80 | public void getFromNet(final OnLoadCompleteListener loadComplete) { 81 | if (!V2EX.getInstance().isNetworkConnected()) { 82 | //网络未连接 83 | V2EX.getInstance().toast(R.string.network_error); 84 | if (loadComplete != null) { 85 | loadComplete.loadComplete(null); 86 | } 87 | return; 88 | } 89 | memberService.getMember(username, new Callback() { 90 | @Override 91 | public void success(Member member, Response response) { 92 | Log.d("getFromNet", "success"); 93 | if (loadComplete != null) { 94 | loadComplete.loadComplete(member); 95 | } 96 | } 97 | 98 | @Override 99 | public void failure(RetrofitError error) { 100 | Log.d("getFromNet", "failure", error); 101 | if (loadComplete != null) { 102 | loadComplete.loadComplete(null); 103 | } 104 | } 105 | }); 106 | } 107 | 108 | @Override 109 | public boolean hasLoad() { 110 | return member != null; 111 | } 112 | 113 | @Override 114 | public boolean needCache() { 115 | return isSelf || V2EX.getInstance().saveCache(); 116 | } 117 | 118 | public static Member getSelf() { 119 | File file = new File(V2EX.getInstance().getCacheDir(), SElF); 120 | Member self = null; 121 | if (file.exists()) { 122 | try { 123 | self = (Member) FileUtils.readObject(file); 124 | } catch (Exception e) { 125 | Log.d("getSelf", "error", e); 126 | } 127 | } 128 | return self; 129 | } 130 | 131 | public static boolean clearSelf() { 132 | File file = new File(V2EX.getInstance().getCacheDir(), SElF); 133 | return file.exists() && file.delete(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/data/NodesProvider.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.data; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.File; 6 | import java.util.List; 7 | 8 | import retrofit.Callback; 9 | import retrofit.RestAdapter; 10 | import retrofit.RetrofitError; 11 | import retrofit.client.Response; 12 | import com.zzhoujay.v2ex.R; 13 | import com.zzhoujay.v2ex.V2EX; 14 | import com.zzhoujay.v2ex.interfaces.NodesService; 15 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 16 | import com.zzhoujay.v2ex.model.Node; 17 | import com.zzhoujay.v2ex.util.FileUtils; 18 | 19 | /** 20 | * Created by 州 on 2015/7/20 0020. 21 | * Node的数据提供器的实现 22 | */ 23 | public class NodesProvider implements DataProvider> { 24 | 25 | public static final String FILE_NAME = "node.cache"; 26 | 27 | private List nodes; 28 | private NodesService nodesService; 29 | 30 | public NodesProvider(RestAdapter restAdapter) { 31 | nodesService = restAdapter.create(NodesService.class); 32 | } 33 | 34 | @Override 35 | public void persistence() { 36 | if (nodes == null) { 37 | return; 38 | } 39 | new Thread() { 40 | @Override 41 | public void run() { 42 | File file = new File(V2EX.getInstance().getCacheDir(), FILE_NAME); 43 | FileUtils.writeObject(file, nodes); 44 | } 45 | }.start(); 46 | } 47 | 48 | @Override 49 | public List get() { 50 | return nodes; 51 | } 52 | 53 | @Override 54 | public void set(List nodes) { 55 | this.nodes = nodes; 56 | } 57 | 58 | @Override 59 | @SuppressWarnings("unchecked") 60 | public void getFromLocal(OnLoadCompleteListener> loadComplete) { 61 | File file = new File(V2EX.getInstance().getCacheDir(), FILE_NAME); 62 | List ns = null; 63 | if (file.exists()) { 64 | try { 65 | ns = (List) FileUtils.readObject(file); 66 | } catch (Exception e) { 67 | Log.d("getFromLocal", "NodesProvider", e); 68 | } 69 | } 70 | if (loadComplete != null) { 71 | loadComplete.loadComplete(ns); 72 | } 73 | } 74 | 75 | @Override 76 | public void getFromNet(final OnLoadCompleteListener> loadComplete) { 77 | if (!V2EX.getInstance().isNetworkConnected()) { 78 | //网络未连接 79 | V2EX.getInstance().toast(R.string.network_error); 80 | if (loadComplete != null) { 81 | loadComplete.loadComplete(null); 82 | } 83 | return; 84 | } 85 | nodesService.listNode(new Callback>() { 86 | @Override 87 | public void success(List nodes, Response response) { 88 | Log.d("getFromNet", "success"); 89 | if (loadComplete != null) { 90 | loadComplete.loadComplete(nodes); 91 | } 92 | } 93 | 94 | @Override 95 | public void failure(RetrofitError error) { 96 | Log.d("getFromNet", "failure", error); 97 | if (loadComplete != null) { 98 | loadComplete.loadComplete(null); 99 | } 100 | } 101 | }); 102 | } 103 | 104 | @Override 105 | public boolean hasLoad() { 106 | return nodes != null; 107 | } 108 | 109 | @Override 110 | public boolean needCache() { 111 | return true; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/data/RepliesProvider.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.data; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.File; 6 | import java.util.List; 7 | 8 | import retrofit.Callback; 9 | import retrofit.RestAdapter; 10 | import retrofit.RetrofitError; 11 | import retrofit.client.Response; 12 | import com.zzhoujay.v2ex.R; 13 | import com.zzhoujay.v2ex.V2EX; 14 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 15 | import com.zzhoujay.v2ex.interfaces.RepliesService; 16 | import com.zzhoujay.v2ex.model.Replies; 17 | import com.zzhoujay.v2ex.model.Topic; 18 | import com.zzhoujay.v2ex.util.FileUtils; 19 | 20 | /** 21 | * Created by 州 on 2015/7/20 0020. 22 | * Replies的数据提供器的实现 23 | */ 24 | public class RepliesProvider implements DataProvider> { 25 | 26 | public String FILE_NAME = "topic_replies_"; 27 | 28 | private List replies; 29 | private Topic topic; 30 | private RepliesService repliesService; 31 | 32 | public RepliesProvider(RestAdapter restAdapter, Topic topic) { 33 | this.topic = topic; 34 | FILE_NAME = FILE_NAME + topic.id; 35 | repliesService = restAdapter.create(RepliesService.class); 36 | } 37 | 38 | @Override 39 | public void persistence() { 40 | if(replies==null){ 41 | return; 42 | } 43 | new Thread() { 44 | @Override 45 | public void run() { 46 | File file = new File(V2EX.getInstance().getCacheDir(), FILE_NAME); 47 | FileUtils.writeObject(file, replies); 48 | } 49 | }.start(); 50 | } 51 | 52 | @Override 53 | public List get() { 54 | return replies; 55 | } 56 | 57 | @Override 58 | public void set(List replies) { 59 | this.replies = replies; 60 | } 61 | 62 | @Override 63 | @SuppressWarnings("unchecked") 64 | public void getFromLocal(OnLoadCompleteListener> loadComplete) { 65 | File file = new File(V2EX.getInstance().getCacheDir(), FILE_NAME); 66 | List rs = null; 67 | if (file.exists()) { 68 | try { 69 | rs = (List) FileUtils.readObject(file); 70 | } catch (Exception e) { 71 | Log.d("getFromLocal", "RepliesProvider", e); 72 | } 73 | } 74 | if (loadComplete != null) { 75 | loadComplete.loadComplete(rs); 76 | } 77 | } 78 | 79 | @Override 80 | public void getFromNet(final OnLoadCompleteListener> loadComplete) { 81 | if (!V2EX.getInstance().isNetworkConnected()) { 82 | //网络未连接 83 | V2EX.getInstance().toast(R.string.network_error); 84 | if (loadComplete != null) { 85 | loadComplete.loadComplete(null); 86 | } 87 | return; 88 | } 89 | repliesService.getReplise(topic.id, new Callback>() { 90 | @Override 91 | public void success(List replies, Response response) { 92 | Log.d("getFromNet", "success"); 93 | if (loadComplete != null) { 94 | loadComplete.loadComplete(replies); 95 | } 96 | } 97 | 98 | @Override 99 | public void failure(RetrofitError error) { 100 | Log.d("getFromNet", "failure", error); 101 | if (loadComplete != null) { 102 | loadComplete.loadComplete(null); 103 | } 104 | } 105 | }); 106 | } 107 | 108 | @Override 109 | public boolean hasLoad() { 110 | return replies != null; 111 | } 112 | 113 | @Override 114 | public boolean needCache() { 115 | return V2EX.getInstance().saveCache(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/data/TopicProvider.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.data; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.File; 6 | import java.util.List; 7 | 8 | import retrofit.Callback; 9 | import retrofit.RestAdapter; 10 | import retrofit.RetrofitError; 11 | import retrofit.client.Response; 12 | import com.zzhoujay.v2ex.R; 13 | import com.zzhoujay.v2ex.V2EX; 14 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 15 | import com.zzhoujay.v2ex.interfaces.TopicService; 16 | import com.zzhoujay.v2ex.model.Topic; 17 | import com.zzhoujay.v2ex.util.FileUtils; 18 | 19 | /** 20 | * Created by zzhoujay on 2015/7/22 0022. 21 | * Topic的数据提供器的实现 22 | */ 23 | public class TopicProvider implements DataProvider { 24 | 25 | public String FILE_NAME = "topic_"; 26 | 27 | private Topic topic; 28 | private TopicService topicService; 29 | private int id; 30 | 31 | public TopicProvider(RestAdapter restAdapter, int id) { 32 | this.id = id; 33 | topicService = restAdapter.create(TopicService.class); 34 | FILE_NAME += id; 35 | } 36 | 37 | 38 | @Override 39 | public void persistence() { 40 | if (!hasLoad()) { 41 | return; 42 | } 43 | new Thread() { 44 | @Override 45 | public void run() { 46 | File file = new File(V2EX.getInstance().getCacheDir(), FILE_NAME); 47 | FileUtils.writeObject(file, topic); 48 | } 49 | }.start(); 50 | } 51 | 52 | @Override 53 | public Topic get() { 54 | return topic; 55 | } 56 | 57 | @Override 58 | public void set(Topic topic) { 59 | this.topic = topic; 60 | } 61 | 62 | @Override 63 | public void getFromLocal(OnLoadCompleteListener loadComplete) { 64 | File file = new File(V2EX.getInstance().getCacheDir(), FILE_NAME); 65 | Topic t = null; 66 | if (file.exists()) { 67 | try { 68 | t = (Topic) FileUtils.readObject(file); 69 | } catch (Exception e) { 70 | Log.d("getFromLocal", "error", e); 71 | } 72 | } 73 | if (loadComplete != null) { 74 | loadComplete.loadComplete(t); 75 | } 76 | } 77 | 78 | @Override 79 | public void getFromNet(final OnLoadCompleteListener loadComplete) { 80 | if (!V2EX.getInstance().isNetworkConnected()) { 81 | //网络未连接 82 | V2EX.getInstance().toast(R.string.network_error); 83 | if (loadComplete != null) { 84 | loadComplete.loadComplete(null); 85 | } 86 | return; 87 | } 88 | topicService.getTopic(id, new Callback>() { 89 | @Override 90 | public void success(List topic, Response response) { 91 | Log.d("getFromNet", "success_topic"); 92 | if (loadComplete != null) { 93 | loadComplete.loadComplete(topic.get(0)); 94 | } 95 | } 96 | 97 | @Override 98 | public void failure(RetrofitError error) { 99 | Log.d("getFromNet", "failure_topic", error); 100 | if (loadComplete != null) { 101 | loadComplete.loadComplete(null); 102 | } 103 | } 104 | }); 105 | } 106 | 107 | @Override 108 | public boolean hasLoad() { 109 | return topic != null; 110 | } 111 | 112 | @Override 113 | public boolean needCache() { 114 | return V2EX.getInstance().saveCache(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/data/TopicsProvider.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.data; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import android.util.Log; 6 | 7 | import java.io.File; 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | import retrofit.Callback; 12 | import retrofit.RestAdapter; 13 | import retrofit.RetrofitError; 14 | import retrofit.client.Response; 15 | import com.zzhoujay.v2ex.R; 16 | import com.zzhoujay.v2ex.V2EX; 17 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 18 | import com.zzhoujay.v2ex.interfaces.TopicsService; 19 | import com.zzhoujay.v2ex.model.Topic; 20 | import com.zzhoujay.v2ex.util.FileUtils; 21 | 22 | /** 23 | * Created by 州 on 2015/7/20 0020. 24 | * Topic列表的数据提供器的实现 25 | */ 26 | public class TopicsProvider implements DataProvider> { 27 | 28 | private TopicType topicType; 29 | 30 | private List topics; 31 | private TopicsService topicsService; 32 | 33 | public TopicsProvider(RestAdapter restAdapter, TopicType topicType) { 34 | topicsService = restAdapter.create(TopicsService.class); 35 | this.topicType = topicType; 36 | } 37 | 38 | @Override 39 | public void persistence() { 40 | if (topics == null) { 41 | return; 42 | } 43 | new Thread() { 44 | @Override 45 | public void run() { 46 | File file = new File(V2EX.getInstance().getCacheDir(), topicType.fileName); 47 | FileUtils.writeObject(file, topics); 48 | } 49 | }.start(); 50 | } 51 | 52 | @Override 53 | public List get() { 54 | return topics; 55 | } 56 | 57 | @Override 58 | public void set(List topics) { 59 | this.topics = topics; 60 | } 61 | 62 | @Override 63 | @SuppressWarnings("unchecked") 64 | public void getFromLocal(OnLoadCompleteListener> loadComplete) { 65 | File file = new File(V2EX.getInstance().getCacheDir(), topicType.fileName); 66 | List ts = null; 67 | if (file.exists()) { 68 | try { 69 | ts = (List) FileUtils.readObject(file); 70 | } catch (Exception e) { 71 | Log.d("getFromLocal", "TopicsProvider", e); 72 | } 73 | } 74 | if (loadComplete != null) { 75 | loadComplete.loadComplete(ts); 76 | } 77 | } 78 | 79 | 80 | @Override 81 | public void getFromNet(final OnLoadCompleteListener> loadComplete) { 82 | if (!V2EX.getInstance().isNetworkConnected()) { 83 | //网络未连接 84 | V2EX.getInstance().toast(R.string.network_error); 85 | if (loadComplete != null) { 86 | loadComplete.loadComplete(null); 87 | } 88 | return; 89 | } 90 | //加载完成后的回调 91 | Callback> callback = new Callback>() { 92 | @Override 93 | public void success(List topics, Response response) { 94 | Log.d("getFromNet", "success"); 95 | if (loadComplete != null) { 96 | loadComplete.loadComplete(topics); 97 | } 98 | } 99 | 100 | @Override 101 | public void failure(RetrofitError error) { 102 | Log.d("getFromNet", "failure", error); 103 | if (loadComplete != null) { 104 | loadComplete.loadComplete(null); 105 | } 106 | } 107 | }; 108 | 109 | if (TopicType.FILE_NAME_LATEST.equals(topicType.fileName)) {//最新主题 110 | topicsService.getLatest(callback); 111 | } else if (TopicType.FILE_NAME_HOT.equals(topicType.fileName)) {//最热主题 112 | topicsService.getHot(callback); 113 | } else {//其他主题 114 | if (topicType.nodeName != null) {//通过节点名字查找 115 | topicsService.getTopicsByNodeName(topicType.nodeName, callback); 116 | } else if (topicType.userName != null) {//通过用户名查找 117 | topicsService.getTopicsByUserName(topicType.userName, callback); 118 | } else if (topicType.nodeId != -1) {//通过节点ID查找 119 | topicsService.getTopicByNodeId(topicType.nodeId, callback); 120 | } else {//异常情况 121 | V2EX.getInstance().toast(R.string.unknown_error); 122 | if (loadComplete != null) { 123 | loadComplete.loadComplete(null); 124 | } 125 | } 126 | } 127 | } 128 | 129 | 130 | @Override 131 | public boolean hasLoad() { 132 | return topics != null; 133 | } 134 | 135 | @Override 136 | public boolean needCache() { 137 | return topicType == TopicType.HOT || topicType == TopicType.LATEST; 138 | } 139 | 140 | public static class TopicType implements Serializable, Parcelable { 141 | 142 | public static final String FILE_NAME_LATEST = "latest.cache"; 143 | public static final String FILE_NAME_HOT = "hot.cache"; 144 | 145 | public String fileName; 146 | public String userName; 147 | public String nodeName; 148 | public int nodeId; 149 | 150 | public static final TopicType LATEST = new TopicType(FILE_NAME_LATEST); 151 | public static final TopicType HOT = new TopicType(FILE_NAME_HOT); 152 | 153 | private TopicType(String fileName) { 154 | this.fileName = fileName; 155 | userName = null; 156 | nodeName = null; 157 | nodeId = -1; 158 | } 159 | 160 | public static TopicType newTopicTypeByUserName(String fileName, String userName) { 161 | TopicType topicType = new TopicType(fileName); 162 | topicType.userName = userName; 163 | return topicType; 164 | } 165 | 166 | @SuppressWarnings("unused") 167 | public static TopicType newTopicTypeByNodeName(String fileName, String nodeName) { 168 | TopicType topicType = new TopicType(fileName); 169 | topicType.nodeName = nodeName; 170 | return topicType; 171 | } 172 | 173 | public static TopicType newTopicTypeByNodeId(String fileName, int nodeId) { 174 | TopicType topicType = new TopicType(fileName); 175 | topicType.nodeId = nodeId; 176 | return topicType; 177 | } 178 | 179 | 180 | @Override 181 | public int describeContents() { 182 | return 0; 183 | } 184 | 185 | @Override 186 | public void writeToParcel(Parcel dest, int flags) { 187 | dest.writeString(this.fileName); 188 | dest.writeString(this.userName); 189 | dest.writeString(this.nodeName); 190 | dest.writeInt(this.nodeId); 191 | } 192 | 193 | protected TopicType(Parcel in) { 194 | this.fileName = in.readString(); 195 | this.userName = in.readString(); 196 | this.nodeName = in.readString(); 197 | this.nodeId = in.readInt(); 198 | } 199 | 200 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 201 | public TopicType createFromParcel(Parcel source) { 202 | return new TopicType(source); 203 | } 204 | 205 | public TopicType[] newArray(int size) { 206 | return new TopicType[size]; 207 | } 208 | }; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/ClickCallback.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | /** 4 | * Created by 州 on 2015/7/20 0020. 5 | * 点击回调 6 | */ 7 | public interface ClickCallback { 8 | void callback(T t); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/MemberService.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | import retrofit.Callback; 4 | import retrofit.http.GET; 5 | import retrofit.http.Query; 6 | import com.zzhoujay.v2ex.model.Member; 7 | 8 | /** 9 | * Created by 州 on 2015/7/18 0018. 10 | * Member的Service 11 | */ 12 | public interface MemberService { 13 | @GET("/api/members/show.json") 14 | void getMember(@Query("username") String username, Callback memberCallback); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/NodeService.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | import retrofit.Callback; 4 | import retrofit.http.GET; 5 | import retrofit.http.Query; 6 | import com.zzhoujay.v2ex.model.Node; 7 | 8 | /** 9 | * Created by 州 on 2015/7/20 0020. 10 | * Node的Service 11 | */ 12 | @SuppressWarnings("unused") 13 | public interface NodeService { 14 | 15 | @GET("/api/nodes/show.json") 16 | @SuppressWarnings("unused") 17 | void getNode(@Query("id") int id, Callback node); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/NodesService.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | import java.util.List; 4 | 5 | import retrofit.Callback; 6 | import retrofit.http.GET; 7 | import com.zzhoujay.v2ex.model.Node; 8 | 9 | /** 10 | * Created by 州 on 2015/7/18 0018. 11 | * Node列表的Service 12 | */ 13 | public interface NodesService { 14 | @GET("/api/nodes/all.json") 15 | void listNode(Callback> ns); 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/Notifier.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | /** 4 | * Created by zzhoujay on 2015/7/28 0028. 5 | * 消息通知 6 | */ 7 | public interface Notifier { 8 | void notice(); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/OnItemClickListener.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | import android.view.View; 4 | 5 | /** 6 | * Created by 州 on 2015/7/20 0020. 7 | * 列表项的item点击回调接口 8 | */ 9 | public interface OnItemClickListener { 10 | void onItemClicked(View view, int position); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/OnLoadCompleteListener.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | /** 4 | * Created by zzhoujay on 2015/7/24 0024. 5 | * 加载完成后的回调接口 6 | */ 7 | public interface OnLoadCompleteListener { 8 | void loadComplete(T t); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/RepliesService.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | import java.util.List; 4 | 5 | import retrofit.Callback; 6 | import retrofit.http.GET; 7 | import retrofit.http.Query; 8 | import com.zzhoujay.v2ex.model.Replies; 9 | 10 | /** 11 | * Created by 州 on 2015/7/18 0018. 12 | * Replies的Service 13 | */ 14 | public interface RepliesService { 15 | @GET("/api/replies/show.json") 16 | void getReplise(@Query("topic_id") int topic_id, Callback> callback); 17 | 18 | @SuppressWarnings("unused") 19 | @GET("/api/replies/show.json") 20 | void getReplise(@Query("topic_id") int topic_id, @Query("page") int page, @Query("page_size") int page_size, Callback> callback); 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/TopicService.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | import java.util.List; 4 | 5 | import retrofit.Callback; 6 | import retrofit.http.GET; 7 | import retrofit.http.Query; 8 | import com.zzhoujay.v2ex.model.Topic; 9 | 10 | /** 11 | * Created by 州 on 2015/7/20 0020. 12 | * Topic的Service 13 | */ 14 | public interface TopicService { 15 | 16 | @GET("/api/topics/show.json") 17 | void getTopic(@Query("id") int id, Callback> topicCallback); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/interfaces/TopicsService.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.interfaces; 2 | 3 | import java.util.List; 4 | 5 | import retrofit.Callback; 6 | import retrofit.http.GET; 7 | import retrofit.http.Query; 8 | import com.zzhoujay.v2ex.model.Topic; 9 | 10 | /** 11 | * Created by 州 on 2015/7/18 0018. 12 | * Topic列表的Service 13 | */ 14 | public interface TopicsService { 15 | 16 | 17 | @GET("/api/topics/show.json") 18 | void getTopicsByUserName(@Query("username") String username, Callback> callback); 19 | 20 | @GET("/api/topics/show.json") 21 | void getTopicByNodeId(@Query("node_id") int node_id, Callback> callback); 22 | 23 | @GET("/api/topics/show.json") 24 | void getTopicsByNodeName(@Query("node_name") String node_name, Callback> callback); 25 | 26 | @GET("/api/topics/hot.json") 27 | void getHot(Callback> listCallback); 28 | 29 | @GET("/api/topics/latest.json") 30 | void getLatest(Callback> listCallback); 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/model/Member.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Created by 州 on 2015/7/18 0018. 10 | * Member的模型类 11 | */ 12 | public class Member implements Serializable, Parcelable { 13 | 14 | public static final String MEMBER = "member"; 15 | public static final String MEMBER_NAME = "member_name"; 16 | 17 | public int id; 18 | public String username; 19 | public String website; 20 | public String twitter; 21 | public String location; 22 | public String tagline; 23 | public String bio; 24 | public String avatar_mini; 25 | public String avatar_normal; 26 | public String avatar_large; 27 | public long created; 28 | 29 | @Override 30 | public String toString() { 31 | return "Member{" + 32 | "id=" + id + 33 | ", username='" + username + '\'' + 34 | ", website='" + website + '\'' + 35 | ", twitter='" + twitter + '\'' + 36 | ", location='" + location + '\'' + 37 | ", tagline='" + tagline + '\'' + 38 | ", bio='" + bio + '\'' + 39 | ", avatar_mini='" + avatar_mini + '\'' + 40 | ", avater_normal='" + avatar_normal + '\'' + 41 | ", avatar_large='" + avatar_large + '\'' + 42 | ", created=" + created + 43 | '}'; 44 | } 45 | 46 | 47 | @Override 48 | public int describeContents() { 49 | return 0; 50 | } 51 | 52 | @Override 53 | public void writeToParcel(Parcel dest, int flags) { 54 | dest.writeInt(this.id); 55 | dest.writeString(this.username); 56 | dest.writeString(this.website); 57 | dest.writeString(this.twitter); 58 | dest.writeString(this.location); 59 | dest.writeString(this.tagline); 60 | dest.writeString(this.bio); 61 | dest.writeString(this.avatar_mini); 62 | dest.writeString(this.avatar_normal); 63 | dest.writeString(this.avatar_large); 64 | dest.writeLong(this.created); 65 | } 66 | 67 | public Member() { 68 | } 69 | 70 | protected Member(Parcel in) { 71 | this.id = in.readInt(); 72 | this.username = in.readString(); 73 | this.website = in.readString(); 74 | this.twitter = in.readString(); 75 | this.location = in.readString(); 76 | this.tagline = in.readString(); 77 | this.bio = in.readString(); 78 | this.avatar_mini = in.readString(); 79 | this.avatar_normal = in.readString(); 80 | this.avatar_large = in.readString(); 81 | this.created = in.readLong(); 82 | } 83 | 84 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 85 | public Member createFromParcel(Parcel source) { 86 | return new Member(source); 87 | } 88 | 89 | public Member[] newArray(int size) { 90 | return new Member[size]; 91 | } 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/model/Node.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Created by 州 on 2015/7/18 0018. 10 | * Node的模型类 11 | */ 12 | public class Node implements Serializable, Parcelable { 13 | 14 | public static final String NODE = "node"; 15 | public static final String NODE_NAME = "node_name"; 16 | 17 | public int id; 18 | public String name; 19 | public String url; 20 | public String title; 21 | public String title_alternative; 22 | public int topics; 23 | public String header; 24 | public String footer; 25 | public long created; 26 | 27 | @Override 28 | public String toString() { 29 | return "Node{" + 30 | "id=" + id + 31 | ", name='" + name + '\'' + 32 | ", url='" + url + '\'' + 33 | ", title='" + title + '\'' + 34 | ", title_alternative='" + title_alternative + '\'' + 35 | ", topics=" + topics + 36 | ", num='" + header + '\'' + 37 | ", footer='" + footer + '\'' + 38 | ", created=" + created + 39 | '}'; 40 | } 41 | 42 | 43 | @Override 44 | public int describeContents() { 45 | return 0; 46 | } 47 | 48 | @Override 49 | public void writeToParcel(Parcel dest, int flags) { 50 | dest.writeInt(this.id); 51 | dest.writeString(this.name); 52 | dest.writeString(this.url); 53 | dest.writeString(this.title); 54 | dest.writeString(this.title_alternative); 55 | dest.writeInt(this.topics); 56 | dest.writeString(this.header); 57 | dest.writeString(this.footer); 58 | dest.writeLong(this.created); 59 | } 60 | 61 | public Node() { 62 | } 63 | 64 | protected Node(Parcel in) { 65 | this.id = in.readInt(); 66 | this.name = in.readString(); 67 | this.url = in.readString(); 68 | this.title = in.readString(); 69 | this.title_alternative = in.readString(); 70 | this.topics = in.readInt(); 71 | this.header = in.readString(); 72 | this.footer = in.readString(); 73 | this.created = in.readLong(); 74 | } 75 | 76 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 77 | public Node createFromParcel(Parcel source) { 78 | return new Node(source); 79 | } 80 | 81 | public Node[] newArray(int size) { 82 | return new Node[size]; 83 | } 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/model/Replies.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Created by 州 on 2015/7/18 0018. 10 | * 回复的模型类 11 | */ 12 | public class Replies implements Serializable, Parcelable { 13 | public int id; 14 | public int thanks; 15 | public String content; 16 | public String content_rendered; 17 | public Member member; 18 | public long created; 19 | public long last_modified; 20 | 21 | @Override 22 | public String toString() { 23 | return "Replies{" + 24 | "id=" + id + 25 | ", thanks=" + thanks + 26 | ", content='" + content + '\'' + 27 | ", content_rendered='" + content_rendered + '\'' + 28 | ", member=" + member + 29 | ", created=" + created + 30 | ", last_modified=" + last_modified + 31 | '}'; 32 | } 33 | 34 | 35 | @Override 36 | public int describeContents() { 37 | return 0; 38 | } 39 | 40 | @Override 41 | public void writeToParcel(Parcel dest, int flags) { 42 | dest.writeInt(this.id); 43 | dest.writeInt(this.thanks); 44 | dest.writeString(this.content); 45 | dest.writeString(this.content_rendered); 46 | dest.writeSerializable(this.member); 47 | dest.writeLong(this.created); 48 | dest.writeLong(this.last_modified); 49 | } 50 | 51 | public Replies() { 52 | } 53 | 54 | protected Replies(Parcel in) { 55 | this.id = in.readInt(); 56 | this.thanks = in.readInt(); 57 | this.content = in.readString(); 58 | this.content_rendered = in.readString(); 59 | this.member = (Member) in.readSerializable(); 60 | this.created = in.readLong(); 61 | this.last_modified = in.readLong(); 62 | } 63 | 64 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 65 | public Replies createFromParcel(Parcel source) { 66 | return new Replies(source); 67 | } 68 | 69 | public Replies[] newArray(int size) { 70 | return new Replies[size]; 71 | } 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/model/Topic.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.model; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * Created by 州 on 2015/7/18 0018. 10 | * Topic的模型类 11 | */ 12 | public class Topic implements Serializable, Parcelable { 13 | 14 | public static final String TOPIC = "topic"; 15 | 16 | public int id; 17 | public String title; 18 | public String url; 19 | public String content; 20 | public String content_rendered; 21 | public int replies; 22 | public Member member; 23 | public Node node; 24 | public long created; 25 | public long lastModified; 26 | public long lastTouched; 27 | 28 | 29 | @Override 30 | public String toString() { 31 | return "Topic{" + 32 | "id=" + id + 33 | ", title='" + title + '\'' + 34 | ", url='" + url + '\'' + 35 | ", content='" + content + '\'' + 36 | ", content_rendered='" + content_rendered + '\'' + 37 | ", replies=" + replies + 38 | ", member=" + member + 39 | ", node=" + node + 40 | ", created=" + created + 41 | ", lastModified=" + lastModified + 42 | ", lastTouched=" + lastTouched + 43 | '}'; 44 | } 45 | 46 | @Override 47 | public int describeContents() { 48 | return 0; 49 | } 50 | 51 | @Override 52 | public void writeToParcel(Parcel dest, int flags) { 53 | dest.writeInt(this.id); 54 | dest.writeString(this.title); 55 | dest.writeString(this.url); 56 | dest.writeString(this.content); 57 | dest.writeString(this.content_rendered); 58 | dest.writeInt(this.replies); 59 | dest.writeSerializable(this.member); 60 | dest.writeSerializable(this.node); 61 | dest.writeLong(this.created); 62 | dest.writeLong(this.lastModified); 63 | dest.writeLong(this.lastTouched); 64 | } 65 | 66 | public Topic() { 67 | } 68 | 69 | protected Topic(Parcel in) { 70 | this.id = in.readInt(); 71 | this.title = in.readString(); 72 | this.url = in.readString(); 73 | this.content = in.readString(); 74 | this.content_rendered = in.readString(); 75 | this.replies = in.readInt(); 76 | this.member = (Member) in.readSerializable(); 77 | this.node = (Node) in.readSerializable(); 78 | this.created = in.readLong(); 79 | this.lastModified = in.readLong(); 80 | this.lastTouched = in.readLong(); 81 | } 82 | 83 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 84 | public Topic createFromParcel(Parcel source) { 85 | return new Topic(source); 86 | } 87 | 88 | public Topic[] newArray(int size) { 89 | return new Topic[size]; 90 | } 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/net/NetworkManager.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.net; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | import android.support.annotation.NonNull; 6 | import android.util.Log; 7 | 8 | import com.squareup.okhttp.Callback; 9 | import com.squareup.okhttp.OkHttpClient; 10 | import com.squareup.okhttp.Request; 11 | import com.squareup.okhttp.Response; 12 | 13 | import java.io.IOException; 14 | import java.net.CookieManager; 15 | import java.net.CookiePolicy; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | import com.zzhoujay.v2ex.V2EX; 19 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 20 | 21 | /** 22 | * Created by zzhoujay on 2015/7/27 0027. 23 | * 网络请求管理器 24 | */ 25 | public class NetworkManager { 26 | 27 | private static NetworkManager networkManager; 28 | private OkHttpClient client; 29 | private Handler mainHandler; 30 | 31 | 32 | private NetworkManager() { 33 | client = new OkHttpClient(); 34 | CookieManager manager = new CookieManager(new PersistentCookieStore(V2EX.getInstance()), CookiePolicy.ACCEPT_ALL); 35 | client.setCookieHandler(manager); 36 | client.setFollowRedirects(false); 37 | client.getCookieHandler(); 38 | client.setConnectTimeout(3, TimeUnit.SECONDS); 39 | client.setReadTimeout(3, TimeUnit.SECONDS); 40 | client.setWriteTimeout(3, TimeUnit.SECONDS); 41 | mainHandler = new Handler(Looper.getMainLooper()); 42 | } 43 | 44 | public static NetworkManager getInstance() { 45 | if (networkManager == null) { 46 | networkManager = new NetworkManager(); 47 | } 48 | return networkManager; 49 | } 50 | 51 | /** 52 | * 获取Request.Builder 53 | * 54 | * @return builder 55 | */ 56 | public Request.Builder requestBuilder() { 57 | Request.Builder builder; 58 | builder = new Request.Builder() 59 | .addHeader("Cache-Control", "max-age=0") 60 | .addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") 61 | .addHeader("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7") 62 | .addHeader("Accept-Language", "zh-CN, en-US") 63 | .addHeader("Host", "v2ex.com") 64 | .addHeader("X-Requested-With", "com.android.browser") 65 | .addHeader("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.2.1; en-us; M040 Build/JOP40D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30"); 66 | return builder; 67 | } 68 | 69 | /** 70 | * 异步请求 71 | * 72 | * @param request 请求体 73 | * @param callback 回调 74 | */ 75 | public void request(Request request, final Callback callback) { 76 | client.newCall(request).enqueue(new Callback() { 77 | @Override 78 | public void onFailure(final Request request, final IOException e) { 79 | mainHandler.post(new Runnable() { 80 | @Override 81 | public void run() { 82 | if (callback != null) { 83 | callback.onFailure(request, e); 84 | } 85 | } 86 | }); 87 | } 88 | 89 | @Override 90 | public void onResponse(final Response response) throws IOException { 91 | mainHandler.post(new Runnable() { 92 | 93 | @Override 94 | public void run() { 95 | if (callback != null) { 96 | try { 97 | callback.onResponse(response); 98 | } catch (IOException e) { 99 | e.printStackTrace(); 100 | } 101 | } 102 | } 103 | }); 104 | } 105 | }); 106 | } 107 | 108 | /** 109 | * 异步请求字符串内容 110 | * 111 | * @param request 请求体 112 | * @param loadComplete 回调 113 | */ 114 | public void requestString(Request request, @NonNull final OnLoadCompleteListener loadComplete) { 115 | client.newCall(request).enqueue(new Callback() { 116 | @Override 117 | public void onFailure(Request request, IOException e) { 118 | Log.d("requestString", "failure", e); 119 | mainHandler.post(new Runnable() { 120 | @Override 121 | public void run() { 122 | loadComplete.loadComplete(null); 123 | } 124 | }); 125 | } 126 | 127 | @Override 128 | public void onResponse(Response response) throws IOException { 129 | Log.d("requestString", "success"); 130 | final String str = response.body().string(); 131 | mainHandler.post(new Runnable() { 132 | @Override 133 | public void run() { 134 | loadComplete.loadComplete(str); 135 | } 136 | }); 137 | } 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/net/SerializableHttpCookie.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.net; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.ObjectInputStream; 9 | import java.io.ObjectOutputStream; 10 | import java.io.Serializable; 11 | import java.lang.reflect.Field; 12 | import java.net.HttpCookie; 13 | 14 | /** 15 | * Created by zzhoujay on 2015/7/27 0027. 16 | * 可序列化的cookie 17 | */ 18 | public class SerializableHttpCookie implements Serializable { 19 | private static final String TAG = SerializableHttpCookie.class 20 | .getSimpleName(); 21 | 22 | private static final long serialVersionUID = 6374381323722046732L; 23 | 24 | private transient HttpCookie cookie; 25 | 26 | // Workaround httpOnly: The httpOnly attribute is not accessible so when we 27 | // serialize and deserialize the cookie it not preserve the same value. We 28 | // need to access it using reflection 29 | private Field fieldHttpOnly; 30 | 31 | public SerializableHttpCookie() { 32 | } 33 | 34 | public String encode(HttpCookie cookie) { 35 | this.cookie = cookie; 36 | 37 | ByteArrayOutputStream os = new ByteArrayOutputStream(); 38 | try { 39 | ObjectOutputStream outputStream = new ObjectOutputStream(os); 40 | outputStream.writeObject(this); 41 | } catch (IOException e) { 42 | return null; 43 | } 44 | 45 | return byteArrayToHexString(os.toByteArray()); 46 | } 47 | 48 | public HttpCookie decode(String encodedCookie) { 49 | byte[] bytes = hexStringToByteArray(encodedCookie); 50 | ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( 51 | bytes); 52 | HttpCookie cookie = null; 53 | try { 54 | ObjectInputStream objectInputStream = new ObjectInputStream( 55 | byteArrayInputStream); 56 | cookie = ((SerializableHttpCookie) objectInputStream.readObject()).cookie; 57 | } catch (IOException e) { 58 | Log.d(TAG, "IOException in decodeCookie", e); 59 | } catch (ClassNotFoundException e) { 60 | Log.d(TAG, "ClassNotFoundException in decodeCookie", e); 61 | } 62 | 63 | return cookie; 64 | } 65 | 66 | // Workaround httpOnly (getter) 67 | private boolean getHttpOnly() { 68 | try { 69 | initFieldHttpOnly(); 70 | return (boolean) fieldHttpOnly.get(cookie); 71 | } catch (Exception e) { 72 | // NoSuchFieldException || IllegalAccessException || 73 | // IllegalArgumentException 74 | Log.w(TAG, e); 75 | } 76 | return false; 77 | } 78 | 79 | // Workaround httpOnly (setter) 80 | private void setHttpOnly(boolean httpOnly) { 81 | try { 82 | initFieldHttpOnly(); 83 | fieldHttpOnly.set(cookie, httpOnly); 84 | } catch (Exception e) { 85 | // NoSuchFieldException || IllegalAccessException || 86 | // IllegalArgumentException 87 | Log.w(TAG, e); 88 | } 89 | } 90 | 91 | private void initFieldHttpOnly() throws NoSuchFieldException { 92 | fieldHttpOnly = cookie.getClass().getDeclaredField("httpOnly"); 93 | fieldHttpOnly.setAccessible(true); 94 | } 95 | 96 | private void writeObject(ObjectOutputStream out) throws IOException { 97 | out.writeObject(cookie.getName()); 98 | out.writeObject(cookie.getValue()); 99 | out.writeObject(cookie.getComment()); 100 | out.writeObject(cookie.getCommentURL()); 101 | out.writeObject(cookie.getDomain()); 102 | out.writeLong(cookie.getMaxAge()); 103 | out.writeObject(cookie.getPath()); 104 | out.writeObject(cookie.getPortlist()); 105 | out.writeInt(cookie.getVersion()); 106 | out.writeBoolean(cookie.getSecure()); 107 | out.writeBoolean(cookie.getDiscard()); 108 | out.writeBoolean(getHttpOnly()); 109 | } 110 | 111 | private void readObject(ObjectInputStream in) throws IOException, 112 | ClassNotFoundException { 113 | String name = (String) in.readObject(); 114 | String value = (String) in.readObject(); 115 | cookie = new HttpCookie(name, value); 116 | cookie.setComment((String) in.readObject()); 117 | cookie.setCommentURL((String) in.readObject()); 118 | cookie.setDomain((String) in.readObject()); 119 | cookie.setMaxAge(in.readLong()); 120 | cookie.setPath((String) in.readObject()); 121 | cookie.setPortlist((String) in.readObject()); 122 | cookie.setVersion(in.readInt()); 123 | cookie.setSecure(in.readBoolean()); 124 | cookie.setDiscard(in.readBoolean()); 125 | setHttpOnly(in.readBoolean()); 126 | } 127 | 128 | /** 129 | * Using some super basic byte array <-> hex conversions so we don't 130 | * have to rely on any large Base64 libraries. Can be overridden if you 131 | * like! 132 | * 133 | * @param bytes byte array to be converted 134 | * @return string containing hex values 135 | */ 136 | private String byteArrayToHexString(byte[] bytes) { 137 | StringBuilder sb = new StringBuilder(bytes.length * 2); 138 | for (byte element : bytes) { 139 | int v = element & 0xff; 140 | if (v < 16) { 141 | sb.append('0'); 142 | } 143 | sb.append(Integer.toHexString(v)); 144 | } 145 | return sb.toString(); 146 | } 147 | 148 | /** 149 | * Converts hex values from strings to byte array 150 | * 151 | * @param hexString string of hex-encoded values 152 | * @return decoded byte array 153 | */ 154 | private byte[] hexStringToByteArray(String hexString) { 155 | int len = hexString.length(); 156 | byte[] data = new byte[len / 2]; 157 | for (int i = 0; i < len; i += 2) { 158 | data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character 159 | .digit(hexString.charAt(i + 1), 16)); 160 | } 161 | return data; 162 | } 163 | } -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/activity/LoginActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.activity; 2 | 3 | import android.app.ProgressDialog; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.os.Parcelable; 7 | import android.support.design.widget.TextInputLayout; 8 | import android.support.v7.app.ActionBar; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.support.v7.widget.Toolbar; 11 | import android.text.Editable; 12 | import android.text.TextUtils; 13 | import android.text.TextWatcher; 14 | import android.view.MenuItem; 15 | import android.view.View; 16 | import android.widget.Button; 17 | import android.widget.EditText; 18 | 19 | import com.zzhoujay.v2ex.R; 20 | import com.zzhoujay.v2ex.V2EX; 21 | import com.zzhoujay.v2ex.data.DataManger; 22 | import com.zzhoujay.v2ex.data.MemberProvider; 23 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 24 | import com.zzhoujay.v2ex.model.Member; 25 | import com.zzhoujay.v2ex.util.UserUtils; 26 | 27 | /** 28 | * Created by zzhoujay on 2015/7/24 0024. 29 | * 登录 30 | */ 31 | public class LoginActivity extends AppCompatActivity { 32 | 33 | private EditText username, password; 34 | private ProgressDialog progressDialogUserInfo; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_login); 40 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 41 | setSupportActionBar(toolbar); 42 | ActionBar actionBar = getSupportActionBar(); 43 | if (actionBar != null) { 44 | actionBar.setDisplayHomeAsUpEnabled(true); 45 | actionBar.setDisplayShowTitleEnabled(true); 46 | } 47 | setTitle(R.string.signin); 48 | 49 | initView(); 50 | } 51 | 52 | private void initView() { 53 | final TextInputLayout usernameLayout = (TextInputLayout) findViewById(R.id.login_username_layout); 54 | final TextInputLayout passwordLayout = (TextInputLayout) findViewById(R.id.login_password_layout); 55 | username = (EditText) findViewById(R.id.login_username); 56 | password = (EditText) findViewById(R.id.login_password); 57 | Button login = (Button) findViewById(R.id.login_btn); 58 | 59 | username.addTextChangedListener(new TextWatcher() { 60 | @Override 61 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 62 | 63 | } 64 | 65 | @Override 66 | public void onTextChanged(CharSequence s, int start, int before, int count) { 67 | 68 | } 69 | 70 | @Override 71 | public void afterTextChanged(Editable s) { 72 | if (TextUtils.isEmpty(username.getText())) { 73 | usernameLayout.setError(getString(R.string.username_empty)); 74 | usernameLayout.setErrorEnabled(true); 75 | } else { 76 | usernameLayout.setErrorEnabled(false); 77 | } 78 | } 79 | }); 80 | 81 | password.addTextChangedListener(new TextWatcher() { 82 | @Override 83 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 84 | 85 | } 86 | 87 | @Override 88 | public void onTextChanged(CharSequence s, int start, int before, int count) { 89 | 90 | } 91 | 92 | @Override 93 | public void afterTextChanged(Editable s) { 94 | if (TextUtils.isEmpty(password.getText())) { 95 | passwordLayout.setErrorEnabled(true); 96 | passwordLayout.setError(getString(R.string.password_empty)); 97 | } else { 98 | passwordLayout.setErrorEnabled(false); 99 | } 100 | } 101 | }); 102 | 103 | login.setOnClickListener(new View.OnClickListener() { 104 | @Override 105 | public void onClick(View v) { 106 | final String usernameStr = username.getText().toString(); 107 | final String passwordStr = password.getText().toString(); 108 | if (usernameStr.isEmpty()) { 109 | V2EX.getInstance().toast(R.string.username_empty); 110 | return; 111 | } 112 | if (passwordStr.isEmpty()) { 113 | V2EX.getInstance().toast(R.string.password_empty); 114 | } 115 | final ProgressDialog progressDialog = new ProgressDialog(LoginActivity.this); 116 | progressDialog.setMessage(getString(R.string.signin_progress)); 117 | progressDialog.setCanceledOnTouchOutside(false); 118 | progressDialog.setCancelable(false); 119 | progressDialog.show(); 120 | UserUtils.login(usernameStr, passwordStr, new OnLoadCompleteListener() { 121 | @Override 122 | public void loadComplete(Boolean aBoolean) { 123 | if (aBoolean) { 124 | V2EX.getInstance().toast(R.string.login_success); 125 | getUserInfo(usernameStr); 126 | } else { 127 | V2EX.getInstance().toast(R.string.login_error); 128 | } 129 | progressDialog.dismiss(); 130 | } 131 | }); 132 | } 133 | }); 134 | 135 | } 136 | 137 | private void getUserInfo(String username) { 138 | MemberProvider memberProvider = new MemberProvider(DataManger.getInstance().getRestAdapter(), username, true); 139 | DataManger.getInstance().addProvider(memberProvider.FILE_NAME, memberProvider, true); 140 | progressDialogUserInfo = new ProgressDialog(LoginActivity.this); 141 | progressDialogUserInfo.setMessage(getString(R.string.get_user_info)); 142 | progressDialogUserInfo.show(); 143 | DataManger.getInstance().getData(MemberProvider.SElF, onLoadListener); 144 | } 145 | 146 | private OnLoadCompleteListener onLoadListener = new OnLoadCompleteListener() { 147 | @Override 148 | public void loadComplete(Member member) { 149 | V2EX.getInstance().setSelf(member); 150 | progressDialogUserInfo.dismiss(); 151 | Intent intent = new Intent(LoginActivity.this, MemberActivity.class); 152 | intent.putExtra(Member.MEMBER, (Parcelable) member); 153 | startActivity(intent); 154 | finish(); 155 | } 156 | }; 157 | 158 | @Override 159 | public boolean onOptionsItemSelected(MenuItem item) { 160 | switch (item.getItemId()) { 161 | case android.R.id.home: 162 | finish(); 163 | return true; 164 | } 165 | return super.onOptionsItemSelected(item); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/activity/MemberActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.activity; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.support.design.widget.AppBarLayout; 7 | import android.support.design.widget.CollapsingToolbarLayout; 8 | import android.support.v7.app.ActionBar; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.support.v7.widget.Toolbar; 11 | import android.text.TextUtils; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | import android.widget.ImageView; 15 | import android.widget.TextView; 16 | 17 | 18 | import com.bumptech.glide.Glide; 19 | import com.zzhoujay.v2ex.R; 20 | import com.zzhoujay.v2ex.V2EX; 21 | import com.zzhoujay.v2ex.data.DataManger; 22 | import com.zzhoujay.v2ex.data.MemberProvider; 23 | import com.zzhoujay.v2ex.data.TopicsProvider; 24 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 25 | import com.zzhoujay.v2ex.model.Member; 26 | import com.zzhoujay.v2ex.ui.fragment.TopicsFragment; 27 | import com.zzhoujay.v2ex.util.UserUtils; 28 | 29 | /** 30 | * Created by zzhoujay on 2015/7/22 0022. 31 | * Member详细资料 32 | */ 33 | public class MemberActivity extends AppCompatActivity implements AppBarLayout.OnOffsetChangedListener { 34 | 35 | private CollapsingToolbarLayout collapsingToolbarLayout; 36 | private Member member; 37 | private ImageView icon; 38 | private TextView name; 39 | private AppBarLayout appBarLayout; 40 | private MemberProvider memberProvider; 41 | private TopicsFragment topicsFragment; 42 | 43 | @Override 44 | protected void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.activity_member); 47 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 48 | setSupportActionBar(toolbar); 49 | ActionBar actionBar = getSupportActionBar(); 50 | if (actionBar != null) { 51 | actionBar.setDisplayHomeAsUpEnabled(true); 52 | actionBar.setDisplayHomeAsUpEnabled(true); 53 | actionBar.setDisplayShowTitleEnabled(true); 54 | } 55 | 56 | Intent intent = getIntent(); 57 | Uri uri = intent.getData(); 58 | String username = null; 59 | if (uri != null) { 60 | username = uri.getPathSegments().get(1); 61 | } else { 62 | if (intent.hasExtra(Member.MEMBER)) { 63 | member = intent.getParcelableExtra(Member.MEMBER); 64 | } else if (intent.hasExtra(Member.MEMBER_NAME)) { 65 | username = intent.getStringExtra(Member.MEMBER_NAME); 66 | } 67 | } 68 | 69 | 70 | initView(); 71 | 72 | 73 | if (username != null) { 74 | memberProvider = new MemberProvider(DataManger.getInstance().getRestAdapter() 75 | , username, V2EX.getInstance().isSelf(username)); 76 | DataManger.getInstance().addProvider(memberProvider.FILE_NAME, memberProvider); 77 | DataManger.getInstance().getData(memberProvider.FILE_NAME, memberOnLoadComplete); 78 | } else if (member != null) { 79 | initData(member); 80 | } 81 | 82 | } 83 | 84 | private void initData(Member member) { 85 | if (member != null) { 86 | collapsingToolbarLayout.setTitle(member.username); 87 | name.setText(TextUtils.isEmpty(member.bio) ? getString(R.string.describe_empty) : member.bio); 88 | Glide 89 | .with(this) 90 | .load("http:" + member.avatar_large) 91 | .placeholder(R.mipmap.ic_launcher) 92 | .error(R.mipmap.ic_launcher) 93 | .centerCrop() 94 | .crossFade() 95 | .into(icon); 96 | topicsFragment = TopicsFragment.newInstance(TopicsProvider.TopicType.newTopicTypeByUserName("member_topic_" + member.username, member.username)); 97 | getSupportFragmentManager().beginTransaction().add(R.id.member_fragment, topicsFragment).commit(); 98 | } 99 | } 100 | 101 | private void initView() { 102 | collapsingToolbarLayout = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar); 103 | appBarLayout = (AppBarLayout) findViewById(R.id.appBar); 104 | icon = (ImageView) findViewById(R.id.member_icon); 105 | name = (TextView) findViewById(R.id.member_name); 106 | 107 | 108 | } 109 | 110 | private OnLoadCompleteListener memberOnLoadComplete = new OnLoadCompleteListener() { 111 | @Override 112 | public void loadComplete(Member member) { 113 | initData(member); 114 | } 115 | }; 116 | 117 | @Override 118 | public boolean onCreateOptionsMenu(Menu menu) { 119 | if (V2EX.getInstance().isSelf(member == null ? null : member.username)) { 120 | MenuItem item = menu.add(0, 10086, 1, R.string.logout); 121 | item.setIcon(R.drawable.ic_exit_to_app_white); 122 | item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 123 | MenuItem item1 = menu.add(0, 10010, 0, R.string.refresh); 124 | item1.setIcon(R.drawable.ic_refresh_white); 125 | item1.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 126 | } 127 | 128 | return super.onCreateOptionsMenu(menu); 129 | } 130 | 131 | @Override 132 | public boolean onOptionsItemSelected(MenuItem item) { 133 | switch (item.getItemId()) { 134 | case android.R.id.home: 135 | finish(); 136 | return true; 137 | case 10086: 138 | if (V2EX.getInstance().isNetworkConnected()) { 139 | if (UserUtils.logout()) { 140 | V2EX.getInstance().toast(R.string.logout_success); 141 | finish(); 142 | } 143 | } else { 144 | V2EX.getInstance().toast(R.string.network_error); 145 | } 146 | return true; 147 | case 10010: 148 | if (member != null && V2EX.getInstance().isSelf(member.username)) { 149 | DataManger.getInstance().refresh(MemberProvider.SElF, new OnLoadCompleteListener() { 150 | @Override 151 | public void loadComplete(Member member) { 152 | if (member != null) { 153 | initData(member); 154 | V2EX.getInstance().toast(R.string.refresh_success); 155 | } else { 156 | V2EX.getInstance().toast(R.string.refresh_error); 157 | } 158 | } 159 | }); 160 | } 161 | return true; 162 | } 163 | return super.onOptionsItemSelected(item); 164 | } 165 | 166 | @Override 167 | public void onOffsetChanged(AppBarLayout appBarLayout, int i) { 168 | if (topicsFragment != null) { 169 | topicsFragment.setSwipeRefreshEnable(i == 0); 170 | } 171 | } 172 | 173 | @Override 174 | protected void onResume() { 175 | super.onResume(); 176 | appBarLayout.addOnOffsetChangedListener(this); 177 | } 178 | 179 | @Override 180 | protected void onPause() { 181 | super.onPause(); 182 | appBarLayout.removeOnOffsetChangedListener(this); 183 | } 184 | 185 | @Override 186 | protected void onDestroy() { 187 | super.onDestroy(); 188 | if (member != null && memberProvider != null && V2EX.getInstance().isSelf(member.username)) { 189 | DataManger.getInstance().removeProvider(memberProvider.FILE_NAME); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/activity/NewTopicActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.activity; 2 | 3 | import android.app.ProgressDialog; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.app.ActionBar; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.text.Editable; 10 | import android.text.TextWatcher; 11 | import android.util.Log; 12 | import android.view.Menu; 13 | import android.view.MenuItem; 14 | import android.widget.EditText; 15 | 16 | import com.zzhoujay.v2ex.R; 17 | import com.zzhoujay.v2ex.V2EX; 18 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 19 | import com.zzhoujay.v2ex.model.Node; 20 | import com.zzhoujay.v2ex.util.UserUtils; 21 | 22 | /** 23 | * Created by zzhoujay on 2015/7/28 0028. 24 | * 新建主题 25 | */ 26 | public class NewTopicActivity extends AppCompatActivity { 27 | 28 | private String nodeName; 29 | private MenuItem item; 30 | private EditText title, content; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_new_topic); 36 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 37 | setSupportActionBar(toolbar); 38 | ActionBar actionBar = getSupportActionBar(); 39 | if (actionBar != null) { 40 | actionBar.setDisplayShowTitleEnabled(true); 41 | actionBar.setDisplayHomeAsUpEnabled(true); 42 | } 43 | 44 | setTitle(R.string.new_topic); 45 | 46 | Intent intent = getIntent(); 47 | if (intent.hasExtra(Node.NODE_NAME)) { 48 | nodeName = intent.getStringExtra(Node.NODE_NAME); 49 | } 50 | title = (EditText) findViewById(R.id.new_topic_title); 51 | content = (EditText) findViewById(R.id.new_topic_content); 52 | 53 | title.addTextChangedListener(new TextWatcher() { 54 | @Override 55 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 56 | 57 | } 58 | 59 | @Override 60 | public void onTextChanged(CharSequence s, int start, int before, int count) { 61 | 62 | } 63 | 64 | @Override 65 | public void afterTextChanged(Editable s) { 66 | if (title.getText().toString().isEmpty()) { 67 | item.setEnabled(false); 68 | } else { 69 | item.setEnabled(true); 70 | } 71 | } 72 | }); 73 | } 74 | 75 | @Override 76 | public boolean onCreateOptionsMenu(Menu menu) { 77 | item = menu.add(0, 10086, 0, R.string.new_topic_publish); 78 | item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 79 | item.setIcon(R.drawable.ic_done_white); 80 | return super.onCreateOptionsMenu(menu); 81 | } 82 | 83 | @Override 84 | public boolean onOptionsItemSelected(MenuItem item) { 85 | switch (item.getItemId()) { 86 | case android.R.id.home: 87 | finish(); 88 | return true; 89 | case 10086: 90 | Log.d("test", "clicked:" + nodeName); 91 | String c = content.getText().toString(); 92 | if (c.isEmpty()) { 93 | V2EX.getInstance().toast(R.string.content_empty); 94 | } else { 95 | if (nodeName != null) { 96 | String t = title.getText().toString(); 97 | if (t.isEmpty()) { 98 | V2EX.getInstance().toast(R.string.title_empty); 99 | } else { 100 | final ProgressDialog progressDialog = new ProgressDialog(NewTopicActivity.this); 101 | progressDialog.setMessage(getString(R.string.new_topic_progress)); 102 | progressDialog.show(); 103 | UserUtils.createTopic(nodeName, t, c, new OnLoadCompleteListener() { 104 | @Override 105 | public void loadComplete(Boolean aBoolean) { 106 | progressDialog.dismiss(); 107 | if (aBoolean) { 108 | V2EX.getInstance().toast(R.string.new_topic_success); 109 | finish(); 110 | } else { 111 | V2EX.getInstance().toast(R.string.new_topic_error); 112 | } 113 | } 114 | }); 115 | } 116 | } 117 | } 118 | return true; 119 | } 120 | return super.onOptionsItemSelected(item); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/activity/NodeActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.design.widget.FloatingActionButton; 6 | import android.support.v7.app.ActionBar; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.view.MenuItem; 10 | import android.view.View; 11 | 12 | import com.zzhoujay.v2ex.R; 13 | import com.zzhoujay.v2ex.V2EX; 14 | import com.zzhoujay.v2ex.data.TopicsProvider; 15 | import com.zzhoujay.v2ex.model.Node; 16 | import com.zzhoujay.v2ex.ui.fragment.TopicsFragment; 17 | 18 | /** 19 | * Created by 州 on 2015/7/20 0020. 20 | * Node详情 21 | */ 22 | public class NodeActivity extends AppCompatActivity { 23 | 24 | private Node node; 25 | private FloatingActionButton floatingActionButton; 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | setContentView(R.layout.activity_fragment_floatactionbar); 31 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 32 | setSupportActionBar(toolbar); 33 | ActionBar actionBar = getSupportActionBar(); 34 | if (actionBar != null) { 35 | actionBar.setDisplayUseLogoEnabled(true); 36 | actionBar.setDisplayShowTitleEnabled(true); 37 | actionBar.setDisplayShowHomeEnabled(true); 38 | actionBar.setDisplayHomeAsUpEnabled(true); 39 | } 40 | floatingActionButton = (FloatingActionButton) findViewById(R.id.floatingActionBar); 41 | 42 | Intent intent = getIntent(); 43 | if (intent.hasExtra(Node.NODE)) { 44 | node = intent.getParcelableExtra(Node.NODE); 45 | setTitle(node.name); 46 | TopicsProvider.TopicType topicType = TopicsProvider.TopicType.newTopicTypeByNodeId("node_" + node.id, node.id); 47 | TopicsFragment topicsFragment = TopicsFragment.newInstance(topicType); 48 | getSupportFragmentManager().beginTransaction().add(R.id.fragment_content, topicsFragment).commit(); 49 | floatingActionButton.setOnClickListener(new View.OnClickListener() { 50 | @Override 51 | public void onClick(View v) { 52 | if (node != null) { 53 | Intent i = new Intent(NodeActivity.this, NewTopicActivity.class); 54 | i.putExtra(Node.NODE_NAME, node.name); 55 | startActivity(i); 56 | } 57 | } 58 | }); 59 | } 60 | } 61 | 62 | @Override 63 | protected void onResume() { 64 | super.onResume(); 65 | if (V2EX.getInstance().isLogin()) { 66 | floatingActionButton.setVisibility(View.VISIBLE); 67 | } else { 68 | floatingActionButton.setVisibility(View.INVISIBLE); 69 | } 70 | } 71 | 72 | @Override 73 | public boolean onOptionsItemSelected(final MenuItem item) { 74 | switch (item.getItemId()) { 75 | case android.R.id.home: 76 | finish(); 77 | return true; 78 | } 79 | return super.onOptionsItemSelected(item); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/activity/NodesActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.ActionBar; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.Toolbar; 7 | import android.view.MenuItem; 8 | 9 | import com.zzhoujay.v2ex.R; 10 | import com.zzhoujay.v2ex.ui.fragment.NodesFragment; 11 | 12 | /** 13 | * Created by 州 on 2015/7/20 0020. 14 | * Node列表 15 | */ 16 | public class NodesActivity extends AppCompatActivity { 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_fragment); 22 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 23 | setSupportActionBar(toolbar); 24 | ActionBar actionBar = getSupportActionBar(); 25 | if (actionBar != null) { 26 | actionBar.setDisplayUseLogoEnabled(true); 27 | actionBar.setDisplayShowTitleEnabled(true); 28 | actionBar.setDisplayShowHomeEnabled(true); 29 | actionBar.setDisplayHomeAsUpEnabled(true); 30 | } 31 | setTitle(R.string.all_node_list); 32 | NodesFragment nodesFragment = NodesFragment.newInstance(); 33 | getSupportFragmentManager().beginTransaction().add(R.id.fragment_content, nodesFragment).commit(); 34 | } 35 | 36 | @Override 37 | public boolean onOptionsItemSelected(MenuItem item) { 38 | switch (item.getItemId()) { 39 | case android.R.id.home: 40 | finish(); 41 | return true; 42 | } 43 | return super.onOptionsItemSelected(item); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/activity/TopicDetailActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v7.app.ActionBar; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.MenuItem; 9 | 10 | import com.zzhoujay.v2ex.R; 11 | import com.zzhoujay.v2ex.V2EX; 12 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 13 | import com.zzhoujay.v2ex.model.Replies; 14 | import com.zzhoujay.v2ex.model.Topic; 15 | import com.zzhoujay.v2ex.ui.fragment.ReplyFragment; 16 | import com.zzhoujay.v2ex.ui.fragment.TopicDetailFragment; 17 | 18 | /** 19 | * Created by 州 on 2015/7/20 0020. 20 | * Topic详情 21 | */ 22 | public class TopicDetailActivity extends AppCompatActivity { 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_fragment); 28 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 29 | setSupportActionBar(toolbar); 30 | ActionBar actionBar = getSupportActionBar(); 31 | if (actionBar != null) { 32 | actionBar.setDisplayUseLogoEnabled(true); 33 | actionBar.setDisplayShowTitleEnabled(true); 34 | actionBar.setDisplayShowHomeEnabled(true); 35 | actionBar.setDisplayHomeAsUpEnabled(true); 36 | } 37 | setTitle(R.string.topic_detail); 38 | Intent intent = getIntent(); 39 | if (intent.hasExtra(Topic.TOPIC)) { 40 | Topic topic = intent.getParcelableExtra(Topic.TOPIC); 41 | final TopicDetailFragment topicDetailFragment = TopicDetailFragment.newInstance(topic); 42 | getSupportFragmentManager().beginTransaction().add(R.id.fragment_content, topicDetailFragment).commit(); 43 | // if (V2EX.getInstance().isLogin()) { 44 | // final ReplyFragment replyFragment = ReplyFragment.newInstance(topic); 45 | // replyFragment.setOnReplySuccessListener(new ReplyFragment.OnReplySuccessListener() { 46 | // @Override 47 | // public void replySuccess() { 48 | // topicDetailFragment.refresh(); 49 | // topicDetailFragment.scrollToBottom(); 50 | // } 51 | // }); 52 | // getSupportFragmentManager().beginTransaction().add(R.id.fragment_bottom, replyFragment).commit(); 53 | // 54 | // topicDetailFragment.setOnItemClickCallback(new OnLoadCompleteListener() { 55 | // @Override 56 | // public void loadComplete(Replies replies) { 57 | // String content = "@" + replies.member.username + " "; 58 | // replyFragment.setContent(content); 59 | // replyFragment.setSelection(content.length()); 60 | // } 61 | // }); 62 | // } 63 | 64 | } 65 | } 66 | 67 | @Override 68 | public boolean onOptionsItemSelected(final MenuItem item) { 69 | switch (item.getItemId()) { 70 | case android.R.id.home: 71 | finish(); 72 | return true; 73 | } 74 | return super.onOptionsItemSelected(item); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/adapter/NodesAdapter.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.adapter; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import java.util.List; 10 | 11 | import com.zzhoujay.richtext.RichText; 12 | import com.zzhoujay.v2ex.R; 13 | import com.zzhoujay.v2ex.interfaces.ClickCallback; 14 | import com.zzhoujay.v2ex.interfaces.OnItemClickListener; 15 | import com.zzhoujay.v2ex.model.Node; 16 | import com.zzhoujay.v2ex.util.ContentUtils; 17 | 18 | /** 19 | * Created by 州 on 2015/7/20 0020. 20 | * Node列表的Adapter 21 | */ 22 | public class NodesAdapter extends RecyclerView.Adapter { 23 | 24 | private List nodes; 25 | private ClickCallback clickCallback; 26 | 27 | public NodesAdapter(List nodes) { 28 | this.nodes = nodes; 29 | } 30 | 31 | private OnItemClickListener onItemClickListener = new OnItemClickListener() { 32 | @Override 33 | public void onItemClicked(View view, int position) { 34 | Node node = nodes.get(position); 35 | if (clickCallback != null) { 36 | clickCallback.callback(node); 37 | } 38 | } 39 | }; 40 | 41 | 42 | @Override 43 | public Holder onCreateViewHolder(ViewGroup parent, int viewType) { 44 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_node, null); 45 | Holder holder = new Holder(view); 46 | holder.setOnItemClickListener(onItemClickListener); 47 | return holder; 48 | } 49 | 50 | @Override 51 | public void onBindViewHolder(Holder holder, int position) { 52 | Node node = nodes.get(position); 53 | holder.title.setText(node.title); 54 | RichText.from(ContentUtils.formatContent(node.header)).into(holder.content); 55 | holder.num.setText(node.topics + "个主题"); 56 | } 57 | 58 | @Override 59 | public int getItemCount() { 60 | return nodes == null ? 0 : nodes.size(); 61 | } 62 | 63 | public static class Holder extends RecyclerView.ViewHolder { 64 | 65 | public TextView title, num; 66 | public TextView content; 67 | 68 | private View parent; 69 | private OnItemClickListener onItemClickListener; 70 | 71 | public Holder(View itemView) { 72 | super(itemView); 73 | parent = itemView; 74 | title = (TextView) itemView.findViewById(R.id.item_node_title); 75 | content = (TextView) itemView.findViewById(R.id.item_node_content); 76 | num = (TextView) itemView.findViewById(R.id.item_node_num); 77 | 78 | parent.setOnClickListener(new View.OnClickListener() { 79 | @Override 80 | public void onClick(View v) { 81 | if (onItemClickListener != null) { 82 | onItemClickListener.onItemClicked(parent, getAdapterPosition()); 83 | } 84 | } 85 | }); 86 | } 87 | 88 | public void setOnItemClickListener(OnItemClickListener onItemClickListener) { 89 | this.onItemClickListener = onItemClickListener; 90 | } 91 | } 92 | 93 | public void setNodes(List nodes) { 94 | this.nodes = nodes; 95 | notifyDataSetChanged(); 96 | } 97 | 98 | public void setClickCallback(ClickCallback clickCallback) { 99 | this.clickCallback = clickCallback; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/adapter/RepliesAdapter.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.adapter; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import java.util.List; 11 | 12 | import com.bumptech.glide.Glide; 13 | import com.zzhoujay.richtext.RichText; 14 | import com.zzhoujay.v2ex.R; 15 | import com.zzhoujay.v2ex.interfaces.OnItemClickListener; 16 | import com.zzhoujay.v2ex.model.Replies; 17 | import com.zzhoujay.v2ex.util.ContentUtils; 18 | import com.zzhoujay.v2ex.util.TimeUtils; 19 | 20 | /** 21 | * Created by 州 on 2015/7/20 0020. 22 | * 回复列表Adapter 23 | */ 24 | public class RepliesAdapter extends RecyclerView.Adapter { 25 | 26 | private List replies; 27 | private OnItemClickListener iconClickCallback; 28 | private OnItemClickListener itemClickCallback; 29 | 30 | public RepliesAdapter(List replies) { 31 | this.replies = replies; 32 | } 33 | 34 | 35 | private OnItemClickListener onIconClickListener = new OnItemClickListener() { 36 | @Override 37 | public void onItemClicked(View view, int position) { 38 | if (iconClickCallback != null) { 39 | iconClickCallback.onItemClicked(view, position); 40 | } 41 | } 42 | }; 43 | 44 | private OnItemClickListener onItemClickListener = new OnItemClickListener() { 45 | @Override 46 | public void onItemClicked(View view, int position) { 47 | if (itemClickCallback != null) { 48 | itemClickCallback.onItemClicked(view, position); 49 | } 50 | } 51 | }; 52 | 53 | @Override 54 | public Holder onCreateViewHolder(ViewGroup parent, int viewType) { 55 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_replies, null); 56 | Holder holder = new Holder(view); 57 | holder.setIconClickListener(onIconClickListener); 58 | holder.setOnItemClickListener(onItemClickListener); 59 | return holder; 60 | } 61 | 62 | @Override 63 | public void onBindViewHolder(RecyclerView.ViewHolder h, int position) { 64 | if (h instanceof Holder) { 65 | Holder holder= (Holder) h; 66 | Replies reply = replies.get(position); 67 | holder.user.setText(reply.member.username); 68 | holder.time.setText(TimeUtils.friendlyFormat(reply.created * 1000)); 69 | holder.floor.setText(String.format("%d楼", (position + 1))); 70 | RichText.from(ContentUtils.formatContent(reply.content_rendered)).into(holder.content); 71 | Glide 72 | .with(holder.icon.getContext()) 73 | .load("http:" + reply.member.avatar_normal) 74 | .placeholder(R.drawable.default_image) 75 | .crossFade() 76 | .centerCrop() 77 | .into(holder.icon); 78 | } 79 | } 80 | 81 | @Override 82 | public int getItemCount() { 83 | return replies == null ? 0 : replies.size(); 84 | } 85 | 86 | public static class Holder extends RecyclerView.ViewHolder { 87 | 88 | public ImageView icon; 89 | public TextView user, time, floor; 90 | public TextView content; 91 | 92 | private OnItemClickListener iconClickListener; 93 | private OnItemClickListener onItemClickListener; 94 | 95 | public Holder(final View itemView) { 96 | super(itemView); 97 | icon = (ImageView) itemView.findViewById(R.id.item_replies_icon); 98 | user = (TextView) itemView.findViewById(R.id.item_replies_user); 99 | time = (TextView) itemView.findViewById(R.id.item_replies_time); 100 | floor = (TextView) itemView.findViewById(R.id.item_replies_floor); 101 | content = (TextView) itemView.findViewById(R.id.item_replies_content); 102 | 103 | icon.setOnClickListener(new View.OnClickListener() { 104 | @Override 105 | public void onClick(View v) { 106 | if (iconClickListener != null) { 107 | iconClickListener.onItemClicked(icon, getLayoutPosition()); 108 | getAdapterPosition(); 109 | } 110 | } 111 | }); 112 | 113 | itemView.setOnClickListener(new View.OnClickListener() { 114 | @Override 115 | public void onClick(View v) { 116 | if (onItemClickListener != null) { 117 | onItemClickListener.onItemClicked(itemView, getAdapterPosition()); 118 | } 119 | } 120 | }); 121 | } 122 | 123 | public void setIconClickListener(OnItemClickListener iconClickListener) { 124 | this.iconClickListener = iconClickListener; 125 | } 126 | 127 | public void setOnItemClickListener(OnItemClickListener onItemClickListener) { 128 | this.onItemClickListener = onItemClickListener; 129 | } 130 | } 131 | 132 | public Replies getItem(int position) { 133 | return replies == null ? null : replies.get(position); 134 | } 135 | 136 | public void setReplies(List replies) { 137 | this.replies = replies; 138 | notifyDataSetChanged(); 139 | } 140 | 141 | public void setIconClickCallback(OnItemClickListener iconClickCallback) { 142 | this.iconClickCallback = iconClickCallback; 143 | } 144 | 145 | public void setItemClickCallback(OnItemClickListener itemClickCallback) { 146 | this.itemClickCallback = itemClickCallback; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/adapter/TopicsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.adapter; 2 | 3 | import android.support.annotation.Nullable; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | 12 | import java.util.List; 13 | 14 | import com.bumptech.glide.Glide; 15 | import com.zzhoujay.v2ex.R; 16 | import com.zzhoujay.v2ex.interfaces.ClickCallback; 17 | import com.zzhoujay.v2ex.interfaces.OnItemClickListener; 18 | import com.zzhoujay.v2ex.model.Topic; 19 | import com.zzhoujay.v2ex.util.TimeUtils; 20 | 21 | /** 22 | * Created by 州 on 2015/7/20 0020. 23 | * Topic列表Adapter 24 | */ 25 | public class TopicsAdapter extends RecyclerView.Adapter { 26 | 27 | private List topics; 28 | private ClickCallback clickCallback; 29 | 30 | public TopicsAdapter(@Nullable List topics) { 31 | this.topics = topics; 32 | } 33 | 34 | private OnItemClickListener onItemClickListener = new OnItemClickListener() { 35 | @Override 36 | public void onItemClicked(View view, int position) { 37 | Topic topic = topics.get(position); 38 | if (clickCallback != null) { 39 | clickCallback.callback(topic); 40 | } 41 | } 42 | }; 43 | 44 | @Override 45 | public Holder onCreateViewHolder(ViewGroup parent, int viewType) { 46 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_topic, null); 47 | Holder holder = new Holder(view); 48 | holder.setOnItemClickListener(onItemClickListener); 49 | return holder; 50 | } 51 | 52 | @Override 53 | public void onBindViewHolder(Holder holder, int position) { 54 | Topic topic = topics.get(position); 55 | 56 | holder.title.setText(topic.title); 57 | holder.node.setText(topic.node.title); 58 | holder.user.setText(topic.member.username); 59 | holder.time.setText(TimeUtils.friendlyFormat(topic.created * 1000)); 60 | holder.reply.setText(String.format("%d个回复", topic.replies)); 61 | Glide.with(holder.icon.getContext()) 62 | .load("http:" + topic.member.avatar_normal) 63 | .placeholder(R.drawable.default_image) 64 | .centerCrop() 65 | .crossFade() 66 | .into(holder.icon); 67 | } 68 | 69 | @Override 70 | public int getItemCount() { 71 | return topics == null ? 0 : topics.size(); 72 | } 73 | 74 | public static class Holder extends RecyclerView.ViewHolder { 75 | 76 | public TextView title, node, user, time, reply; 77 | public ImageView icon; 78 | 79 | private View parent; 80 | private OnItemClickListener onItemClickListener; 81 | 82 | public Holder(View itemView) { 83 | super(itemView); 84 | parent = itemView; 85 | title = (TextView) itemView.findViewById(R.id.item_topic_title); 86 | node = (TextView) itemView.findViewById(R.id.item_topic_node); 87 | user = (TextView) itemView.findViewById(R.id.item_topic_user); 88 | time = (TextView) itemView.findViewById(R.id.item_topic_time); 89 | icon = (ImageView) itemView.findViewById(R.id.item_topic_icon); 90 | reply = (TextView) itemView.findViewById(R.id.item_topic_reply); 91 | 92 | itemView.setOnClickListener(new View.OnClickListener() { 93 | @Override 94 | public void onClick(View v) { 95 | if (onItemClickListener != null) { 96 | onItemClickListener.onItemClicked(parent, getAdapterPosition()); 97 | } 98 | } 99 | }); 100 | } 101 | 102 | public void setOnItemClickListener(OnItemClickListener onItemClickListener) { 103 | this.onItemClickListener = onItemClickListener; 104 | } 105 | } 106 | 107 | public void setTopics(List topics) { 108 | this.topics = topics; 109 | notifyDataSetChanged(); 110 | } 111 | 112 | public void setClickCallback(ClickCallback clickCallback) { 113 | this.clickCallback = clickCallback; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/dialog/ContentDialog.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.dialog; 2 | 3 | import android.app.Dialog; 4 | import android.os.Bundle; 5 | import android.support.annotation.NonNull; 6 | import android.support.annotation.Nullable; 7 | import android.support.v4.app.DialogFragment; 8 | import android.support.v7.app.AlertDialog; 9 | 10 | import com.zzhoujay.v2ex.R; 11 | 12 | /** 13 | * Created by zzhoujay on 2015/7/28 0028. 14 | * 显示内容的Dialog 15 | */ 16 | public class ContentDialog extends DialogFragment { 17 | 18 | public static final String CONTENT = "content"; 19 | public static final String TITLE = "title"; 20 | 21 | private CharSequence content; 22 | private String title; 23 | 24 | @Override 25 | public void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | Bundle bundle = getArguments(); 28 | if (bundle.containsKey(CONTENT)) { 29 | content = bundle.getString(CONTENT); 30 | } 31 | if (bundle.containsKey(TITLE)) { 32 | title = bundle.getString(TITLE); 33 | } 34 | } 35 | 36 | @NonNull 37 | @Override 38 | public Dialog onCreateDialog(Bundle savedInstanceState) { 39 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 40 | if (title != null) { 41 | builder.setTitle(title); 42 | } 43 | if (content != null) { 44 | builder.setMessage(content); 45 | } 46 | builder.setPositiveButton(R.string.confirm, null); 47 | return builder.create(); 48 | } 49 | 50 | public static ContentDialog newInstance(@Nullable String title, @Nullable CharSequence content) { 51 | ContentDialog contentDialog = new ContentDialog(); 52 | Bundle bundle = new Bundle(); 53 | bundle.putString(TITLE, title); 54 | bundle.putCharSequence(CONTENT, content); 55 | contentDialog.setArguments(bundle); 56 | return contentDialog; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/fragment/NodesFragment.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.fragment; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.os.Parcelable; 6 | import android.support.annotation.Nullable; 7 | import android.support.v4.app.Fragment; 8 | import android.support.v4.widget.SwipeRefreshLayout; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.support.v7.widget.StaggeredGridLayoutManager; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | 15 | import java.util.List; 16 | 17 | import com.zzhoujay.v2ex.R; 18 | import com.zzhoujay.v2ex.data.DataManger; 19 | import com.zzhoujay.v2ex.data.NodesProvider; 20 | import com.zzhoujay.v2ex.interfaces.ClickCallback; 21 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 22 | import com.zzhoujay.v2ex.model.Node; 23 | import com.zzhoujay.v2ex.ui.activity.NodeActivity; 24 | import com.zzhoujay.v2ex.ui.adapter.NodesAdapter; 25 | 26 | /** 27 | * Created by 州 on 2015/7/20 0020. 28 | * 显示Node列表的Fragment 29 | */ 30 | public class NodesFragment extends Fragment { 31 | 32 | private RecyclerView recyclerView; 33 | private SwipeRefreshLayout swipeRefreshLayout; 34 | private NodesAdapter nodesAdapter; 35 | 36 | @Override 37 | public void onCreate(@Nullable Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | NodesProvider nodesProvider = new NodesProvider(DataManger.getInstance().getRestAdapter()); 40 | DataManger.getInstance().addProvider(NodesProvider.FILE_NAME, nodesProvider); 41 | } 42 | 43 | @Nullable 44 | @Override 45 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 46 | View view = inflater.inflate(R.layout.fragment_recycler_view, container, false); 47 | swipeRefreshLayout = (SwipeRefreshLayout) view; 48 | swipeRefreshLayout.setColorSchemeResources(android.R.color.holo_purple, android.R.color.holo_blue_bright, android.R.color.holo_orange_light, 49 | android.R.color.holo_red_light); 50 | recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView); 51 | StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); 52 | recyclerView.setLayoutManager(staggeredGridLayoutManager); 53 | swipeRefreshLayout.setRefreshing(true); 54 | DataManger.getInstance().getData(NodesProvider.FILE_NAME, new OnLoadCompleteListener>() { 55 | @Override 56 | public void loadComplete(List nodes) { 57 | setUp(nodes); 58 | } 59 | }); 60 | swipeRefreshLayout.setOnRefreshListener(onRefreshListener); 61 | return view; 62 | } 63 | 64 | private SwipeRefreshLayout.OnRefreshListener onRefreshListener = new SwipeRefreshLayout.OnRefreshListener() { 65 | @Override 66 | public void onRefresh() { 67 | DataManger.getInstance().refresh(NodesProvider.FILE_NAME, onLoadComplete); 68 | } 69 | }; 70 | 71 | private OnLoadCompleteListener> onLoadComplete = new OnLoadCompleteListener>() { 72 | @Override 73 | public void loadComplete(List nodes) { 74 | if (nodes != null) { 75 | nodesAdapter.setNodes(nodes); 76 | } 77 | swipeRefreshLayout.setRefreshing(false); 78 | } 79 | }; 80 | 81 | private ClickCallback clickCallback = new ClickCallback() { 82 | @Override 83 | public void callback(Node node) { 84 | Intent intent = new Intent(getActivity(), NodeActivity.class); 85 | intent.putExtra(Node.NODE, (Parcelable) node); 86 | startActivity(intent); 87 | } 88 | }; 89 | 90 | private void setUp(List nodes) { 91 | swipeRefreshLayout.setRefreshing(false); 92 | nodesAdapter = new NodesAdapter(nodes); 93 | nodesAdapter.setClickCallback(clickCallback); 94 | recyclerView.setAdapter(nodesAdapter); 95 | } 96 | 97 | public static NodesFragment newInstance() { 98 | NodesFragment nodesFragment; 99 | nodesFragment = new NodesFragment(); 100 | return nodesFragment; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/fragment/ReplyFragment.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.annotation.Nullable; 5 | import android.support.v4.app.Fragment; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.EditText; 10 | import android.widget.ImageButton; 11 | 12 | import com.zzhoujay.v2ex.R; 13 | import com.zzhoujay.v2ex.V2EX; 14 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 15 | import com.zzhoujay.v2ex.model.Topic; 16 | import com.zzhoujay.v2ex.util.UserUtils; 17 | 18 | /** 19 | * Created by zzhoujay on 2015/7/27 0027. 20 | * 回复栏 21 | */ 22 | public class ReplyFragment extends Fragment { 23 | 24 | private EditText editText; 25 | private Topic topic; 26 | private OnReplySuccessListener onReplySuccessListener; 27 | 28 | @Override 29 | public void onCreate(@Nullable Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | Bundle bundle = getArguments(); 32 | if (bundle != null && bundle.containsKey(Topic.TOPIC)) { 33 | topic = bundle.getParcelable(Topic.TOPIC); 34 | } 35 | } 36 | 37 | @Nullable 38 | @Override 39 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 40 | View view = inflater.inflate(R.layout.fragment_reply, container, false); 41 | editText = (EditText) view.findViewById(R.id.reply_content); 42 | ImageButton imageButton = (ImageButton) view.findViewById(R.id.reply_btn); 43 | imageButton.setOnClickListener(replyListener); 44 | return view; 45 | } 46 | 47 | private View.OnClickListener replyListener = new View.OnClickListener() { 48 | @Override 49 | public void onClick(View v) { 50 | String content = editText.getText().toString(); 51 | if (content.isEmpty()) { 52 | V2EX.getInstance().toast(R.string.empty_reply); 53 | } else { 54 | if (!V2EX.getInstance().isNetworkConnected()) { 55 | V2EX.getInstance().toast(R.string.network_error); 56 | } else { 57 | if (topic != null) { 58 | UserUtils.replyTopic(topic.id, content, new OnLoadCompleteListener() { 59 | @Override 60 | public void loadComplete(Boolean aBoolean) { 61 | if (aBoolean) { 62 | V2EX.getInstance().toast(R.string.reply_success); 63 | editText.setText(""); 64 | if (onReplySuccessListener != null) { 65 | onReplySuccessListener.replySuccess(); 66 | } 67 | } else { 68 | V2EX.getInstance().toast(R.string.reply_error); 69 | } 70 | } 71 | }); 72 | } 73 | } 74 | } 75 | } 76 | }; 77 | 78 | public void setContent(String content) { 79 | editText.setText(content); 80 | } 81 | 82 | public void setSelection(int len) { 83 | editText.setSelection(len); 84 | } 85 | 86 | public void setOnReplySuccessListener(OnReplySuccessListener onReplySuccessListener) { 87 | this.onReplySuccessListener = onReplySuccessListener; 88 | } 89 | 90 | public static ReplyFragment newInstance(@Nullable Topic topic) { 91 | ReplyFragment replyFragment = new ReplyFragment(); 92 | Bundle bundle = new Bundle(); 93 | bundle.putParcelable(Topic.TOPIC, topic); 94 | replyFragment.setArguments(bundle); 95 | return replyFragment; 96 | } 97 | 98 | public interface OnReplySuccessListener { 99 | void replySuccess(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/fragment/TopicsFragment.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.fragment; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.os.Parcelable; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.app.Fragment; 9 | import android.support.v4.widget.SwipeRefreshLayout; 10 | import android.support.v7.widget.LinearLayoutManager; 11 | import android.support.v7.widget.RecyclerView; 12 | import android.util.Log; 13 | import android.view.LayoutInflater; 14 | import android.view.View; 15 | import android.view.ViewGroup; 16 | 17 | import java.util.List; 18 | 19 | import com.zzhoujay.v2ex.R; 20 | import com.zzhoujay.v2ex.data.DataManger; 21 | import com.zzhoujay.v2ex.data.TopicsProvider; 22 | import com.zzhoujay.v2ex.interfaces.ClickCallback; 23 | import com.zzhoujay.v2ex.interfaces.OnLoadCompleteListener; 24 | import com.zzhoujay.v2ex.model.Topic; 25 | import com.zzhoujay.v2ex.ui.activity.TopicDetailActivity; 26 | import com.zzhoujay.v2ex.ui.adapter.TopicsAdapter; 27 | 28 | /** 29 | * Created by 州 on 2015/7/20 0020. 30 | * Topic列表 31 | */ 32 | public class TopicsFragment extends Fragment { 33 | 34 | public static final String TYPE = "type"; 35 | 36 | private RecyclerView recyclerView; 37 | private SwipeRefreshLayout swipeRefreshLayout; 38 | private TopicsProvider.TopicType topicType; 39 | private TopicsAdapter topicsAdapter; 40 | 41 | @Override 42 | public void onCreate(@Nullable Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | Bundle bundle = getArguments(); 45 | if (bundle.containsKey(TYPE)) { 46 | topicType = bundle.getParcelable(TYPE); 47 | if (topicType != null) { 48 | TopicsProvider topicsProvider = new TopicsProvider(DataManger.getInstance().getRestAdapter(), topicType); 49 | DataManger.getInstance().addProvider(topicType.fileName, topicsProvider); 50 | } 51 | } 52 | } 53 | 54 | @Nullable 55 | @Override 56 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 57 | View view = inflater.inflate(R.layout.fragment_recycler_view, container, false); 58 | swipeRefreshLayout = (SwipeRefreshLayout) view; 59 | swipeRefreshLayout.setColorSchemeResources(android.R.color.holo_purple, android.R.color.holo_blue_bright, android.R.color.holo_orange_light, 60 | android.R.color.holo_red_light); 61 | recyclerView = (RecyclerView) view.findViewById(R.id.recyclerView); 62 | LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getActivity()); 63 | recyclerView.setLayoutManager(linearLayoutManager); 64 | setUp(null); 65 | swipeRefreshLayout.setRefreshing(true); 66 | DataManger.getInstance().getData(topicType.fileName, new OnLoadCompleteListener>() { 67 | @Override 68 | public void loadComplete(List topics) { 69 | if (topics != null && topics.size() > 0) { 70 | topicsAdapter.setTopics(topics); 71 | } 72 | swipeRefreshLayout.setRefreshing(false); 73 | } 74 | }); 75 | swipeRefreshLayout.setOnRefreshListener(onRefreshListener); 76 | return view; 77 | } 78 | 79 | private void setUp(List topics) { 80 | swipeRefreshLayout.setRefreshing(false); 81 | topicsAdapter = new TopicsAdapter(topics); 82 | topicsAdapter.setClickCallback(clickCallback); 83 | recyclerView.setAdapter(topicsAdapter); 84 | } 85 | 86 | private SwipeRefreshLayout.OnRefreshListener onRefreshListener = new SwipeRefreshLayout.OnRefreshListener() { 87 | @Override 88 | public void onRefresh() { 89 | DataManger.getInstance().refresh(topicType.fileName, refreshListener); 90 | } 91 | }; 92 | 93 | private OnLoadCompleteListener> refreshListener = new OnLoadCompleteListener>() { 94 | @Override 95 | public void loadComplete(List topics) { 96 | if (topics != null && topics.size() > 0) { 97 | topicsAdapter.setTopics(topics); 98 | } 99 | swipeRefreshLayout.setRefreshing(false); 100 | } 101 | }; 102 | 103 | private ClickCallback clickCallback = new ClickCallback() { 104 | @Override 105 | public void callback(Topic topic) { 106 | Intent intent = new Intent(getActivity(), TopicDetailActivity.class); 107 | intent.putExtra(Topic.TOPIC, (Parcelable) topic); 108 | startActivity(intent); 109 | } 110 | }; 111 | 112 | public void setSwipeRefreshEnable(boolean enable) { 113 | try { 114 | swipeRefreshLayout.setEnabled(enable); 115 | } catch (Exception e) { 116 | Log.d("setSwipeRefreshEnable", "error", e); 117 | } 118 | } 119 | 120 | 121 | @Override 122 | public void onDestroy() { 123 | super.onDestroy(); 124 | recyclerView.setAdapter(null); 125 | recyclerView = null; 126 | if (!topicType.fileName.equals(TopicsProvider.TopicType.FILE_NAME_HOT) && !topicType.fileName.equals(TopicsProvider.TopicType.FILE_NAME_LATEST)) { 127 | DataManger.getInstance().removeProvider(topicType.fileName); 128 | } 129 | } 130 | 131 | public static TopicsFragment newInstance(@NonNull TopicsProvider.TopicType topicType) { 132 | TopicsFragment topicsFragment = new TopicsFragment(); 133 | Bundle bundle = new Bundle(); 134 | bundle.putParcelable(TYPE, topicType); 135 | topicsFragment.setArguments(bundle); 136 | return topicsFragment; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/ui/view/SwipeToRefreshLayout.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.ui.view; 2 | 3 | import android.content.Context; 4 | import android.support.v4.widget.SwipeRefreshLayout; 5 | import android.util.AttributeSet; 6 | 7 | /** 8 | * Created by zzhoujay on 2015/8/10 0010. 9 | * 继承至SwipeRefreshLayout,修复了setRefreshing(true)时不显示的BUG 10 | */ 11 | public class SwipeToRefreshLayout extends SwipeRefreshLayout { 12 | 13 | public SwipeToRefreshLayout(Context context) { 14 | super(context); 15 | } 16 | 17 | public SwipeToRefreshLayout(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | private boolean mMeasured = false; 22 | private boolean mPreMeasureRefreshing = false; 23 | 24 | @Override 25 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 26 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 27 | if (!mMeasured) { 28 | mMeasured = true; 29 | setRefreshing(mPreMeasureRefreshing); 30 | } 31 | } 32 | 33 | 34 | @Override 35 | public void setRefreshing(boolean refreshing) { 36 | if (mMeasured) { 37 | super.setRefreshing(refreshing); 38 | } else { 39 | mPreMeasureRefreshing = refreshing; 40 | } 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/util/ContentUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.util; 2 | 3 | /** 4 | * Created by zzhoujay on 2015/7/23 0023. 5 | * ContentUtil 6 | */ 7 | public class ContentUtils { 8 | public static String formatContent(String content) { 9 | if (content == null) { 10 | return ""; 11 | } 12 | return content.replace("href=\"/member/", "href=\"http://www.v2ex.com/member/") 13 | .replace("href=\"/i/", "href=\"https://i.v2ex.co/") 14 | .replace("href=\"/t/", "href=\"http://www.v2ex.com/t/") 15 | .replace("href=\"/go/", "href=\"http://www.v2ex.com/go/"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/util/FileComparator.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.util; 2 | 3 | import java.io.File; 4 | import java.util.Comparator; 5 | 6 | /** 7 | * Created by zzhoujay on 2015/7/22 0022. 8 | * 文件比较器 9 | */ 10 | public class FileComparator implements Comparator { 11 | @Override 12 | public int compare(File lhs, File rhs) { 13 | return (int) (rhs.lastModified() - lhs.lastModified()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/util/FileNameFilter.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.util; 2 | 3 | import java.io.File; 4 | import java.io.FilenameFilter; 5 | 6 | /** 7 | * Created by zzhoujay on 2015/7/22 0022. 8 | * 文件过滤 9 | */ 10 | public class FileNameFilter implements FilenameFilter { 11 | 12 | private String start; 13 | 14 | public FileNameFilter(String start) { 15 | this.start = start; 16 | } 17 | 18 | @Override 19 | public boolean accept(File dir, String filename) { 20 | return filename != null && filename.startsWith(start); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.util; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.File; 6 | import java.io.FileInputStream; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.ObjectInputStream; 10 | import java.io.ObjectOutputStream; 11 | import java.text.DecimalFormat; 12 | import java.util.Arrays; 13 | 14 | /** 15 | * Created by 州 on 2015/7/4 0004. 16 | * 文件操作相关工具类 17 | */ 18 | public class FileUtils { 19 | 20 | public static DecimalFormat decimalFormat = new DecimalFormat(".00"); 21 | 22 | /** 23 | * 判断文件是否存在 24 | * 25 | * @param path 文件的全路径 26 | * @return 文件是否存在 27 | */ 28 | @SuppressWarnings("unused") 29 | public static boolean isFileExists(String path) { 30 | File file = new File(path); 31 | return file.exists(); 32 | } 33 | 34 | @SuppressWarnings("unused") 35 | public static boolean isFileExists(File parent, String name) { 36 | File file = new File(parent, name); 37 | return file.exists(); 38 | } 39 | 40 | /** 41 | * 获取文件的后缀名 42 | * 43 | * @param path 文件全路径 44 | * @return 后缀名 45 | */ 46 | @SuppressWarnings("unused") 47 | public static String getFileExtension(String path) { 48 | return path == null ? null : path.substring(path.lastIndexOf(".") + 1); 49 | } 50 | 51 | /** 52 | * 去掉路径的后缀名 53 | * 54 | * @param path 文件全路径 55 | * @return 去后缀名后的文件名 56 | */ 57 | @SuppressWarnings("unused") 58 | public static String getPathWithoutExtension(String path) { 59 | return path == null ? null : path.substring(0, path.lastIndexOf(".")); 60 | } 61 | 62 | /** 63 | * 将对象写入指定路径的文件中 64 | * 65 | * @param path 文件路径 66 | * @param obj 需要被写入的对象 67 | */ 68 | @SuppressWarnings("unused") 69 | public static void writeObject(String path, Object obj) { 70 | File file = new File(path); 71 | writeObject(file, obj); 72 | } 73 | 74 | /** 75 | * 写入对象到文件中 76 | * 77 | * @param file 文件对象 78 | * @param obj 需要写入文件的对象 79 | */ 80 | @SuppressWarnings("unused") 81 | public static void writeObject(File file, Object obj) { 82 | if (null == file || obj == null) { 83 | return; 84 | } 85 | FileOutputStream fileOutputStream = null; 86 | ObjectOutputStream objectOutputStream = null; 87 | 88 | try { 89 | fileOutputStream = new FileOutputStream(file); 90 | objectOutputStream = new ObjectOutputStream(fileOutputStream); 91 | 92 | objectOutputStream.writeObject(obj); 93 | objectOutputStream.flush(); 94 | } catch (IOException e) { 95 | Log.d("writeObject", e.getMessage()); 96 | } finally { 97 | try { 98 | if (objectOutputStream != null) { 99 | objectOutputStream.close(); 100 | } 101 | if (fileOutputStream != null) { 102 | fileOutputStream.close(); 103 | } 104 | } catch (IOException e) { 105 | Log.d("writeObject", e.getMessage()); 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * 从指定路径的文件中读取对象 112 | * 113 | * @param path 文件路径 114 | * @return 读取到的对象 115 | */ 116 | @SuppressWarnings("unused") 117 | public static Object readObject(String path) { 118 | File file = new File(path); 119 | return readObject(file); 120 | } 121 | 122 | /** 123 | * 从文件中读取对象 124 | * 125 | * @param file 文件对象 126 | * @return 读取到的对象 127 | */ 128 | public static Object readObject(File file) { 129 | Object obj = null; 130 | if (file != null && file.exists()) { 131 | FileInputStream fileInputStream = null; 132 | ObjectInputStream objectInputStream = null; 133 | 134 | try { 135 | fileInputStream = new FileInputStream(file); 136 | objectInputStream = new ObjectInputStream(fileInputStream); 137 | 138 | obj = objectInputStream.readObject(); 139 | } catch (IOException | ClassNotFoundException e) { 140 | Log.d("readObject", e.getMessage()); 141 | } finally { 142 | try { 143 | if (objectInputStream != null) { 144 | objectInputStream.close(); 145 | } 146 | if (fileInputStream != null) { 147 | fileInputStream.close(); 148 | } 149 | } catch (IOException e) { 150 | Log.d("readObject", e.getMessage()); 151 | } 152 | 153 | } 154 | } 155 | return obj; 156 | } 157 | 158 | /** 159 | * 格式化文件大小以字符串输出 160 | * 161 | * @param size 大小 162 | * @return B、KB、MB类型的字符串 163 | */ 164 | @SuppressWarnings("unused") 165 | public static String formatSize(int size) { 166 | if (size < 1024 * 0.6) { 167 | return size + "B"; 168 | } else if (size < 1024 * 1024 * 0.6) { 169 | return decimalFormat.format((float) size / 1024) + "KB"; 170 | } else { 171 | return decimalFormat.format((float) size / (1024 * 1024)) + "MB"; 172 | } 173 | } 174 | 175 | @SuppressWarnings("unused") 176 | public static boolean createFolder(File file, String name) { 177 | if (file.exists()) { 178 | File f = new File(file, name); 179 | if (!f.exists()) { 180 | return f.mkdirs(); 181 | } 182 | } 183 | return false; 184 | } 185 | 186 | @SuppressWarnings("unused") 187 | public static void deleteSomeCache(File parent, String start, int maxSize) { 188 | if(parent!=null){ 189 | File[] files = parent.listFiles(new FileNameFilter(start)); 190 | if (files != null && files.length > maxSize) { 191 | Arrays.sort(files, new FileComparator()); 192 | for (int len = files.length, i = maxSize; i < len; i++) { 193 | files[i].delete(); 194 | } 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /app/src/main/java/com/zzhoujay/v2ex/util/TimeUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzhoujay.v2ex.util; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Calendar; 5 | import java.util.Date; 6 | 7 | /** 8 | * Created by 州 on 2015/7/20 0020. 9 | * TimeUtils 10 | */ 11 | public class TimeUtils { 12 | 13 | /** 14 | * 友好的方式显示时间 15 | */ 16 | public static String friendlyFormat(long time) { 17 | Date date = new Date(time); 18 | 19 | Calendar now = getCal(); 20 | String t = new SimpleDateFormat("HH:mm").format(date); 21 | 22 | // 第一种情况,日期在同一天 23 | String curDate = dateFormat.get().format(now.getTime()); 24 | String paramDate = dateFormat.get().format(date); 25 | if (curDate.equals(paramDate)) { 26 | int hour = (int) ((now.getTimeInMillis() - date.getTime()) / 3600000); 27 | if (hour > 0) 28 | return t; 29 | int minute = (int) ((now.getTimeInMillis() - date.getTime()) / 60000); 30 | if (minute < 2) 31 | return "刚刚"; 32 | if (minute > 30) 33 | return "半个小时以前"; 34 | return minute + "分钟前"; 35 | } 36 | 37 | // 第二种情况,不在同一天 38 | int days = (int) ((getBegin(getDate()).getTime() - getBegin(date).getTime()) / 86400000); 39 | if (days == 1) 40 | return "昨天 " + t; 41 | if (days == 2) 42 | return "前天 " + t; 43 | if (days <= 7) 44 | return days + "天前"; 45 | return dateToStr(date); 46 | } 47 | 48 | /** 49 | * 返回日期的0点:2012-07-07 20:20:20 --> 2012-07-07 00:00:00 50 | */ 51 | public static Date getBegin(Date date) { 52 | return strToTime(dateToStr(date) + " 00:00:00"); 53 | } 54 | 55 | /** 56 | * 日期格式 57 | */ 58 | private final static ThreadLocal dateFormat = new ThreadLocal() { 59 | protected SimpleDateFormat initialValue() { 60 | return new SimpleDateFormat("yyyy-MM-dd"); 61 | } 62 | }; 63 | 64 | /** 65 | * 时间格式 66 | */ 67 | private final static ThreadLocal timeFormat = new ThreadLocal() { 68 | protected SimpleDateFormat initialValue() { 69 | return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 70 | } 71 | }; 72 | 73 | /** 74 | * 获取当前时间:Date 75 | */ 76 | public static Date getDate() { 77 | return new Date(); 78 | } 79 | 80 | /** 81 | * 获取当前时间:Calendar 82 | */ 83 | public static Calendar getCal() { 84 | return Calendar.getInstance(); 85 | } 86 | 87 | /** 88 | * 日期转换为字符串:yyyy-MM-dd 89 | */ 90 | public static String dateToStr(Date date) { 91 | if (date != null) 92 | return dateFormat.get().format(date); 93 | return null; 94 | } 95 | 96 | /** 97 | * 字符串转换为时间:yyyy-MM-dd HH:mm:ss 98 | */ 99 | public static Date strToTime(String str) { 100 | Date date = null; 101 | try { 102 | date = timeFormat.get().parse(str); 103 | } catch (Exception e) { 104 | e.printStackTrace(); 105 | } 106 | return date; 107 | } 108 | 109 | /** 110 | * 字符串转换为日期:yyyy-MM-dd 111 | */ 112 | @SuppressWarnings("unused") 113 | public static Date strToDate(String str) { 114 | Date date = null; 115 | try { 116 | date = dateFormat.get().parse(str); 117 | } catch (Exception e) { 118 | e.printStackTrace(); 119 | } 120 | return date; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_add_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_add_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_dashboard_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_dashboard_grey.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_done_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_done_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_drawer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_exit_to_app_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_exit_to_app_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_info_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_info_grey.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_refresh_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_refresh_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_send_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_send_grey.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_settings_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_settings_grey.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_view_agenda_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-hdpi/ic_view_agenda_grey.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-mdpi/ic_drawer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_add_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-xhdpi/ic_add_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_done_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-xhdpi/ic_done_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-xhdpi/ic_drawer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_exit_to_app_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-xhdpi/ic_exit_to_app_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_refresh_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-xhdpi/ic_refresh_white.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_send_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-xhdpi/ic_send_grey.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_drawer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zzhoujay/V2EX/be1ee2a9f73bcd085b51e6f448cfde59b6a8dd97/app/src/main/res/drawable-xxhdpi/ic_drawer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/default_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/image_btn_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 25 | 26 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_fragment_floatactionbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 21 | 22 | 28 | 29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_login.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | 24 | 25 | 35 | 36 | 46 | 47 | 54 | 55 | 60 | 61 | 62 | 70 | 71 | 77 | 78 | 79 |