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
@Override
public 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 切换
@Override
public 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所选标签标志颜色,一张图看懂,此处改为蓝色
- 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)。
@Override
public int getItemViewType(int position) {
// 最后一个item设置为footerView
if(!mShowFooter) {
return TYPE_ITEM;
}
if (position + 1 == getItemCount()) {
return TYPE_FOOTER;
} else {
return TYPE_ITEM;
}
}
@Override
public 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就实现了,不需要将每个参数都传入,二是为每个条目的点击事件设立了依赖注入,使其解耦。
@Override
public 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 显示失败消息
@Override
public 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相关属性介绍:
- 在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只能以最小高度进入,只有当滚动视图到达顶部时才扩大到完整高度。
- 在ImageView控件中属性:
- app:layout_collapseMode — 折叠模式 有俩个值
- pin —设置这个模式时,当CollapsingToolbarLayout完全收缩后,ImageView显示的内容系统自己决定
- parallax –设置这个模式时,当CollapsingToolbalLayout完全收缩后,ImageView显示的内容可以通过设置layout_collapseParallaxMultiplier来决定显示图片的哪部分内容
- app:layout_collapseParallaxMultiplier=”0.7” 设置滚动视差,值为0~1。注意这个属性需要layout_collapseMode开启parallax模式后才会有作用,它决定CollapsingToolbarLayout完全折叠显示的内容
- app:layout_collapseMode — 折叠模式 有俩个值
- 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 视图相关
@Override
protected 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());
}
@Override
public 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
*/
@Override
public 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解析,思路容易理解,就不作分析了。
@Override
public 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作者的无私奉献,然后祝大家学习进步!
参考
- android 4.2的新特性layoutRtl,让布局自动从右往左显示
- Google推荐的图片加载库Glide介绍
- [Android] 可以折叠的CollapsingToolbarLayout
- Android5.0+(CollapsingToolbarLayout)
- 支持加载Html内容的TextView:HtmlTextView for Android