《简易新闻》源码分析

来源:互联网 发布:安徽宝德网络 编辑:程序博客网 时间:2024/06/06 19:08

0. 前言

本文将对github上 liuling开发的基于Material Design和MVP的《简易新闻》源码进行简要分析,通过本文你将学到:

  • 阅读应用源码的步骤
  • RecyclerView
  • NavigationView
  • 下拉刷新和上拉加载
  • Material过渡动画
  • CollapsingToolbarLayout

1. 寻找入口

分析一个应用就是从MainActivity下手,那么如何找到MainActivity呢?当然还是通过Manifest文件,不过,在进入Manifest文件前,我们先来看看工程的一个结构。

1.1 工程总览

工程结构
工程的目录结构如上图所示,有两个Module,一个是应用本身,还有一个是导入的swipeback库,用于滑动返回,如图:
滑动返回效果图

1.2 Manifest文件

1.2.1 权限声明

<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

首先声明了权限,分别是网络相关和位置相关的权限。

1.2.2 应用层

<application    ...    android:supportsRtl="true"    android:theme="@style/AppTheme">    <activity        android:name=".main.widget.MainActivity" ... >    ...    </activity>    ...</application>

这里Application标签中有一个属性 android:supportsRtl="true" 是什么意思呢?这是Android 4.2的一个新特性 layoutRtl,主要是方便开发者去支持阿拉伯语/波斯语等从右到左的阅读习惯。
接下来指明了两个主要的Activity:
- MainActivity
- NewsDetailActivity

2. MainActivity

public class MainActivity extends AppCompatActivity implements MainView

主Activity实现了MainView接口,所以我们来先看看该接口:
*

2.1 MainView接口

接口很简单,包含四个方法声明,分别是主界面的四个拨动页面。

public interface MainView {    void switch2News();    void switch2Images();    void switch2Weather();    void switch2About();}

2.2 onCreate()方法

2.2.1 布局文件

首先来看主布局文件,布局可以说是简单易懂,清晰明了。一个DrawerLayout中夹了协调布局包裹的FrameLayout作为主界面和一个NavigationView。
主界面

其中值得注意的是就是这个NavigationView:
导航抽屉界面

<android.support.design.widget.NavigationView    android:id="@+id/navigation_view"    android:layout_width="wrap_content"    android:layout_height="match_parent"    android:layout_gravity="start"    app:headerLayout="@layout/navigation_header"    app:menu="@menu/navigation_menu" />
  • app:headerLayout 属性: 头布局文件,及抽屉上方的个人头像。说起头像,就要用到CircleImageView,相信也会有读者像我一样曾经好奇过CircleImageView用来干什么,怎么用吧,没错,就是这样用的:
<de.hdodenhof.circleimageview.CircleImageView    android:id="@+id/profile_image"    android:layout_width="72dp"    android:layout_height="72dp"    android:layout_marginTop="20dp"    android:src="@drawable/protrait"    app:border_color="@color/primary_light"    app:border_width="2dp" />
  • app:menu 属性: 使用菜单来填充选项,大家就不要以为只可以使用ListView自定义来实现菜单选择咯,但是笔者认为这里有个缺陷就是,抽屉会默认遮住状态栏和Toolbar。
<group android:checkableBehavior="single">    <item        android:id="@+id/navigation_item_news"        android:icon="@drawable/ic_assessment_white_24dp"        android:checked="true"        android:title="@string/navigation_news" />    ...</group>

2.2.2 初始化视图

mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.drawer_open,        R.string.drawer_close);mDrawerToggle.syncState();mDrawerLayout.setDrawerListener(mDrawerToggle);mNavigationView = (NavigationView) findViewById(R.id.navigation_view);setupDrawerContent(mNavigationView);private void setupDrawerContent(NavigationView navigationView) {    navigationView.setNavigationItemSelectedListener(            new NavigationView.OnNavigationItemSelectedListener() {                @Override                public boolean onNavigationItemSelected(MenuItem menuItem) {                    mMainPresenter.switchNavigation(menuItem.getItemId());                    menuItem.setChecked(true);                    mDrawerLayout.closeDrawers();                    return true;                }            });}

首先实例化了ActionBar开关,同时调用syncState()同步状态,后面对mNavigationView设置了监听,实现了切换选项卡的效果。

// in class MainPresenterImpl@Overridepublic void switchNavigation(int id) {    switch (id) {        case R.id.navigation_item_news:            mMainView.switch2News();            break;        case R.id.navigation_item_images:            mMainView.switch2Images();            break;        case R.id.navigation_item_weather:            mMainView.switch2Weather();            break;        case R.id.navigation_item_about:            mMainView.switch2About();            break;        default:            mMainView.switch2News();            break;    }}

2.2.3 切换

@Overridepublic void switch2News() {    getSupportFragmentManager().beginTransaction().replace(R.id.frame_content, new NewsFragment()).commit();    mToolbar.setTitle(R.string.navigation_news);}

四个选项卡切换Fragment即可,那我们依次来看看这几个Fragment。

3. 新闻界面

3.1 布局文件

<android.support.design.widget.CoordinatorLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent">    <android.support.design.widget.AppBarLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">        <android.support.design.widget.TabLayout            android:id="@+id/tab_layout"            android:layout_width="match_parent"            android:layout_height="?attr/actionBarSize"            app:tabIndicatorColor="@color/icons"/>    </android.support.design.widget.AppBarLayout>    <android.support.v4.view.ViewPager        android:id="@+id/viewpager"        android:layout_width="match_parent"        android:layout_height="wrap_content"        app:layout_behavior="@string/appbar_scrolling_view_behavior"/></android.support.design.widget.CoordinatorLayout>

注意点

  • tabIndicatorColor: TabLayout所选标签标志颜色,一张图看懂,此处改为蓝色
    tabIndicatorColor


  • layout_behavior: 滚动时自动消失Toolbar

3.2 视图初始化

    private void setupViewPager(ViewPager mViewPager) {        //Fragment中嵌套使用Fragment一定要使用getChildFragmentManager(),否则会有问题        MyPagerAdapter adapter = new MyPagerAdapter(getChildFragmentManager());        adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_TOP), getString(R.string.top));        adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_NBA), getString(R.string.nba));        adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_CARS), getString(R.string.cars));        adapter.addFragment(NewsListFragment.newInstance(NEWS_TYPE_JOKES), getString(R.string.jokes));        mViewPager.setAdapter(adapter);    }    public static class MyPagerAdapter extends FragmentPagerAdapter {        private final List<Fragment> mFragments = new ArrayList<>();        private final List<String> mFragmentTitles = new ArrayList<>();        public MyPagerAdapter(FragmentManager fm) {            super(fm);        }        public void addFragment(Fragment fragment, String title) {            mFragments.add(fragment);            mFragmentTitles.add(title);        }        @Override        public Fragment getItem(int position) {            return mFragments.get(position);        }        @Override        public int getCount() {            return mFragments.size();        }        @Override        public CharSequence getPageTitle(int position) {            return mFragmentTitles.get(position);        }    }

值得学习的是Adapter的写法,它将mFragments和mFragmentTitles两个List整合到了Adapter内部。还有要注意Fragment中嵌套使用Fragment一定要使用getChildFragmentManager(),接下来看其子项Fragment。

3.3 NewsListFragment

3.3.1 布局

布局很简单,一个SwipeRefreshLayout包裹RecyclerView。

3.3.2 初始化视图

    @Nullable    @Override    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {        View view = inflater.inflate(R.layout.fragment_newslist, null);        mSwipeRefreshWidget = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh_widget);        mSwipeRefreshWidget.setColorSchemeResources(R.color.primary,                R.color.primary_dark, R.color.primary_light,                R.color.accent);        mSwipeRefreshWidget.setOnRefreshListener(this);        mRecyclerView = (RecyclerView)view.findViewById(R.id.recycle_view);        mRecyclerView.setHasFixedSize(true);        mLayoutManager = new LinearLayoutManager(getActivity());        mRecyclerView.setLayoutManager(mLayoutManager);        mRecyclerView.setItemAnimator(new DefaultItemAnimator());        mAdapter = new NewsAdapter(getActivity().getApplicationContext());        mAdapter.setOnItemClickListener(mOnItemClickListener);        mRecyclerView.setAdapter(mAdapter);        mRecyclerView.addOnScrollListener(mOnScrollListener);        onRefresh();        return view;    }

值得提的是mSwipeRefreshWidget.setColorSchemeResources()方法可以设置刷新等待条的颜色;mRecyclerView.setItemAnimator()可以设置增加卡片动画。

3.3.3 点击事件

        @Override        public void onItemClick(View view, int position) {            NewsBean news = mAdapter.getItem(position);            Intent intent = new Intent(getActivity(), NewsDetailActivity.class);            intent.putExtra("news", news);            View transitionView = view.findViewById(R.id.ivNews);            ActivityOptionsCompat options =                    ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(),                            transitionView, getString(R.string.transition_news_img));            ActivityCompat.startActivity(getActivity(), intent, options.toBundle());        }

点击时,通过带有NewsBean参数的Intent启动新闻详情Activity,此外,在跳转页面的同时会有一个动画,通过以上代码可以实现动画。具体流程是先取得CardView中的ImageView,然后通过ActivityOptionsCompat makeSceneTransitionAnimation()方法取得过渡动画参数,并加在startActivity中。

3.3.4 上拉加载实现

上拉加载更多的实现主要有两个关键部分,一个是滚动事件的监听,另一个是Adapter内的视图创建。

private RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {    private int lastVisibleItem;    @Override    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {        super.onScrolled(recyclerView, dx, dy);        lastVisibleItem = mLayoutManager.findLastVisibleItemPosition();    }    @Override    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {        super.onScrollStateChanged(recyclerView, newState);        if (newState == RecyclerView.SCROLL_STATE_IDLE                && lastVisibleItem + 1 == mAdapter.getItemCount()                && mAdapter.isShowFooter()) {            //加载更多            LogUtils.d(TAG, "loading more data");            mNewsPresenter.loadNews(mType, pageIndex + Urls.PAZE_SIZE);        }    }};

滚动监听中判断三个条件:是否处于滚动暂停状态、当前页面的最后一个条目是否为所有信息中的最后一个条目、是否不处于正在加载新的条目状态。三个条件同时满足的情况下加载新条目。加载完新条目后又会调用mAdapter.isShowFooter(true)。

@Overridepublic int getItemViewType(int position) {    // 最后一个item设置为footerView    if(!mShowFooter) {        return TYPE_ITEM;    }    if (position + 1 == getItemCount()) {        return TYPE_FOOTER;    } else {        return TYPE_ITEM;    }}@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,                                                  int viewType) {    if(viewType == TYPE_ITEM) {        View v = LayoutInflater.from(parent.getContext())                .inflate(R.layout.item_news, parent, false);        ItemViewHolder vh = new ItemViewHolder(v);        return vh;    } else {        View view = LayoutInflater.from(parent.getContext()).inflate(                R.layout.footer, null);        view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,                ViewGroup.LayoutParams.WRAP_CONTENT));        return new FooterViewHolder(view);    }}

onCreateViewHolder()方法是在每个条目创建时都会调用的方法,它用来填充视图,所以需要在这里进行选择需要创建的视图类型,这样便实现了上拉加载。

3.3.5 NewsAdapter

NewsAdapter其实在上文中有所提及,这里再进行一些补充。

    public class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {        public TextView mTitle;        public TextView mDesc;        public ImageView mNewsImg;        public ItemViewHolder(View v) {            super(v);            mTitle = (TextView) v.findViewById(R.id.tvTitle);            mDesc = (TextView) v.findViewById(R.id.tvDesc);            mNewsImg = (ImageView) v.findViewById(R.id.ivNews);            v.setOnClickListener(this);        }        @Override        public void onClick(View view) {            if(mOnItemClickListener != null) {                mOnItemClickListener.onItemClick(view, this.getPosition());            }        }    }

首先是内部类ItemViewHolder,在这里有两个细节值得注意:一个是它的成员变量到初始化通过传入构造函数的View就实现了,不需要将每个参数都传入,二是为每个条目的点击事件设立了依赖注入,使其解耦。

@Overridepublic void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {    if(holder instanceof ItemViewHolder) {        NewsBean news = mData.get(position);        if(news == null) {            return;        }        ((ItemViewHolder) holder).mTitle.setText(news.getTitle());        ((ItemViewHolder) holder).mDesc.setText(news.getDigest());        ImageLoaderUtils.display(mContext, ((ItemViewHolder) holder).mNewsImg, news.getImgsrc());    }}public static void display(Context context, ImageView imageView, String url) {    if(imageView == null) {        throw new IllegalArgumentException("argument error");    }    Glide.with(context).load(url).placeholder(R.drawable.ic_image_loading)            .error(R.drawable.ic_image_loadfail).crossFade().into(imageView);}

在每个子条目的内容设置中,调用了Glide图片加载库进行图片的加载。关于Glide的使用读者可以参考这篇博客: Google推荐的图片加载库Glide介绍

3.3.6 显示失败消息

@Overridepublic void showLoadFailMsg() {    if(pageIndex == 0) {        mAdapter.isShowFooter(false);        mAdapter.notifyDataSetChanged();    }    View view = getActivity() == null ? mRecyclerView.getRootView() : getActivity().findViewById(R.id.drawer_layout);    Snackbar.make(view, getString(R.string.load_fail), Snackbar.LENGTH_SHORT).show();}

调用Snackbar显示消息即可。

4. 新闻详情Activity

4.1 界面布局

布局页面

<?xml version="1.0" encoding="utf-8"?><android.support.design.widget.CoordinatorLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:fitsSystemWindows="true">    <android.support.design.widget.AppBarLayout        android:layout_width="match_parent"        android:layout_height="256dp"        android:fitsSystemWindows="true"        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">        <android.support.design.widget.CollapsingToolbarLayout            android:id="@+id/collapsing_toolbar"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:fitsSystemWindows="true"            app:contentScrim="?attr/colorPrimary"            app:expandedTitleMarginEnd="64dp"            app:expandedTitleMarginStart="48dp"            app:layout_scrollFlags="scroll|exitUntilCollapsed">            <ImageView                android:id="@+id/ivImage"                android:layout_width="match_parent"                android:layout_height="match_parent"                android:fitsSystemWindows="true"                android:scaleType="centerCrop"                android:transitionName="@string/transition_news_img"                app:layout_collapseMode="parallax"                app:layout_collapseParallaxMultiplier="0.7"/>            <android.support.v7.widget.Toolbar                android:id="@+id/toolbar"                android:layout_width="match_parent"                android:layout_height="?attr/actionBarSize"                app:layout_collapseMode="pin"                app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>        </android.support.design.widget.CollapsingToolbarLayout>    </android.support.design.widget.AppBarLayout>    <android.support.v4.widget.NestedScrollView        android:layout_width="match_parent"        android:layout_height="match_parent"        app:layout_behavior="@string/appbar_scrolling_view_behavior">        <LinearLayout            android:layout_width="match_parent"            android:layout_height="match_parent"            android:orientation="vertical">            <ProgressBar                android:id="@+id/progress"                style="?android:attr/progressBarStyle"                android:layout_width="wrap_content"                android:layout_height="wrap_content"                android:layout_gravity="center"/>            <org.sufficientlysecure.htmltextview.HtmlTextView                android:id="@+id/htNewsContent"                android:layout_width="match_parent"                android:layout_height="match_parent"                android:padding="12dp"                android:textAppearance="@android:style/TextAppearance.Medium"/>        </LinearLayout>    </android.support.v4.widget.NestedScrollView></android.support.design.widget.CoordinatorLayout>

这个页面布局代码全部都粘贴过来了,大家可想而知这个布局的重要性,让我们来细嚼一下这段布局代码。

4.1.1 CollapsingToolbarLayout

CollapsingToolbarLayout作用是提供了一个可以折叠的Toolbar,它继承至FrameLayout,给它设置layout_scrollFlags,它可以控制包含在CollapsingToolbarLayout中的控件(如:ImageView、Toolbar)在响应layout_behavior事件时作出相应的scrollFlags滚动事件(移除屏幕或固定在屏幕顶端)。
app相关属性介绍:

  1. 在CollapasingToolbarLayout 的属性:

    • app:contentScrim=”?attr/colorPrimary” — 设置此属性生,CollapsingToolbarLayout完成折叠动画后,Title部分会显示一个普通的颜色,代码中的颜色来自于style文件中的colorPrimary属性
    • app:expandedTitleMarginStart=”48dp” — 控制文本的边距
    • app:expandedTitleMarginEnd=”64dp” — 控制文本的边距
    • app:layout_scrollFlags —设置CollapsingToolbarLayout滚动折叠,关于这个属性需要详细解说,请看以下内容:

    • scroll -想要滚动就必须设置这个标记
    • exitUntilCollapsed -向上滚动收缩View,可以一直固定在ToolBar上面
    • enterAlwaysCollapsed -当你的View已经设置minHeight属性又使用此标志时,你的View只能以最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。
  2. 在ImageView控件中属性:

    • app:layout_collapseMode — 折叠模式 有俩个值

      • pin —设置这个模式时,当CollapsingToolbarLayout完全收缩后,ImageView显示的内容系统自己决定
      • parallax –设置这个模式时,当CollapsingToolbalLayout完全收缩后,ImageView显示的内容可以通过设置layout_collapseParallaxMultiplier来决定显示图片的哪部分内容
    • app:layout_collapseParallaxMultiplier=”0.7” 设置滚动视差,值为0~1。注意这个属性需要layout_collapseMode开启parallax模式后才会有作用,它决定CollapsingToolbarLayout完全折叠显示的内容

  3. CollapsingToolbarLayout配置完成之后,它下面的Layout必须设置layout_behavior属性来响应CollapsingToolbarLayout,如果没有配置layout_behavio,CollapsingToolbarLayout将没有折叠效果。

    注意:app:layout_behavior=”@string/appbar_scrolling_view_behavior”如果没有此属性那么CollapsingToolbarLayout将不会有折叠效果。

4.1.2 NestedScrollView

Android 在发布 Lollipop版本之后,为了更好的用户体验,Google为Android的滑动机制提供了NestedScrolling特性,这便是NestedScrollView,没什么多讲的,感兴趣的读者可以自行研究。

4.1.3 HtmlTextView

HtmlTextView是github上的一个开源框架,它是Android TextView控件的一个扩展,可以加载的HTML并将其转换成Spannable用于显示它。这是WebView组件的一个替代。

4.2 视图相关

@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_news_detail);    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);    mProgressBar = (ProgressBar) findViewById(R.id.progress);    mTVNewsContent = (HtmlTextView) findViewById(R.id.htNewsContent);    setSupportActionBar(toolbar);    getSupportActionBar().setDisplayHomeAsUpEnabled(true);    toolbar.setNavigationOnClickListener(new View.OnClickListener() {        @Override        public void onClick(View view) {            onBackPressed();        }    });    mSwipeBackLayout = getSwipeBackLayout();    mSwipeBackLayout.setEdgeSize(ToolsUtil.getWidthInPx(this));    mSwipeBackLayout.setEdgeTrackingEnabled(SwipeBackLayout.EDGE_LEFT);    mNews = (NewsBean) getIntent().getSerializableExtra("news");    CollapsingToolbarLayout collapsingToolbar = (CollapsingToolbarLayout) findViewById(R.id.collapsing_toolbar);    collapsingToolbar.setTitle(mNews.getTitle());    ImageLoaderUtils.display(getApplicationContext(), (ImageView) findViewById(R.id.ivImage), mNews.getImgsrc());    mNewsDetailPresenter = new NewsDetailPresenterImpl(getApplication(), this);    mNewsDetailPresenter.loadNewsDetail(mNews.getDocid());}@Overridepublic void showNewsDetialContent(String newsDetailContent) {    mTVNewsContent.setHtmlFromString(newsDetailContent, new HtmlTextView.LocalImageGetter());}

首先值得一提的是SwipeBackLayout,这是一个滑动返回库,使用方法非常简单:
1. 继承SwipeBackActivity
2. mKeyTrackingMode = getString(R.string.key_tracking_mode);
3. mSwipeBackLayout = getSwipeBackLayout();
4. mSwipeBackLayout.setEdgeTrackingEnabled(edgeFlag);
5. saveTrackingMode(edgeFlag);

新闻详情界面

还有注意的是,在使用CollapsingToolbarLayout时,设置Toolbar标题要调用collapsingToolbar.setTitle(),而不是Toolbar的set方法,另外,笔者暂时还未发现如何为展开的Toolbar和折叠的Toolbar设置两个不同的标题,不过可以通过collapsingToolbar.setCollapsedTitleTextColor和collapsingToolbar.setExpandedTitleColor设置为不同颜色,并且底层自动处理颜色的过渡与渐变。

5. 新闻业务处理

/** * 加载新闻详情 * @param docid * @param listener */@Overridepublic void loadNewsDetail(final String docid, final OnLoadNewsDetailListener listener) {    String url = getDetailUrl(docid);    OkHttpUtils.ResultCallback<String> loadNewsCallback = new OkHttpUtils.ResultCallback<String>() {        @Override        public void onSuccess(String response) {            NewsDetailBean newsDetailBean = NewsJsonUtils.readJsonNewsDetailBeans(response, docid);            listener.onSuccess(newsDetailBean);        }        @Override        public void onFailure(Exception e) {            listener.onFailure("load news detail info failure.", e);        }    };    OkHttpUtils.get(url, loadNewsCallback);}

业务层主要包括网络请求和Json数据处理,这些主要通过框架来实现,由于此应用使用的框架在当今不太火热,所以就不作具体分析了。

6. 天气界面

天气界面
天气界面主要看的就是布局和Json解析,思路容易理解,就不作分析了。

@Overridepublic void loadWeatherData(String cityName, final LoadWeatherListener listener) {    try {        String url = Urls.WEATHER + URLEncoder.encode(cityName, "utf-8");        OkHttpUtils.ResultCallback<String> callback = new OkHttpUtils.ResultCallback<String>() {            @Override            public void onSuccess(String response) {                List<WeatherBean> lists = WeatherJsonUtils.getWeatherInfo(response);                listener.onSuccess(lists);            }            @Override            public void onFailure(Exception e) {                listener.onFailure("load weather data failure.", e);            }        };        OkHttpUtils.get(url, callback);    } catch (UnsupportedEncodingException e) {        LogUtils.e(TAG, "url encode error.", e);    }}

总结

至此,《简易新闻》的代码分析就基本结束了,回顾全文,其实学到的不仅是Material的UI,还包括整个App的架构——MVP模式,该架构体现在每个模块,在代码方面,每个包以功能区分,在抽象方面每个模块又以MVP区分。最后,感谢读者的耐心阅读和App作者的无私奉献,然后祝大家学习进步!

参考

  1. android 4.2的新特性layoutRtl,让布局自动从右往左显示
  2. Google推荐的图片加载库Glide介绍
  3. [Android] 可以折叠的CollapsingToolbarLayout
  4. Android5.0+(CollapsingToolbarLayout)
  5. 支持加载Html内容的TextView:HtmlTextView for Android
0 0
原创粉丝点击