Diycode开源项目 MainActivity分析
1.分析MainActivity整体结构
1.1.首先看一下这个界面的整体效果。
1.2.活动源代码如下
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-03-08 01:01:18 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.activity; import android.support.design.widget.NavigationView; import android.support.design.widget.TabLayout; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewPager; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.widget.Toolbar; import android.view.GestureDetector; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.Glide; import com.gcssloop.diycode.R; import com.gcssloop.diycode.base.app.BaseActivity; import com.gcssloop.diycode.base.app.ViewHolder; import com.gcssloop.diycode.fragment.NewsListFragment; import com.gcssloop.diycode.fragment.SitesListFragment; import com.gcssloop.diycode.fragment.TopicListFragment; import com.gcssloop.diycode.test.TextFragment; import com.gcssloop.diycode.utils.Config; import com.gcssloop.diycode.utils.DataCache; import com.gcssloop.diycode_sdk.api.login.event.LogoutEvent; import com.gcssloop.diycode_sdk.api.user.bean.User; import com.gcssloop.diycode_sdk.api.user.bean.UserDetail; import com.gcssloop.diycode_sdk.api.user.event.GetMeEvent; import com.gcssloop.diycode_sdk.log.Logger; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; public class MainActivity extends BaseActivity implements NavigationView.OnNavigationItemSelectedListener, View.OnClickListener { private DataCache mCache; private Config mConfig; private int mCurrentPosition = 0; private TopicListFragment mFragment1; private NewsListFragment mFragment2; private SitesListFragment mFragment3; private boolean isToolbarFirstClick = true; @Override public int getLayoutId() { return R.layout.activity_main; } @Override public void initViews(ViewHolder holder, View root) { EventBus.getDefault().register(this); mCache = new DataCache(this); mConfig = Config.getSingleInstance(); initMenu(holder); initViewPager(holder); } //--- viewpager adapter ------------------------------------------------------------------------ private void initViewPager(ViewHolder holder) { ViewPager mViewPager = holder.get(R.id.view_pager); TabLayout mTabLayout = holder.get(R.id.tab_layout); mViewPager.setOffscreenPageLimit(3); // 防止滑动到第三个页面时,第一个页面被销毁 mFragment1 = TopicListFragment.newInstance(); mFragment2 = NewsListFragment.newInstance(); mFragment3 = SitesListFragment.newInstance(); mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) { String[] types = {"Topics", "News", "Sites", "Test"}; @Override public Fragment getItem(int position) { if (position == 0) return mFragment1; if (position == 1) return mFragment2; if (position == 2) return mFragment3; return TextFragment.newInstance(types[position]); } @Override public int getCount() { return 3; } @Override public CharSequence getPageTitle(int position) { return types[position]; } }); mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { mCurrentPosition = position; } @Override public void onPageScrollStateChanged(int state) { } }); mCurrentPosition = mConfig.getMainViewPagerPosition(); mViewPager.setCurrentItem(mCurrentPosition); mTabLayout.setupWithViewPager(mViewPager); } // 快速返回顶部 private void quickToTop() { switch (mCurrentPosition) { case 0: mFragment1.quickToTop(); break; case 1: mFragment2.quickToTop(); break; case 2: mFragment3.quickToTop(); break; } } // 如果收到此状态说明用户已经登录成功了 @Subscribe(threadMode = ThreadMode.MAIN) public void onLogin(GetMeEvent event) { if (event.isOk()) { UserDetail me = event.getBean(); mCache.saveMe(me); loadMenuData(); // 加载菜单数据 } } // 如果收到此状态说明用户登出了 @Subscribe(threadMode = ThreadMode.MAIN) public void onLogout(LogoutEvent event) { loadMenuData(); // 加载菜单数据 } //--- menu ------------------------------------------------------------------------------------- // 初始化菜单(包括侧边栏菜单和顶部菜单选项) private void initMenu(ViewHolder holder) { Toolbar toolbar = holder.get(R.id.toolbar); toolbar.setLogo(R.mipmap.logo_actionbar); toolbar.setTitle(""); DrawerLayout drawer = holder.get(R.id.drawer_layout); setSupportActionBar(toolbar); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); drawer.setDrawerListener(toggle); toggle.syncState(); NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); // 双击 666 final GestureDetector detector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDoubleTap(MotionEvent e) { quickToTop(); // 快速返回头部 return super.onDoubleTap(e); } }); toolbar.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { detector.onTouchEvent(event); return false; } }); toolbar.setOnClickListener(this); holder.setOnClickListener(this, R.id.fab); loadMenuData(); } // 加载侧边栏菜单数据(与用户相关的) private void loadMenuData() { NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); View headerView = navigationView.getHeaderView(0); ImageView avatar = (ImageView) headerView.findViewById(R.id.nav_header_image); TextView username = (TextView) headerView.findViewById(R.id.nav_header_name); TextView tagline = (TextView) headerView.findViewById(R.id.nav_header_tagline); if (mDiycode.isLogin()) { UserDetail me = mCache.getMe(); if (me == null) { Logger.e("获取自己缓存失效"); mDiycode.getMe(); // 重新加载 return; } username.setText(me.getLogin()); tagline.setText(me.getTagline()); Glide.with(this).load(me.getAvatar_url()).into(avatar); avatar.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { UserDetail me = mCache.getMe(); if (me == null) { try { me = mDiycode.getMeNow(); } catch (Exception e) { e.printStackTrace(); } } if (me != null) { User user = new User(); user.setId(me.getId()); user.setName(me.getName()); user.setLogin(me.getLogin()); user.setAvatar_url(me.getAvatar_url()); UserActivity.newInstance(MainActivity.this, user); } } }); } else { mCache.removeMe(); username.setText("(未登录)"); tagline.setText("点击头像登录"); avatar.setImageResource(R.mipmap.ic_launcher); avatar.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { openActivity(LoginActivity.class); } }); } } @Override public void onBackPressed() { DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_settings) { openActivity(SettingActivity.class); return true; } else if (id == R.id.action_notification) { if (!mDiycode.isLogin()) { openActivity(LoginActivity.class); } else { openActivity(NotificationActivity.class); } return true; } return super.onOptionsItemSelected(item); } @Override public boolean onNavigationItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.nav_post) { if (!mDiycode.isLogin()) { openActivity(LoginActivity.class); return true; } MyTopicActivity.newInstance(this, MyTopicActivity.InfoType.MY_TOPIC); } else if (id == R.id.nav_collect) { if (!mDiycode.isLogin()) { openActivity(LoginActivity.class); return true; } MyTopicActivity.newInstance(this, MyTopicActivity.InfoType.MY_COLLECT); } else if (id == R.id.nav_about) { openActivity(AboutActivity.class); } else if (id == R.id.nav_setting) { openActivity(SettingActivity.class); } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); return true; } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); mConfig.saveMainViewPagerPosition(mCurrentPosition); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.toolbar: if (isToolbarFirstClick) { toastShort("双击标题栏快速返回顶部"); isToolbarFirstClick = false; } break; case R.id.fab: quickToTop(); break; } } }
1.3.布局源代码
<?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright 2017 GcsSloop ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. ~ You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, software ~ distributed under the License is distributed on an "AS IS" BASIS, ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ~ See the License for the specific language governing permissions and ~ limitations under the License. ~ ~ Last modified 2017-03-08 01:01:18 ~ ~ GitHub: https://github.com/GcsSloop ~ Website: http://www.gcssloop.com ~ Weibo: http://weibo.com/GcsSloop --> <android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:openDrawer="start"> <include layout="@layout/app_bar_main" android:layout_width="match_parent" android:layout_height="match_parent"/> <android.support.design.widget.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" android:fitsSystemWindows="true" app:headerLayout="@layout/nav_header_main" app:menu="@menu/activity_main_drawer"/> </android.support.v4.widget.DrawerLayout>
1.4.对应关系
- MainActivity的总体布局==>activity_main.xml
- 主活动布局中的include布局==>app_bar_main.xml
- content_main.xml
2.Android布局的参考项
2.1.首先是DrawrLayout。
它包括了一个侧滑的效果,当然这个具体侧滑菜单还要自己定义。
参考文章:android官方侧滑菜单DrawerLayout详解。
2.2.然后是具体的NavigationView。侧滑菜单具体定义。
参考文章:Android5.0之NavigationView的使用。
2.3.然后是主页面的一个CoordinatorLayout。
作用:实现浮动按钮的滚动效果。
2.4.然后是AppBarLayout。
作用就是一些动画效果吧。上滑隐藏了标题栏。
就像下图这样:
2.5.然后是TabLayout。
作用:就是添加一个导航栏,切换后ViewPager也随之切换。
类似于这样的效果:
2.6.然后是浮动按钮==>FloatingActionButton。
这种东西:
参考文章:FloatingActionButton完全解析。
3.MainActivity定义的变量
3.1.首先预览一下所有变量。
可以看到,MainActivity继承了BaseActivity。为了少写一些每个页面都会有的标题栏,获取布局id等。
然后实现了NavigationView的监听器==>上面的导航菜单,左右滑动可以切换页面。
3.2.private DataCache mCache.
DataCache类定义在通用包中,数据缓存工具。
3.2.1.参考一下源代码。
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-03-12 02:50:16 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.utils; import android.content.Context; import android.support.annotation.NonNull; import android.util.LruCache; import com.gcssloop.diycode_sdk.api.news.bean.New; import com.gcssloop.diycode_sdk.api.sites.bean.Sites; import com.gcssloop.diycode_sdk.api.topic.bean.TopicContent; import com.gcssloop.diycode_sdk.api.topic.bean.TopicReply; import com.gcssloop.diycode_sdk.api.user.bean.UserDetail; import com.gcssloop.diycode_sdk.utils.ACache; import java.io.File; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * 数据缓存工具 */ public class DataCache { private static int M = 1024 * 1024; ACache mDiskCache; LruCache<String, Object> mLruCache; public DataCache(Context context) { mDiskCache = ACache.get(new File(FileUtil.getExternalCacheDir(context.getApplicationContext(), "diy-data"))); mLruCache = new LruCache<>(5 * M); } public <T extends Serializable> void saveListData(String key, List<T> data) { ArrayList<T> datas = (ArrayList<T>) data; mLruCache.put(key, datas); mDiskCache.put(key, datas, ACache.TIME_WEEK); // 数据缓存时间为 1 周 } public <T extends Serializable> void saveData(@NonNull String key, @NonNull T data) { mLruCache.put(key, data); mDiskCache.put(key, data, ACache.TIME_WEEK); // 数据缓存时间为 1 周 } public <T extends Serializable> T getData(@NonNull String key) { T result = (T) mLruCache.get(key); if (result == null) { result = (T) mDiskCache.getAsObject(key); if (result != null) { mLruCache.put(key, result); } } return result; } public void removeDate(String key) { mDiskCache.remove(key); } public void saveTopicContent(TopicContent content) { saveData("topic_content_" + content.getId(), content); String preview = HtmlUtil.Html2Text(content.getBody_html()); if (preview.length() > 100) { preview = preview.substring(0, 100); } saveData("topic_content_preview" + content.getId(), preview); } public TopicContent getTopicContent(int id) { return getData("topic_content_" + id); } public String getTopicPreview(int id) { String key = "topic_content_preview" + id; return getData(key); } public void saveTopicRepliesList(int topic_id, List<TopicReply> replyList) { ArrayList<TopicReply> replies = new ArrayList<>(replyList); saveData("topic_reply_" + topic_id, replies); } public List<TopicReply> getTopicRepliesList(int topic_id) { return getData("topic_reply_" + topic_id); } public void saveTopicsListObj(List<Object> topicList) { ArrayList<Object> topics = new ArrayList<>(topicList); saveData("topic_list_obj_", topics); } public List<Object> getTopicsListObj() { return getData("topic_list_obj_"); } public void saveNewsList(List<New> newList) { ArrayList<New> news = new ArrayList<>(newList); saveData("news_list_", news); } public List<New> getNewsList() { return getData("news_list_"); } public void saveNewsListObj(List<Object> newList) { ArrayList<Object> news = new ArrayList<>(newList); saveData("news_list_obj_", news); } public List<Object> getNewsListObj() { return getData("news_list_obj_"); } public void saveMe(UserDetail user) { saveData("Gcs_Me_", user); } public UserDetail getMe() { return getData("Gcs_Me_"); } public void removeMe() { removeDate("Gcs_Me_"); } public void saveSites(List<Sites> sitesList) { saveListData("sites_", sitesList); } public List<Sites> getSites() { return getData("sites_"); } public <T extends Serializable> void saveSitesItems(List<T> sitesList) { saveListData("sites_item_", sitesList); } public <T extends Serializable> ArrayList<T> getSitesItems() { return getData("sites_item_"); } }
3.2.2.看一下DataCache中定义的变量。
这里定义的M是一兆缓存单位的意思。
Acache是定义在sdk中处理缓存的类。
LruCache<String,Object>是Android提供的缓存工具类。
3.2.3.看一下DataCache构造函数。
因为这里又用了一个通用类FileUtil。限于文章篇幅,这里就不写了,今后也会用到。
3.2.4.将数据缓存一周(保留List或者T),得到数据,移除数据。
3.2.5.保存话题内容,保存话题回复。保存话题列表。
3.2.6.保存News列表,保存我的页面数据,保存网站数据。
3.3.private Config mConfig;
作用:用户设置。
3.3.1.源代码如下:
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-03-28 04:48:02 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.utils; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.LruCache; import com.gcssloop.diycode_sdk.utils.ACache; import java.io.Serializable; /** * 用户设置 */ public class Config { private static int M = 1024 * 1024; private volatile static Config mConfig; private static LruCache<String, Object> mLruCache = new LruCache<>(1 * M); private static ACache mDiskCache; private Config(Context context) { mDiskCache = ACache.get(context, "config"); } public static Config init(Context context) { if (null == mConfig) { synchronized (Config.class) { if (null == mConfig) { mConfig = new Config(context); } } } return mConfig; } public static Config getSingleInstance() { return mConfig; } //--- 基础 ----------------------------------------------------------------------------------- public <T extends Serializable> void saveData(@NonNull String key, @NonNull T value) { mLruCache.put(key, value); mDiskCache.put(key, value); } public <T extends Serializable> T getData(@NonNull String key, @Nullable T defaultValue) { T result = (T) mLruCache.get(key); if (result != null) { return result; } result = (T) mDiskCache.getAsObject(key); if (result != null) { mLruCache.put(key, result); return result; } return defaultValue; } //--- 浏览器 --------------------------------------------------------------------------------- private static String Key_Browser = "UseInsideBrowser_"; public void setUesInsideBrowser(@NonNull Boolean bool) { saveData(Key_Browser, bool); } public Boolean isUseInsideBrowser() { return getData(Key_Browser, Boolean.TRUE); } //--- 首页状态 ------------------------------------------------------------------------------- private String Key_MainViewPager_Position = "Key_MainViewPager_Position"; public void saveMainViewPagerPosition(Integer position) { mLruCache.put(Key_MainViewPager_Position, position); } public Integer getMainViewPagerPosition() { return getData(Key_MainViewPager_Position, 0); } //--- Topic状态 ------------------------------------------------------------------------------ private String Key_TopicList_LastPosition = "Key_TopicList_LastPosition"; private String Key_TopicList_LastOffset = "Key_TopicList_LastOffset"; public void saveTopicListState(Integer lastPosition, Integer lastOffset) { saveData(Key_TopicList_LastPosition, lastPosition); saveData(Key_TopicList_LastOffset, lastOffset); } public Integer getTopicListLastPosition() { return getData(Key_TopicList_LastPosition, 0); } public Integer getTopicListLastOffset() { return getData(Key_TopicList_LastOffset, 0); } private String Key_TopicList_PageIndex = "Key_TopicList_PageIndex"; public void saveTopicListPageIndex(Integer pageIndex) { saveData(Key_TopicList_PageIndex, pageIndex); } public Integer getTopicListPageIndex() { return getData(Key_TopicList_PageIndex, 0); } //--- News状态 ------------------------------------------------------------------------------ private String Key_NewsList_LastScroll = "Key_NewsList_LastScroll"; public void saveNewsListScroll(Integer lastScrollY) { saveData(Key_NewsList_LastScroll, lastScrollY); } public Integer getNewsLastScroll() { return getData(Key_NewsList_LastScroll, 0); } private String Key_NewsList_LastPosition = "Key_NewsList_LastPosition"; public void saveNewsListPosition(Integer lastPosition) { saveData(Key_NewsList_LastPosition, lastPosition); } public Integer getNewsListLastPosition() { return getData(Key_NewsList_LastPosition, 0); } private String Key_NewsList_PageIndex = "Key_NewsList_PageIndex"; public void saveNewsListPageIndex(Integer pageIndex) { saveData(Key_NewsList_PageIndex, pageIndex); } public Integer getNewsListPageIndex() { return getData(Key_NewsList_PageIndex, 0); } }
3.3.2.预览一下变量。
首先是M单位定义的大小。
然后是用了一个关键字:volatile。
3.3.3.构造函数。
从Acache中获得API中处理缓存的类。
3.3.4.初始化获得静态配置Config。
3.3.5.获得单例。
3.3.6.Config中配置保存数据和获得数据。
3.3.7.Config中配置浏览器
3.3.8.Config中配置首页状态。
默认从第0页开始。
3.3.9.Config中配置Topic状态。
3.3.10.Config中配置News状态。
3.4.private int mCurrentPosition=0;
约定第一个viewPager是从话题开始。
3.5.private TopicListFragment mFragment1;
话题页面碎片。
源代码如下:
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-04-08 23:15:33 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.fragment; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.View; import com.gcssloop.diycode.fragment.base.SimpleRefreshRecyclerFragment; import com.gcssloop.diycode.fragment.provider.TopicProvider; import com.gcssloop.diycode_sdk.api.topic.bean.Topic; import com.gcssloop.diycode_sdk.api.topic.event.GetTopicsListEvent; import com.gcssloop.diycode_sdk.log.Logger; import com.gcssloop.recyclerview.adapter.multitype.HeaderFooterAdapter; import java.util.List; /** * 首页 topic 列表 */ public class TopicListFragment extends SimpleRefreshRecyclerFragment<Topic, GetTopicsListEvent> { private boolean isFirstLaunch = true; public static TopicListFragment newInstance() { Bundle args = new Bundle(); TopicListFragment fragment = new TopicListFragment(); fragment.setArguments(args); return fragment; } @Override public void initData(HeaderFooterAdapter adapter) { // 优先从缓存中获取数据,如果是第一次加载则恢复滚动位置,如果没有缓存则从网络加载 List<Object> topics = mDataCache.getTopicsListObj(); if (null != topics && topics.size() > 0) { Logger.e("topics : " + topics.size()); pageIndex = mConfig.getTopicListPageIndex(); adapter.addDatas(topics); if (isFirstLaunch) { int lastPosition = mConfig.getTopicListLastPosition(); mRecyclerView.getLayoutManager().scrollToPosition(lastPosition); isFirstAddFooter = false; isFirstLaunch = false; } } else { loadMore(); } } @Override protected void setAdapterRegister(Context context, RecyclerView recyclerView, HeaderFooterAdapter adapter) { adapter.register(Topic.class, new TopicProvider(getContext())); } @NonNull @Override protected String request(int offset, int limit) { return mDiycode.getTopicsList(null, null, offset, limit); } @Override protected void onRefresh(GetTopicsListEvent event, HeaderFooterAdapter adapter) { super.onRefresh(event, adapter); mDataCache.saveTopicsListObj(adapter.getDatas()); } @Override protected void onLoadMore(GetTopicsListEvent event, HeaderFooterAdapter adapter) { // TODO 排除重复数据 super.onLoadMore(event, adapter); mDataCache.saveTopicsListObj(adapter.getDatas()); } @Override public void onDestroyView() { super.onDestroyView(); // 存储 PageIndex mConfig.saveTopicListPageIndex(pageIndex); // 存储 RecyclerView 滚动位置 View view = mRecyclerView.getLayoutManager().getChildAt(0); int lastPosition = mRecyclerView.getLayoutManager().getPosition(view); mConfig.saveTopicListState(lastPosition, 0); } }
3.6.private NewsListFragment mFragment2;
新闻页面碎片。
源代码如下:
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-04-09 05:15:40 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.fragment; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.view.View; import com.gcssloop.diycode.fragment.base.SimpleRefreshRecyclerFragment; import com.gcssloop.diycode.fragment.provider.NewsProvider; import com.gcssloop.diycode_sdk.api.news.bean.New; import com.gcssloop.diycode_sdk.api.news.event.GetNewsListEvent; import com.gcssloop.diycode_sdk.log.Logger; import com.gcssloop.recyclerview.adapter.multitype.HeaderFooterAdapter; import java.util.List; /** * 首页 news 列表 */ public class NewsListFragment extends SimpleRefreshRecyclerFragment<New, GetNewsListEvent> { private boolean isFirstLaunch = true; public static NewsListFragment newInstance() { Bundle args = new Bundle(); NewsListFragment fragment = new NewsListFragment(); fragment.setArguments(args); return fragment; } @Override public void initData(HeaderFooterAdapter adapter) { // 优先从缓存中获取数据,如果是第一次加载则恢复滚动位置,如果没有缓存则从网络加载 List<Object> news = mDataCache.getNewsListObj(); if (null != news && news.size() > 0) { Logger.e("news : " + news.size()); pageIndex = mConfig.getNewsListPageIndex(); adapter.addDatas(news); if (isFirstLaunch) { int lastPosition = mConfig.getNewsListLastPosition(); mRecyclerView.getLayoutManager().scrollToPosition(lastPosition); isFirstAddFooter = false; isFirstLaunch = false; } } else { loadMore(); } } @Override protected void setAdapterRegister(Context context, RecyclerView recyclerView, HeaderFooterAdapter adapter) { adapter.register(New.class, new NewsProvider(getContext())); } @NonNull @Override protected String request(int offset, int limit) { return mDiycode.getNewsList(null, offset,limit); } @Override protected void onRefresh(GetNewsListEvent event, HeaderFooterAdapter adapter) { super.onRefresh(event, adapter); mDataCache.saveNewsListObj(adapter.getDatas()); } @Override protected void onLoadMore(GetNewsListEvent event, HeaderFooterAdapter adapter) { // TODO 排除重复数据 super.onLoadMore(event, adapter); mDataCache.saveNewsListObj(adapter.getDatas()); } @Override public void onDestroyView() { super.onDestroyView(); // 存储 PageIndex mConfig.saveNewsListPageIndex(pageIndex); // 存储 RecyclerView 滚动位置 View view = mRecyclerView.getLayoutManager().getChildAt(0); int lastPosition = mRecyclerView.getLayoutManager().getPosition(view); mConfig.saveNewsListPosition(lastPosition); } }
3.7.private SitesListFragment mFragment3;
网站页面碎片。
源代码如下:
/* * Copyright 2017 GcsSloop * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Last modified 2017-04-09 14:32:41 * * GitHub: https://github.com/GcsSloop * Website: http://www.gcssloop.com * Weibo: http://weibo.com/GcsSloop */ package com.gcssloop.diycode.fragment; import android.content.Context; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.RecyclerView; import com.gcssloop.diycode.fragment.base.RefreshRecyclerFragment; import com.gcssloop.diycode.fragment.bean.SiteItem; import com.gcssloop.diycode.fragment.bean.SitesItem; import com.gcssloop.diycode.fragment.provider.SiteProvider; import com.gcssloop.diycode.fragment.provider.SitesProvider; import com.gcssloop.diycode_sdk.api.sites.bean.Sites; import com.gcssloop.diycode_sdk.api.sites.event.GetSitesEvent; import com.gcssloop.diycode_sdk.log.Logger; import com.gcssloop.recyclerview.adapter.multitype.HeaderFooterAdapter; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * 首页 sites 列表 */ public class SitesListFragment extends RefreshRecyclerFragment<Sites, GetSitesEvent> { public static SitesListFragment newInstance() { Bundle args = new Bundle(); SitesListFragment fragment = new SitesListFragment(); fragment.setArguments(args); return fragment; } @Override public void initData(HeaderFooterAdapter adapter) { setLoadMoreEnable(true); List<Serializable> sitesList = mDataCache.getSitesItems(); if (sitesList != null) { Logger.e("sites : " + sitesList.size()); mAdapter.addDatas(sitesList); setLoadMoreEnable(false); } else { loadMore(); } } @Override protected void setAdapterRegister(Context context, RecyclerView recyclerView, HeaderFooterAdapter adapter) { mAdapter.register(SiteItem.class, new SiteProvider(getContext())); mAdapter.register(SitesItem.class, new SitesProvider(getContext())); } @NonNull @Override protected RecyclerView.LayoutManager getRecyclerViewLayoutManager() { GridLayoutManager layoutManager = new GridLayoutManager(getContext(), 2); layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return (mAdapter.getFullDatas().get(position) instanceof SiteItem) ? 1 : 2; } }); return layoutManager; } @NonNull @Override protected String request(int offset, int limit) { return mDiycode.getSites(); } @Override protected void onRefresh(GetSitesEvent event, HeaderFooterAdapter adapter) { toast("刷新成功"); convertData(event.getBean()); } @Override protected void onLoadMore(GetSitesEvent event, HeaderFooterAdapter adapter) { toast("加载成功"); convertData(event.getBean()); } @Override protected void onError(GetSitesEvent event, String postType) { toast("获取失败"); } // 转换数据 private void convertData(final List<Sites> sitesList) { ArrayList<Serializable> items = new ArrayList<>(); for (Sites sites : sitesList) { items.add(new SitesItem(sites.getName())); for (Sites.Site site : sites.getSites()) { items.add(new SiteItem(site.getName(), site.getUrl(), site.getAvatar_url())); } if (sites.getSites().size() % 2 == 1) { items.add(new SiteItem("", "", "")); } } mAdapter.clearDatas(); mAdapter.addDatas(items); mDataCache.saveSitesItems(items); setLoadMoreEnable(false); } }
3.8.private boolean isToolbarFirstClick=true;
如果是第一次
那么提示:双击回到顶部。
4.分析MainActivity中的部分函数
4.1.首先是复写getLayoutId(),获得布局ID。
在BaseActivity中定义的抽象函数。由于继承关系,必须要实现。
4.2.然后是复写initViews(ViewHolder holder,View root)
在BaseActivity中定义的抽象函数。由于继承关系,必须要实现。
initMenu(holder)==>初始化菜单
initViewPager(holder)==>初始化页面
4.3.初始化菜单(包括侧边栏菜单和顶部菜单选项
initMenu(ViewHoler holder)
实图对应关系
4.4.loadMenuData()
加载侧边栏菜单数据(与用户相关)
实图对应关系
4.5.初始化ViewPager
4.5.1.实图对应关系
4.5.2.Viewpager设置适配器
这里通过一个系统类==>FragmentPagerAdapter来设置页面。
这个项目有3个types。Topics,News,Sites。但是额外添加了一列Test,方便测试。
复写了三个方法。
①.public Fragement getItem(int position)
②.public int getCount()
③.public CharSequence getPageTitle(int position)
4.5.3.ViewPager设置监听器
主页面ViewPage设置监听,复写三个方法。
①.public void onPageScrolled(...)
②.public void onPageSelected(int position)
③.public void onPageScrollStateChanged(int state)
4.5.4.设置其他属性
首先从我的配置中获得当前是哪个页面类型。
然后将这个页面类型在ViewPager中设置为当前页。然后可以左右切换了。
和上面的Tab布局挂钩,这样,页面切换上面的Tab也会变化。
注意点:
①如果我注释了mTabLayout.setupWithViewPager(mViewPager)之后
将变成这样:
可以左右滑动,当时上面没有Tab提示了。
②.如果我注释掉mViewPager.setCurrentItem(mCurrentPosition)
那么,我每次退出APP,但没有杀死进程,然后再点进去
都是从第一个Tab开始。
反之,如果我没有注释掉这行代码,那么APP就能记住这个Tab。
那么,下次点进去就能回到这个Tab。
4.6.快速回到顶部
首先确定当前页面是哪个类型。
因为每个页面类型是不一样的,虽然Topic和News页面很相似,但是Sites页面是不同的。
所以每个mFragment中也有自己的qucikToTop()函数。
这里为了方便管理,采用了同一个函数,但是执行的却是不同代码。
4.7.判断用户是否成功登陆,成功登出。
@Subscribe(threadMode=ThreadMode.MAIN)==>表示该函数在主线程UI线程中执行。
用户登陆成功,或者未登陆,左侧的菜单栏是不一样的。
5.继续分析MainActivity中的剩下的函数
5.1.剩下所有的函数预览(全是@Override)
都是一些有关菜单选项,点击事件,活动销毁,自带的返回键的一些复写方法。
5.2.onBackPressed()
意义:左侧菜单栏打开的时候,按返回键,则菜单栏关闭。
5.3.void onCreateOptionsMenu(Menu menu)
意义:创建标题栏的菜单项。
图解:
解释:
第一个item,通知图标。app:showAsAction:always代表在标题栏上出现。
第二个item,设置的第一个,app:showAsAction:never代表点了三个点才出现。
第三个item,设置的第二个,android:orderInCategory代表优先级,谁大谁在前面。
注意点:
如果我把第一个app:showAsAction修改成never,之后会变成这样。
5.4.boolean onOptionsItemSelected(MenuItem item)
处理菜单真实点击事件。
如果点击了通知,则调转到==>NotificationActivity活动页面。
如果点击了设置,则跳转到==>SettingActivity活动页面。
5.5.boolean onNavigationItemSelected(MenuItem item)
因为implements了一个接口:Navigation.OnNavigationItemSelectedListener
所以这里必须要实现这个类。
源代码为:
作用:如果点击了我的帖子,先判断是否登录,然后决定跳转到MyTopicActivity.InfoType.MY_TOPIC。
如果点击了我的收藏,先判断是否登录,然后决定跳转到MyTopicActivity.InfoType.MY_COLLECT。
图解:
5.6.onDestroy()
意义:EventBus反注册。防止内容泄露。
配置退出页面前当前的页面类型。
5.7.onClick(View v)
处理点击事件。
首先是第一次,仅仅是第一次点击toolbar,提示用户有这个功能。没有实质上的返回顶部。
然后是浮动按钮,点击后判断是哪个碎片,执行返回到顶部。
6.简单总结一下
6.1.其实应该早一点分析这个MainActivity,但是如果一下子接触这个东西,可能会有很多不知道的地方,然后
会影响到内心情绪,所以干脆先把一些简单的,能看懂的地方先摸清,然后再尝试从主活动开始。
6.2.首先要理解这个MainActivity是继承了BaseActivity,主要是有两个抽象方法,基本上每个活动都会有的东西,
标题栏,toast提示,打开另一个活动,布局id,视图ViewHolder。
6.3.然后理解实现Navigation.OnNavigationItemSelectedListener接口,就是左侧菜单栏的点击事件,布局效果直接
利用app:headerLayout和app:Menu两者结合创建这个左侧菜单布局效果。
6.4.然后理解实现View.OnClickListener,主活动中,有两处点击事件(不包括菜单),一个是标题栏,第一次单击
标题栏,会提示双击会回到顶部。还有一个浮动按钮,单击回到顶部。
6.5.然后数据定义有数据缓存DataCache,用户配置Config,这些都是一些通用类,当然耦合度也比较低,所以
可以放心用做其他项目。然后是三大巨头,三个Tab碎片,利用Fragment来实现。
6.6.理解主活动的布局是由最外层的DrawerLayout一个带有抽屉,也就是左侧菜单的布局,左侧布局利用一个
NavigationView来实现,然后左侧布局局部用app:headerLayout和app:menu来组合实现通用效果。
6.7.主活动布局中的主体是include中的app_bar_main.xml,最外层是CoordinatorLayout,就是一个拥有一些
滑动效果的布局方式,然后标题栏用一个AppBarLayout总布局+Toolbar+TabLayout的分布局来实现。然后
主体中的主体是一个include,然后是一个浮动按钮FloatingActionButton。
6.8.主体中的主体content_main.xml结构是一个RelativeLayout中有一个ViewPager,实现分页效果。
6.9.一个小小的贴士:如果在content_main.xml中的RelativeLayout中加入一个tools:showIn=
'@layout/app_bar_main',那么在content_main.xml预览中就能看的这个浮动按钮了。
6.10.在主活动的初始化视图initViews中,EventBus注册+DataCache初始化+Config单例创建+初始化主体的标题栏
+侧边栏菜单+加载侧边栏菜单数据(即判断用户是否处于登录状态),然后是初始化ViewPager。整个界面就
搭建好了,之后就是具体实现怎么加载两个菜单,一个顶部,一个侧边栏,怎么初始化ViewPager了。
6.11.具体的initViewPager,是首先从holder中获得id,然后新建3个主体碎片,然后设置适配器。适配器复写
三个必须实现的函数。然后是页面监听变化效果。也有三个必须实现的函数。然后配置当前页和顶部Tab。
6.12.如何加载用户已经等录过后的具体菜单栏数据。这里需要调用API中的函数和处理缓存数据。
6.13.然后利用ActionBarDrawerToggle将布局中的drawerLayout和Toolbar两者关联起来。然后drawer设置监听器,
drawr.setDrawerListener(上面的toggle)即可。然后toggle.syncState(),就行了。完全关联起来了。
6.14.然后就是左侧的菜单栏设置监听器,因为继承关系,这里复写一个onNavigationItemSelected处理逻辑即可。
6.15.然后处理双击标题栏的快速回到顶部的实质代码。采用了一个手势效果,GestureDetector类来定义双击效果。
然后调用自定义的quickToTop()函数来实现。当然这样不行,要将这个手势效果加到toolbar中,所以下面
继续写一个toolbar触摸监听事件,将手势监听和toolbar关联一下,在toolbar触摸是,将手势执行即可。
6.16.侧边栏菜单数据相对来说要负杂一些,首先要判断用户是否登录,然后看能否从缓存中读取用户信息,如果
登录过,先从Cache中获得我的相关数据,否则调用API得到我的信息。然后设置头像监听,跳转到User活动。
6.17.主体标题栏的设置是通过复写的函数onOptionsItemSelected(MenuItem item)来实现。实现一些具体跳转。
6.18.MainActivity相对于其他活动来说的确复杂很多,但是这个活动是深入了解其他活动的大门,开启了这个大门
之后,其他活动就如鱼得水了。而且这个项目是真的棒,我感觉会学到很多新的东西。