# 第二天教程 # 接下来就可以构建我们的第一个 activity 了,SplashActivity 的页面效果我们模仿知乎日报的 splash 页面,是图片放大的动画效果,布局文件代码如下: SplashActivity 代码如下: public class SplashActivity extends BaseActivity { public static final String IMG = "img"; private ImageView mSplashImageView; private CacheUtil mCache; @TargetApi(Build.VERSION_CODES.KITKAT) @Override protected void initView(Bundle savedInstanceState) { // 全屏 getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); setContentView(R.layout.activity_splash); mSplashImageView = (ImageView) findViewById(R.id.iv_splash); } @Override protected void initData() { mCache = CacheUtil.getInstance(this); // 缓存不为空加载缓存 if (!mCache.isCacheEmpty(IMG)) { ImageUtil.getInstance().displayImage(mCache.getAsString(IMG), mSplashImageView); } // 在联网情况下,写入新缓存,且如果旧缓存为空则显示图片 if (isNetConnect()) { OkUtil.getInstance().okHttpZhihuJObject(API.ZHIHU_START, IMG, new OkUtil.JObjectCallback() { @Override public void onFailure(Call call, IOException e) { e.printStackTrace(); startToMainActivity(); } @Override public void onResponse(Call call, String jObjectUrl) { if (jObjectUrl != null) { if (mCache.isCacheEmpty(IMG)) { ImageUtil.getInstance().displayImage(jObjectUrl, mSplashImageView); } mCache.put(IMG, jObjectUrl); } else { if (mCache.isCacheEmpty(IMG)) { startToMainActivity(); } } } }); } else { // 没网没缓存 if (mCache.isCacheEmpty(IMG)) { startToMainActivity(); } LazyUtil.showToast("网络连接错误"); } ScaleAnimation scaleAnimation = new ScaleAnimation(1.0f, 1.2f, 1.0f, 1.2f, Animation .RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); scaleAnimation.setFillAfter(true); scaleAnimation.setDuration(3000); scaleAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { startToMainActivity(); } @Override public void onAnimationRepeat(Animation animation) { } }); mSplashImageView.startAnimation(scaleAnimation); } private void startToMainActivity() { startActivity(new Intent(this, MainActivity.class)); finish(); } } 这里的 api 都比较简单,我就不多做介绍了,逻辑流程也在代码中写得很清晰了。 SplashActivity 执行完动画后,进入 MainActivity,MainActivity 代码如下: public class MainActivity extends BaseActivity { public MainFragment mContentGank; public MainFragment mContentZhihu; private Toolbar mTitleToolbar; private TabLayout mTitleTabLayout; private NavigationView mContentNavigationView; private DrawerLayout mMainDrawerLayout; private long firstTime; private int mLastItemId; @Override protected void initView(Bundle savedInstanceState) { setContentView(R.layout.activity_main); mTitleToolbar = (Toolbar) findViewById(R.id.tb_title); mTitleTabLayout = (TabLayout) findViewById(R.id.tl_title); mContentNavigationView = (NavigationView) findViewById(R.id.nv_content); mMainDrawerLayout = (DrawerLayout) findViewById(R.id.dl_main); // 设置导航栏顶部图片 View view = mContentNavigationView.getHeaderView(0); ImageView header = (ImageView) view.findViewById(R.id.nav_head); ImageUtil.getInstance().displayImage(CacheUtil.getInstance(this).getAsString(SplashActivity.IMG), header); // 设置 toolBar setSupportActionBar(mTitleToolbar); final ActionBar ab = getSupportActionBar(); ab.setHomeAsUpIndicator(R.drawable.ic_menu); ab.setDisplayHomeAsUpEnabled(true); ActionBarDrawerToggle actionBarDrawerToggle = new ActionBarDrawerToggle(this, mMainDrawerLayout, mTitleToolbar, R.string.meizhi, R.string.meizhi); mMainDrawerLayout.addDrawerListener(actionBarDrawerToggle); actionBarDrawerToggle.syncState(); } @Override protected void initData() { setupDrawerContent(); mContentNavigationView.setCheckedItem(0); } private void setupDrawerContent() { mLastItemId = mContentNavigationView.getMenu().getItem(0).getItemId(); changeFragments(mLastItemId); mContentNavigationView.setNavigationItemSelectedListener(new NavigationView .OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem item) { mMainDrawerLayout.closeDrawers(); if (item.getItemId() == R.id.menu_introduce) { startActivity(new Intent(MainActivity.this, AboutMeActivity.class)); item.setChecked(false); } else { if (item.getItemId() != mLastItemId) { item.setChecked(true); changeFragments(item.getItemId()); mLastItemId = item.getItemId(); } } return true; } }); } public void changeFragments(int itemId) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); hideAll(transaction); switch (itemId) { case R.id.nav_knowledge: // 知乎界面 if (mContentZhihu != null) { transaction.show(mContentZhihu); } else { mContentZhihu = MainFragment.newInstance(MainFragment .MENU_ZHIHU); mContentZhihu.setOnBannerClickListener(this); mContentZhihu.setOnDailyItemClickListener(this); mContentZhihu.setOnBannerClickListener(this); mContentZhihu.setOnHotItemClickListener(this); transaction.add(R.id.fl_content, mContentZhihu); } initToolbar(MainFragment .MENU_ZHIHU); break; case R.id.nav_beauty: // 妹纸界面 if (mContentGank != null) { transaction.show(mContentGank); } else { mContentGank = MainFragment.newInstance(MainFragment .MENU_GANK); mContentGank.setTextListener(this); mContentGank.setImageListener(this); transaction.add(R.id.fl_content, mContentGank); } initToolbar(MainFragment .MENU_GANK); break; default: break; } transaction.commit(); } // 隐藏所有 fragment private void hideAll(FragmentTransaction transaction) { if (mContentZhihu != null) { transaction.hide(mContentZhihu); } if (mContentGank != null) { transaction.hide(mContentGank); } } // 暴露给 fragment 连接 tabLayout public void setupViewPager(ViewPager viewPager) { mTitleTabLayout.setSelectedTabIndicatorColor(Color.WHITE); mTitleTabLayout.setupWithViewPager(viewPager); } public void hideTabLayout(boolean hide) { if (hide) { mTitleTabLayout.setVisibility(View.GONE); } else { mTitleTabLayout.setVisibility(View.VISIBLE); } } public void initToolbar(String args) { if (args.equals(MainFragment.MENU_GANK)) { hideTabLayout(true); setToolbarTitle("妹纸"); } else { hideTabLayout(false); setToolbarTitle("知乎"); } } public void setToolbarTitle(String title) { getSupportActionBar().setTitle(title); } @Override public void onBackPressed() { if (mContentNavigationView.isShown()) { mMainDrawerLayout.closeDrawers(); return; } long secondTime = System.currentTimeMillis(); if (secondTime - firstTime > 2000) { Snackbar sb = Snackbar.make(mContentNavigationView, "再按一次退出", Snackbar.LENGTH_SHORT); sb.getView().setBackgroundColor(getResources().getColor(R.color.red_300)); sb.show(); firstTime = secondTime; } else { finish(); } } } 布局文件如下: 这里涉及到了官方的 material design 库,在此我推荐几个学习的资料 [Android Design Support Library使用详解(徐宜生)](http://blog.csdn.net/eclipsexys/article/details/46349721),[android CoordinatorLayout使用(张兴业)](http://blog.csdn.net/xyz_lmn/article/details/48055919) 以及翻译自CodePath 上 [关于 CoordinatorLayout](./CoordinatorLayout.md) 。最后一篇是我自行翻译的,所以不带原文的图片,文档中附原文链接,可参考原文文档中的图片,看完这三篇文章并且能够理解的话,看懂上面的代码就不成问题了。 所以我这里大概的实现思路就是,主页面就是以 material design 为主的几个控件以及一个 FrameLayout,动态来添加 fragment,来达到我所需要的效果。在点击事件中我们可以看到,无论是妹纸界面还是知乎界面,其实差别就是在于 ``MainFragment.newInstance(String string);`` 中的这个参数不一样。所以重点就在于 MainFragment 了,其代码如下: /** * A simple {@link Fragment} subclass. */ public class MainFragment extends BaseFragment { public final static String MENU_GANK = "menu_gank"; public final static String MENU_ZHIHU = "menu_zhihu"; public final static String MENU_ID = "menu_id"; public GankFragment mGankFragment; public ZhihuDailyNewsFragment mDailyNewsFragment; public ZhihuHotNewsFragment mHotNewsFragment; private MainAdapter adapter; private ViewPager mContentViewPager; private List mFragments = new ArrayList<>(); private List mTitles = new ArrayList<>(); public MainFragment() { // Required empty public constructor } public static MainFragment newInstance(String menuId) { Bundle args = new Bundle(); args.putString(MENU_ID, menuId); MainFragment fragment = new MainFragment(); fragment.setArguments(args); return fragment; } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.fragment_main, container, false); mContentViewPager = (ViewPager) view.findViewById(R.id.vp_content); adapter = new MainAdapter(getChildFragmentManager(), mFragments, mTitles); initFragments(); return view; } public void initFragments() { String args = getArguments().getString(MENU_ID); if (MENU_GANK.equals(args)) { mGankFragment = new GankFragment(); // mGankFragment.setImageListener(this); // mGankFragment.setTextListener(this); mFragments.add(mGankFragment); mTitles.add("妹纸"); adapter.changeDataList(mTitles, mFragments); } else { mDailyNewsFragment = new ZhihuDailyNewsFragment(); // mDailyNewsFragment.setOnItemClickListener(this); // mDailyNewsFragment.setOnBannerClickListener(this); mHotNewsFragment = new ZhihuHotNewsFragment(); // mHotNewsFragment.setOnItemClickListener(this); mFragments.add(mDailyNewsFragment); mFragments.add(mHotNewsFragment); mTitles.add("知乎日报"); mTitles.add("热门消息"); adapter.changeDataList(mTitles, mFragments); } mContentViewPager.setAdapter(adapter); // 调用 MainActivity 的 setupViewPager() 方法 ((MainActivity) mActivity).setupViewPager(mContentViewPager); } } 这里可以看到,实际上我们做的事情就是,把传过来的参数进行匹配,如果是 ``MENU_GANK`` 的话,那么就是显示妹纸页面,如果是 ``MENU_ZHIHU`` 那么我们就显示知乎界面。这里将 MainFragment 的创建封装成一个静态方法,这样的好处显而易见,任何需要创建 MainFragment 的地方都调用这个方法,需要传入什么参数,也都一目了然。 其布局文件代码如下: 这里其实可以看得出来, MainFragment 其实是 GankFragment 和 DailyNewsFragment, HotNewsFragment 的父 fragment,我们在 MainFragment 里面其实就是放了一个 ViewPager,然后在 ViewPager 里面在放置相应的 fragment。所以我们这里使用的是 fragment 嵌套 fragment 思路。 适配器 MainAdapter 代码如下: /** * Created by joker on 2016/8/4. */ public class MainAdapter extends FragmentStatePagerAdapter { private List mFragments; private List mTitles; public MainAdapter(FragmentManager fm, List fragments, List titles) { super(fm); mFragments = fragments; mTitles = titles; } @Override public Fragment getItem(int position) { return mFragments.get(position); } @Override public int getCount() { return mFragments.size(); } @Override public CharSequence getPageTitle(int position) { return mTitles.get(position); } public void changeDataList(List titles, List fragments) { mTitles = titles; mFragments = fragments; } } 在使用 ViewPager 嵌套 fragment 的时候,官方更建议我们使用 FragmentStatePagerAdapter 或者 FragmentPageAdapter,这两者的主要区别在于 FragmentPageAdapter 类内的每一个生成的 Fragment 都将保存在内存之中,所以适用于那些相对静态的页,数量也比较少的那种;如果需要处理有很多页,并且数据动态性较大、占用内存较多的情况,应该使用FragmentStatePagerAdapter。详情可见这篇[FragmentPagerAdapter与FragmentStatePagerAdapter区别](http://www.cnblogs.com/lianghui66/p/3607091.html) 接下来,我们就是构建 GankFragment, ZhihuDailyNewsFragment, ZhihuHotNewsFragment 了,实际上,这三者的逻辑业务是差不多的,所以我们不妨构建一个它们共同的基类 fragment 继承自 BaseFragment,我们命名为 ContentFragment,代码如下: /** * 懒加载 fragment * A simple {@link Fragment} subclass. * Created by joker on 2016/8/8. */ public abstract class ContentFragment extends BaseFragment implements SwipeRefreshLayout.OnRefreshListener { // 数据是否加载完毕 protected boolean isDataLoaded = false; // 视图是否创建完毕 protected boolean isViewCreated = false; protected OkUtil mOkUtil; protected CacheUtil mCache; protected Gson mGson; protected PullLoadRecyclerView mContentRecyclerView; protected SwipeRefreshLayout mContentSwipeRefreshLayout; protected Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0x122: mContentSwipeRefreshLayout.setRefreshing(false); LazyUtil.showToast("网络没有连接哦"); break; case 0x121: // 下拉刷新 loadDataFromNet(getFirstPageUrl()); default: break; } } }; public ContentFragment() { // Required empty public constructor } // 获取最初的 url (刷新或者第一次加载时的 url) protected abstract String getFirstPageUrl(); protected boolean isFirstPage(String url) { return getFirstPageUrl().equals(url); } /** * 1. 缓存为空时第一次加载缓存 或者刷新 * 2. 上拉加载更多 * * @param url 前者使用 getFirstPageUrl() 后者需自己传入 是固定值 */ protected abstract void loadDataFromNet(String url); @Nullable @Override @CallSuper public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); // Inflate the layout for this fragment View view = inflater.inflate(R.layout.fragment_content, container, false); mContentRecyclerView = (PullLoadRecyclerView) view.findViewById(R.id.rv_content); mContentSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.srl_content); SpacesItemDecoration decoration = new SpacesItemDecoration((int) (Math.random() * 5 + 15)); mContentRecyclerView.addItemDecoration(decoration); initView(inflater, container); initSwipeRefreshLayout(); isViewCreated = true; return view; } protected abstract void initView(LayoutInflater inflater, ViewGroup container); protected void initSwipeRefreshLayout() { mContentSwipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_bright, android .R.color.holo_orange_light, android.R.color.holo_green_light, android.R.color .holo_red_dark); mContentSwipeRefreshLayout.setOnRefreshListener(this); } @CallSuper protected void initData() { isDataLoaded = true; mOkUtil = OkUtil.getInstance(); mCache = CacheUtil.getInstance(mActivity); mGson = new Gson(); } @CallSuper @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (isVisibleToUser && !isDataLoaded && isViewCreated) { // ViewPager 其他页面的 fragment,我们进行判断后再加载数据 initData(); } } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // 对于第一个直接呈现在用户面前的 fragment, 我们需要加载数据 if (getUserVisibleHint()) { initData(); } } public boolean isNetConnect() { return mActivity.isNetConnect(); } @Override public void onRefresh() { if (isNetConnect()) { mHandler.sendEmptyMessage(0x121); } else { mHandler.sendEmptyMessage(0x122); } } @Override public void onStop() { super.onStop(); LazyUtil.log(getClass().getName(), " onStop"); OkUtil.getInstance().cancelAll(mOkUtil); if (mContentSwipeRefreshLayout.isRefreshing()) { mContentSwipeRefreshLayout.setRefreshing(false); } } @Override public void onDestroy() { super.onDestroy(); LazyUtil.log(getClass().getName() + " onDestroy"); // RefWatcher refWatcher = GankOrApplication.getRefWatcher(mActivity.getApplicationContext()); // refWatcher.watch(this); mActivity = null; } } 其布局文件如下: 在这里,我们又自定义了一个可上拉加载更多的 RecyclerView,代码如下: /** 上拉加载更多 RecyclerView * Created by joker on 2016/8/15. */ public class PullLoadRecyclerView extends RecyclerView { private int mLastVisiblePosition; private boolean isLoading = false; private PullLoadRecyclerView.onPullLoadListener onPullLoadListener; public PullLoadRecyclerView(Context context) { super(context); } public PullLoadRecyclerView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); } public PullLoadRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void onScrolled(int dx, int dy) { super.onScrolled(dx, dy); if (getLayoutManager() instanceof StaggeredGridLayoutManager) { StaggeredGridLayoutManager manager = (StaggeredGridLayoutManager) getLayoutManager(); int[] position = manager.findLastCompletelyVisibleItemPositions(new int[manager .getSpanCount()]); mLastVisiblePosition = getMaxPosition(position); } else if (getLayoutManager() instanceof LinearLayoutManager) { LinearLayoutManager manager = (LinearLayoutManager) getLayoutManager(); mLastVisiblePosition = manager.findLastCompletelyVisibleItemPosition(); } if (onPullLoadListener != null && mLastVisiblePosition + 1 == getAdapter().getItemCount() && !isLoading && dy > 0) { if (!NetUtil.isNetConnect(getContext())) { LazyUtil.showToast( "网络没有连接,不能加载噢"); return; } isLoading = true; onPullLoadListener.onPullLoad(); } } public int getMaxPosition(int[] positions) { int size = positions.length; int maxPosition = Integer.MIN_VALUE; for (int i = 0; i < size; i++) { maxPosition = Math.max(maxPosition, positions[i]); } return maxPosition; } // 上拉加载更多接口 public void setPullLoadListener(onPullLoadListener onPullLoadListener) { this.onPullLoadListener = onPullLoadListener; } public void setIsLoading(boolean isLoading) { this.isLoading = isLoading; } public interface onPullLoadListener { void onPullLoad(); } } 关于上拉加载更多的 RecyclerView 可以参考这篇[RecyclerView实例-实现可下拉刷新上拉加载更多并可切换线性流和瀑布流模式(1)](http://www.cnblogs.com/xiaoyaoxia/p/4977125.html),简单来说,就是在它的滑动事件中首先判断当前的 layoutManager,其次就是针对当前最后一个可见的 item 的 position 进行监听,如果当前传入的 listener 不为空,且当前最后一个可见的 item 是 adpater 里面最后的一个 item, 且当前不是在加载状态中,且当前是屏幕向上滑动的状态,那么我们就可以调用上拉加载更多接口的 ``onPullLoad()`` 方法啦。 对于日常需求中,我们在很多情况下对于 ViewPager 的预加载功能感到很烦恼,而这个懒加载的 fragment 就很好的解决了这个问题,所谓懒加载,即 fragment 的 UI 对用户可见时才加载数据,具体可参考这篇[Fragment 懒加载实战](http://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ==&mid=2650820834&idx=1&sn=694a94615494bfcaed07188e2601724a&scene=23&srcid=0808vHgojfq1vTzIpSDNBhwq#rd)。在 MainFragment 中我们除了对需要用的 Okhttp, Gson, Cache 对象初始化,需要用到的布局文件初始化之外,最重要的就是在 fragment 销毁的时候将资源都销毁,例如网络请求的取消(防止影响当前页面网络请求阻塞),``SwipeRefreshLayout.setRefreshing(false);``(防止网络过慢导致一直不能获取到网络请求)