参考:Android架构探索
RxBus
相信大家也都用过EventBus, Otto等开源库, 利用RxJava也能很简单的实现类似功能而无需引入其他库.
发送:
RxBus.getDefault().send(new Object, EVENT_STEP_CHANGE);
接收:
RxBus.getDefault().toObservable(Object.class, EVENT_STEP_CHANGE)
.compose(this.<Object>bindToLifecycle())
.subscribe(new RxBusSubscriber<Object>() {
@Override
public void receive(Object data) {
}
});
这里我们同样可以使用bindToLifeCycle()方法来将Observable绑定至Activity/Fragment的生命周期, 自动的在合适的时候取消订阅. 也能使用subscribeOn()与observeOn()方法做线程调度. 为了能很方便的接受事件, 而无需全部实现Subscriber的三个方法, 同样定义一个自己的RxBusSubscriber类
ToastMgr
Android原生的Toast虽说不复杂, 但也不是很方便, 如果需要更换Toast的背景, 则需要写不少重复代码. 本着遵循DRY的原则, 理当将Toast封装一下。
DRY 是指Don't Repeat Yourself (摘自wikipedia),特指在程序设计以及计算中避免重复代码,因为这样会降低灵活性、简洁性,并且可能导致代码之间的矛盾,DRY是Andy Hunt 和 Dave Thomas's 的《 The Pragmatic Programmer 》书中的核心原则。
ToastMgr持有一个Toast的单例, 并且通过Toast.makeText(_context, "", Toast.LENGTH_SHORT).getView()来获取系统默认的吐司背景. 也可通过setBackgroundView(View view)来给Toast设置自定义的背景. 默认情况下只需通过以下方式调用:
ToastMgr.show("toast");
但是需要在程序初始化的时候调用如下方法, 建议使用ApplicationContext:
ToastMgr.init(getApplicationContext());
通过ToastMgr产生的Toast与系统自带的Toast有一个最大的区别就是, 不论前面有多少Toast, 都会被最后一个Toast覆盖, 然后2秒后消失. 原生自带的则是一个接一个的显示. 因为ToastMgr中的是Toast单例, 而原生的不是.
Adapter
单布局
ListView/GridView + Adapter可以说是Android程序员最常用的组件了. 我们应该尽量简化Adapter的代码以提高开发效率. 首先来看看不做任何封装的Adapter需要哪些步骤:
- 继承自BaseAdpter
- 维护一个List数据并实现4个抽象方法
- 声明一个ViewHolder
- 在getView中判断convertView是否为空, 为空则inflate一个布局, 初始化ViewHolder, 初始化控件, 并将ViewHolder通过setTag设置到convertView中.
- 如果不为空则通过getTag将ViewHolder取出来
- 为控件设置值
下面的内容详细可以参考 打造万能的ListView GridView 适配器
虽然有了ViewHolder能减少很多代码量, 但是这不是极限, 还可以继续减少. 比如内部维护的List数据, 另外三个抽象方法等都是可以不用写的. 这里我们引用GitHub上的一个开源工具 base adapter helper. 现在来看看如何使用base adapter helper:
public class HomeAdapter extends QuickAdapter<Attraction> {
public HomeAdapter(Context context, int layoutResId) {
super(context, layoutResId);
}
@Override
protected void convert(BaseAdapterHelper helper, Attraction item) {
helper.setText(R.id.tv_name, item.name)
.setText(R.id.tv_des, item.profile)
}
}
可以调用如下方法修改Adapter的数据
homeAdapter.add(data);
homeAdapter.addAll(list);
homeAdapter.removeAll();
homeAdapter.remove(0);
homeAdapter.set(0, data);
...
base adapter helper的原理在此处就不赘述了, 参考 Android base-adapter-helper 源码分析与扩展. 使用base adapter helper可以减少大量重复的代码, 也可以通过扩展BaseAdapterHelper以适应自己的编程习惯.
以上就是对Android适配器的简化及优化. 但是base adapter helper使用虽然简单, 也还是有不足的地方. 比如不支持多布局, 只支持BaseAdapter, 不支持其他类型比如ViewPager的Adapter等.
以上就是单布局的基本原理与使用方法. 单布局是APP中最常用的, 下面介绍不是很常见的多布局的封装.
多布局
多布局是指一个适配器中使用一个以上的布局文件. 主要是依赖于覆写BaseAdapter
中的getViewTypeCount
与getItemViewType
方法. 多布局使用了代理, 将convert方法代理给多个代理类去实现, 有兴趣的可以看这里, 这里只讲使用方法. 直接上代码:
final QuickMultiAdapter<Integer> adapter = new QuickMultiAdapter<>(this);
adapter.addItemViewDelegate(new LeftDelegate());
adapter.addItemViewDelegate(new RightDelegate());
listView.setAdapter(adapter);
adapter.replaceAll(getData());
class LeftDelegate implements ItemViewDelegate<Integer> {
@Override public int getItemViewLayoutId() {
return R.layout.i_text;
}
@Override public boolean isForViewType(Integer item, int position) {
return item % 2 == 0;
}
@Override public void convert(BaseAdapterHelper helper, Integer item, int position) {
helper.setText(R.id.text, item + "");
}
}
class RightDelegate implements ItemViewDelegate<Integer> {
@Override public int getItemViewLayoutId() {
return R.layout.i_text_right;
}
@Override public boolean isForViewType(Integer item, int position) {
return item % 2 != 0;
}
@Override public void convert(BaseAdapterHelper helper, Integer item, int position) {
helper.setText(R.id.text, item + "");
}
}
效果:
两个布局的差别就是一个TextView的gravity是left, 同时有margin值, 另一个的gravity为right, 没有margin值. 如果是双数则显示gravity是left的TextView, 否则显示gravity是right的TextView. 代码也比较好理解, 接下来看看Delegate接口的声明以及每个方法的含义:
/**
* Adapter中多布局代理
* @param <T> 数据源类型
* @param <H> ViewHolder类型
*/
public interface BaseItemViewDelegate<T, H extends BaseAdapterHelper> {
/** 布局资源id **/
int getItemViewLayoutId();
/** 判断该position是否要加载此类型的布局 **/
boolean isForViewType(T item, int position);
/**
* 当需要条目将被展示到界面上时, 通过此方法适配界面
* @param helper ViewHolder
* @param item 数据
* @param position 位置
*/
void convert(H helper, T item, int position);
}
ItemViewDelegate继承了BaseItemViewDelegate, 同时声明H为BaseAdapterHelper, 所以在不需要自定义AdapterHelper的情况下, 建议直接使用ItemViewDelegate:
/**
* ViewHolder类型为BaseAdapterHelper的快捷代理接口
*/
public interface ItemViewDelegate<T> extends BaseItemViewDelegate<T, BaseAdapterHelper> {}
RecyclerView
使用方法与ListView/GridView一样, 只不过继承从QuickAdapter变成RecyclerAdapter. 多布局也一样, 区别只是从QuickMultiAdapter换成RecyclerMultiAdapter.
多布局例子点这里
SharedPreferences
Android自带操作SharedPreferences的API个人觉得很麻烦, 也一直在想办法去改进. 最初的封装很简单也很直接, 通过简化实例化SharedPreferences以及Editor的代码, 提供一个saveString与getString的公共方法以供外部调用. 这样可以比较方便的存取字符串. 但是这种方式只能存字符串, 其他类型都要转换成字符串再存起来. 取出来也需要转换一遍. 并且每一个字符串都需要额外的去维护与之对应的键名, 因此还是不怎么方便.
设想一下, 如果需要将一个对象都存入SharedPreferences中, 用之前的方法, 则对象的属性越多, 需要维护的键就越多. 存与取的代码量会非常大.
这里可能会存在疑惑, 为何要在SharedPreferences里存取对象, 为何不使用数据库, 或者是文件? 首先, 数据库太重, 如果不是不得已的情况下还是不要用数据库的好. 文件与SharedPreferences比起来, 文件可以存更大的数据, 但是对象一般都是比较轻量的数据, 轻量的数据还是建议使用SharedPreferences来存取会方便一些. 试想一下, 登录之后我们需要缓存一些用户的数据, 如用户名, id, 头像之类的数据. 这些数据如果以User对象的形式存起来, 取的时候能直接返回User对象, 这样是不是会很方便?
要实现上述想法, 有两种办法:
- 反射
- 序列化
反射
SharedPreferencesClassHelper.init(context);
SharedPreferencesClassHelper.getInstance().saveData(user);
User user = SharedPreferencesClassHelper.getInstance().getData(User.class);
已经被废弃,原因看下面
序列化
通过自己写反射的实现方式有明显的缺陷, 首先目前只支持了4种类型int, long, boolean, String, 如果要扩展只能修改SharedPreferencesHolder的getData与setData方法. 其次, 每一个类对应一个文件比较浪费资源, 同时效率也不高. 最后SharedPreferencesClassHelper会缓存使用过的SharedPreferencesHolder对象, 内存有一定开销.
因此上述代码已经被废弃了, 接下来来实现第二种方式, 通过序列化来存储对象. 其主要思想是将对象转换成JSON字符串存入SharedPreferences中, 取对象的时候再对JSON做一次转换. 第三方序列化库有挺多, 但是由于CoreLibs里本身使用了GSON, 因此此处也选用GSON来对对象进行转换.
使用方法:
PreferencesHelper.init(getApplicationContext); // 此处一定要使用ApplicationContext
PreferencesHelper.saveData(user);
User user = PreferencesHelper.getData(User.class);
使用起来要方便多了, 建议不要使用PreferencesHelper存过于复杂的对象, 也不要存带有Bitmap或其他复杂属性的对象, 仅仅存储一些简单的, 由基本类型构成的实体类.
下拉刷新与自动加载
下拉刷新是Android App开发中非常常用的功能, 网上也有很多开源的下拉刷新控件. CoreLibs中原先使用的handmark pulltorefresh, 现在选用的则是 Ultra-Pull-To-Refresh. 新的PTR框架有如下优点:
- 轻量
- 理论上支持所有的View
- 易扩展
- 易自定义
- 性能不错
具体的使用方法请参考上面的链接, 这里就不再赘述了. 但是呢, Ultra ptr也不是没有缺点, 比如库中提供了Lollipop风格的下拉头部, 如果想要在每一个下拉控件中使用还需要加入不少代码. 然后每次使用下拉组件的时候需要一些相似的配置代码. 最主要的是Ultra ptr只支持下拉, 而不支持加载更多. 因此我们需要扩展一下这个库, 目标有两个:
- 默认头部变为Lollipop风格, 去掉重复代码, 使用更简洁
- 加入自动加载更多 - auto load more.
ptr的扩展类均位于com.corelibs.views.ptr下. 以下是包结构:
| ptr
| layout 扩展的布局
-PtrAutoLoadMoreLayout //自动加载更多布局
-PtrLollipopLayout //Lolipop头部风格布局
| loadmore
| adapter
-GridViewAdapter //GridView系列适配器
-ListViewAdapter //ListView系列适配器
-LoadMoreAdapter //自动加载更多的适配类
-RecyclerViewAdapter //RecyclerView系列适配器
| widget
-AutoLoadMoreGridView //自动加载更多的GridView
-AutoLoadMoreListView //自动加载更多的ListView
-AutoLoadMoreSwipeMenuListView //自动加载更多的带侧滑菜单的ListView
-AutoLoadMoreRecyclerView //自动加载更多的RecyclerView
-AutoLoadMoreHandler //自动加载更多的真正处理类
-AutoLoadMoreHook //PtrAutoLoadMoreLayout的child需实现此类以供PtrAutoLoadMoreLayout获取AutoLoadMoreHandler
-OnScrollListener //兼容AdapterView与RecyclerView的OnScrollListener
layout.PtrLollipopLayout
<com.corelibs.views.ptr.layout.PtrLollipopLayout
android:id="@+id/ptrLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="30sp"
android:text="拉我拉我"/>
</com.corelibs.views.ptr.layout.PtrLollipopLayout>
Activity代码:
@Bind(R.id.ptrLayout) PtrLollipopLayout<TextView> ptrLayout;
ptrLayout.setRefreshCallback(new PtrLollipopLayout.RefreshCallback() {
@Override
public void onRefreshing(PtrFrameLayout frame) {
ptrLayout.getPtrView().setText("我被刷了");
ptrLayout.complete();
}
});
用法非常简单, 只需使用PtrLollipopLayout包裹任意你想要刷新的控件即可. 在代码中, 如果在声明PtrLollipopLayout时加上泛型, 如PtrLollipopLayout<TextView>, 就可以使用ptrLayout.getPtrView()将PtrLollipopLayout内的TextView取出. 如果不加泛型, 则需要单独为TextView设置id, 并使用ButterKnife bind出来. 两种方式均可.
PtrLollipopLayout内部默认使用了Lollipop风格的下拉头部, 并且做了一些配置工作. 我们同样可以在代码中为PtrLollipopLayout做一些个性化的配置, 如通过setHeaderView(View header)设置自己的头部, 请注意, 自定义的头部必须实现PtrUIHandler接口. 如果出现PtrLollipopLayout解决不了的滑动冲突, 可以调用setPtrHandler(PtrHandler ptrHandler)自行处理滑动.
以下是几个需要注意的点:
- 此控件只能包含一个子View.
- 此控件仅支持下拉刷新, 如果需要自动加载, 请使用PtrAutoLoadMoreLayout
- 如果出现横向滑动冲突, 请设置disableWhenHorizontalMove(boolean)为true.
- 如果不想为child设置id并使用findViewById取出, 可以在声明PtrLollipopLayout的时候带上child类型的泛型, 然后就可以使用getPtrView()取出child. 如PtrLollipopLayout<ScrollView>.
- 刷新完成或加载完成后请调用complete().
layout.PtrAutoLoadMoreLayout
接下来是第二个目标 - 自动加载更多. 先看例子:
<com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout
android:id="@+id/ptrLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.corelibs.views.ptr.loadmore.widget.AutoLoadMoreListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:listSelector="#00000000"
android:divider="#aaa"
android:dividerHeight="1dp"/>
</com.corelibs.views.ptr.layout.PtrAutoLoadMoreLayout>
Activity代码:
@Bind(R.id.ptrLayout) PtrAutoLoadMoreLayout<AutoLoadMoreListView> ptrLayout;
protected void init(Bundle savedInstanceState) {
ptrLayout.setLoadingBackgroundColor(0xff333333); // 设置自动加载视图的背景颜色
adapter = new MyListAdapter(context);
listView.setAdapter(adapter); // 为AutoLoadMoreListView设置Adapter
adapter.addAll(getData()); // 为adapter添加数据
// 设置刷新和加载回调
ptrLayout.setRefreshLoadCallback(new PtrAutoLoadMoreLayout.RefreshLoadCallback() {
@Override
public void onRefreshing(PtrFrameLayout frame) {
adapter.replaceAll(getData()); // 替换adapter中的数据
ptrLayout.enableLoading(); // 重新启用自动加载
ptrLayout.complete(); // 刷新完成
}
@Override
public void onLoading(PtrFrameLayout frame) {
count++; // 模拟页数++
handler.postDelayed(new Runnable() { // 模拟网络加载延迟
@Override public void run() {
adapter.addAll(getData()); // 将数据加入adapter中.
ptrLayout.complete(); // 加载完成
if (count > 2)
ptrLayout.disableLoading(); // 禁用自动加载
}
}, 1500);
}
});
}
使用带自动加载的下拉刷新就要比单纯的下拉刷新复杂的多. 这种时候就不能使用PtrLollipopLayout而需要使用PtrAutoLoadMoreLayout. 一般情况下, 自动加载更多只会出现在有ListView/GridView的情况下. 因此PtrAutoLoadMoreLayout的子视图基本都是ListView/GridView, 或他们的派生类.
但是如果直接使用PtrAutoLoadMoreLayout加上ListView/GridView, 也是无法实现自动加载的。
不仅没有效果, 还会报如下错误:
java.lang.IllegalStateException: PtrAutoLoadMoreLayout child should implement AutoLoadMoreHook
这是因为PtrAutoLoadMoreLayout只是一个外壳, 本身只带有下拉刷新的功能, 不带有自动加载的功能. PtrAutoLoadMoreLayout所有有关自动加载的api全部是代理至另外一个类 - AutoLoadMoreHandler. AutoLoadMoreHandler才是真正处理自动加载功能的类. PtrAutoLoadMoreLayout需要借助AutoLoadMoreHook来获取AutoLoadMoreHandler, 因此PtrAutoLoadMoreLayout的子控件必须实现AutoLoadMoreHook.
现在PtrAutoLoadMoreLayout就可以通过getLoadMoreHandler来获取AutoLoadMoreHandler实现自动加载更多的功能了. 那么, 例子中的AutoLoadMoreListView又是什么鬼? AutoLoadMoreListView是CoreLibs中预定义好的一个控件, 它实现了AutoLoadMoreHook. 如果我们需要一个带自动加载的ListView, 就可以使用AutoLoadMoreListView. AutoLoadMoreListView全部代码:
public class AutoLoadMoreListView extends ListView implements AutoLoadMoreHook {
public AutoLoadMoreListView(Context context) {
super(context);
}
public AutoLoadMoreListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AutoLoadMoreListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public AutoLoadMoreHandler getLoadMoreHandler() {
return new AutoLoadMoreHandler<>(getContext(), new ListViewAdapter<ListView>(this));
}
}
AutoLoadMoreListView的代码非常简单, 除了三个继承自ListView需要实现的构造函数外, 就只有一个实现了AutoLoadMoreHook的getLoadMoreHandler方法. 该方法中也只是new了一个AutoLoadMoreHandler对象并返回而已.
如果我们有一个自定义的ListView, 实现了侧滑菜单, 名字叫SwipeMenuListView, 我们想要为SwipeMenuListView加上下拉和自动加载怎么办? 很简单, 定义一个新的继承自SwipeMenuListView, 并且实现了AutoLoadMoreHook的控件, 然后我们在PtrAutoLoadMoreLayout中包含该控件即可. 使用方法除了SwipeMenuListView自己的API外, 其他与默认的ListView完全一样.
以下是使用PtrAutoLoadMoreLayout需要注意的几个地方:
- 如果只需下拉刷新功能, 请使用PtrLollipopLayout
- 此控件中的child view必须实现AutoLoadMoreHook
- 此控件是对AutoLoadMoreHandler功能的转发. AutoLoadMoreHook的getLoadMoreHandler()需要的就是AutoLoadMoreHandler.
- 刷新完成或加载完成后请调用complete(), 而不是refreshComplete()或loadingFinished().
总结
下面总结一下PtrLollipopLayout与PtrAutoLoadMoreLayout在使用上的相同点与区别:
- 只有下拉刷新功能时应该使用PtrLollipopLayout, 带有自动加载更多时应该使用PtrAutoLoadMoreLayout.
- 两者都应该使用complete()方法来结束刷新或者加载状态.
- PtrLollipopLayout使用setRefreshCallback(RefreshCallback callback)来设置回调.
- PtrAutoLoadMoreLayout使用setRefreshLoadCallback(RefreshLoadCallback callback)来设置回调.
- RefreshLoadCallback继承自RefreshCallback, 比RefreshCallback多了onLoading方法.
- RefreshCallback定义在PtrLollipopLayout内, RefreshLoadCallback定义在PtrAutoLoadMoreLayout内.
- PtrLollipopLayout与PtrAutoLoadMoreLayout都只能包含一个子视图.
- PtrLollipopLayout子视图可以是任意View, 但是PtrAutoLoadMoreLayout的子视图必须实现AutoLoadMoreHook接口.
状态栏适配
由于不同版本的系统的原因,很多时候我们需要对状态栏做不同的支持方案,主要是针对4.4,5.0以及6.0三种版本的系统。这里统一的使用的一种适配方案,避免自己写复杂的判断逻辑。主要原理如下:
由于不同版本的系统的原因,很多时候我们需要对状态栏做不同的支持方案,主要是针对4.4,5.0以及6.0三种版本的系统。这里统一的使用的一种适配方案,避免自己写复杂的判断逻辑。主要原理如下:
- 将Activity设置为全屏模式,使Activity能渗透到状态栏下,可以通过xml或者代码设置
- 将状态栏设置为透明色
- 自定义标题栏(可使用ToolBar,但考虑到项目里的标题栏完全不符合Android规范,因此使用的是完全自定义的标题栏),在标题栏上方多增加一块区域,刚好和状态栏大小一致
- 设置标题栏的额外区域的颜色,以达到另辟蹊径地改变状态栏地颜色
- 最后是需要解决一个manifest中的windowSoftInputMode属性在全屏模式下的bug
下面我们跟着这个思路一步步看如何去做状态栏适配:
- 在manifest中的application节点里,设置theme属性为corelibs的AppBaseCompactTheme
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppBaseCompactTheme">
- 在BaseActivity的OnCreate里增加全屏以及透明状态栏的代码(默认已设置好,无需额外代码):
@Override
protected void onCreate(Bundle savedInstanceState) {
...
setTranslucentStatusBar();
...
}
- 自定义一个标题栏(NavBar),继承自com.corelibs.views.navigation.TranslucentNavBar。然后覆写三个构造函数(注意,两个参数的一定要覆写)以及两个抽象函数getLayoutId和initView。这里可以直接使用ButterKnife的注解。接着就可以调用一系列设置颜色以及背景图片的方法来达到适配状态栏的效果。示例:
- 将NavBar放到布局文件里即可,注意根节点不能使用fitSystemWindows=“true”。
- 如果Activity或者Fragment中有使用EditText,并且windowSoftInputMode是adjustResize的情况下,软键盘弹出不会Resize布局。要解决这个问题需要在Activity或者Fragment添加一行代码即可:
AndroidBug5497Workaround.assistActivity(activity);
扩展
- TranslucentNavBar具体有哪些方法可以在源码里看到,都有注释,这里就不赘述了。
- setTranslucentStatusBar会判断当前系统版本,将Activity设置为全屏,并将状态栏设置为透明
- 如果需要使用大图作为背景,并且希望图片渗透到状态栏下,可以调用TranslucentNavBar的setTransparentColor()来将标题栏也设置为透明,然后使用布局将标题栏覆盖到大图背景上即可。
- AndroidBug5497Workaround
当初遇到软键盘弹起布局不Resize的问题的时候,也是Google了很久才在StackOverFlow找到答案(具体链接忘记了)。AndroidBug5497Workaround这个类的思路就是基于那篇文章 - 利用为Activity的content view设置 OnGlobalLayoutListener来监听布局的变化。通过判断何时弹起了软键盘来手动设置content view的高度,以此模拟adjustResize的效果。
在此基础上,我加入了一些基于现状的扩展。比如键盘弹起后手动设置的高度要计算状态栏的高度,低于4.4的版本的系统不计算状态栏,不全屏的Activity也不计算。又比如某些机型会有底栏,这个底栏的高度要考虑在内,比如谷歌原生的系统,以及典型的华为系统。其中华为系统的底栏高度计算方式又因为系统版本各有不同(很坑!)。因此在适配上花了不少时间。有兴趣的可以看看源码(不好意思,没有注释)。
圆角
圆角或圆形图片在Android是很常用的. 在CoreLibs中, 圆角相关均位于views包下的roundedimageview包中. 以下是包结构:
| roundedimageview
-RoundedDrawable 圆角Drawable, RoundedImageView与RoundedTransformationBuilder
内部均是使用RoundedDrawable来做圆角 或者圆形效果
-RoundedImageView 圆角ImageView, 任意图片在此控件中均可显示成圆角或圆形
-RoundedTransformationBuilder 圆角TransformationBuilder, 可配合Picasso使用产生圆角或圆形效果.
RoundedDrawable
首先来看看RoundedDrawable. 实现圆角最常见的就是利用Xfermode或Shader. RoundedDrawable就是使用的BitmapShader. 具体BitmapShader的原理或者用法可以自行谷歌.
RoundedDrawable提供了两个方法将Drawable/Bitmap转换成RoundedDrawable:
public static Drawable fromDrawable(Drawable drawable);
public static RoundedDrawable fromBitmap(Bitmap bitmap);
fromDrawable方法内部最终会将Drawable中的Bitmap取出赋值给RoundedDrawable并返回. RoundedDrawable还提供了一系列方法用以描述圆角信息, 如上下左右各个角的圆角角度, 或者是否是圆形等:
public RoundedDrawable setCornerRadius(Corner corner, float radius);
public RoundedDrawable setCornerRadius(float topLeft, float topRight, float bottomRight, float bottomLeft);
public RoundedDrawable setOval(boolean oval);
...
最后在draw方法中, 则会根据这些信息进行绘制, 调用canvas.drawRoundRect或canvas.drawOval得到圆角或圆形图片. 当然RoundedDrawable内部比这里说的复杂, 其内部会对不同的scaleType做适配, 不同的Drawable做适配, 以及其他兼容性问题. 也提供了一些设置Alpha, Shader的TileMode等方法.
RoundedImageView
我们可以使用RoundedImageView来显示圆角或圆形图片. 用法很简单:
XML:
<com.corelibs.views.roundedimageview.RoundedImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:riv_oval="true" // 设置是否是圆形, 如果为true, 则设置的圆角效果会无效
app:riv_corner_radius="5dp" // 四个角统一为5dp, 也可单独这是每个角
app:riv_corner_radius_top_left="5dp" // 左上5dp
app:riv_corner_radius_bottom_left="5dp" // 左下5dp
app:riv_corner_radius_bottom_right="5dp" // 右下5dp
app:riv_corner_radius_top_right="5dp" // 右上5dp
/>
代码:
RoundedImageView imageView;
imageView.setOval(true);
imageView.setCornerRadius(50);
imageView.setCornerRadius(50, 50, 50, 50);
RoundedTransformationBuilder
如果觉得使用自定义控件太麻烦, 则可以使用RoundedTransformationBuilder配合Picasso实现圆角. 使用系统默认ImageView即可. 首先看看用法:
圆形:
int size = (int) getResources().getDimension(R.dimen.avatar_height);
Picasso.with(context).load(user.icon).resize(size, size).centerCrop()
.transform(new RoundedTransformationBuilder().oval(true)
.build()).into(ivIcon);
圆角:
Picasso.with(context).load(user.icon).resize(size, size).centerCrop()
.transform(new RoundedTransformationBuilder().cornerRadius(50)
.build()).into(ivIcon);
Picasso.with(context).load(user.icon).resize(size, size).centerCrop()
.transform(new RoundedTransformationBuilder()
.cornerRadiusTopLeft(50)
.cornerRadiusBottomLeft(50)
.cornerRadiusTopRight(50)
.cornerRadiusBottomRight(50)
.build()).into(ivIcon);
RoundedTransformationBuilder内部也是使用了RoundedDrawable.fromBitmap(source)方法, 将源Bitmap转化为目标Bitmap. 具体原理是十分简单, 这里就不做过多解释.
标签导航
标签导航是常见的几种APP导航模式之一. 我们会经常用到这种模式. 使用标签导航的APP比较著名的有微信。
通常的做法是在底部写几个RadioButton, 或者是ImageView加TextView来模拟单选. 然后在Activity中使用OnClickListener来控制每个标签的状态, 同时还需要控制每个Fragment的创建与销毁, 显示与隐藏. 代码量不少, 如果每个项目都这么写会浪费很多精力与成本. 因此我们需要一个控件, 来代替OnClickListener去维护标签的状态, 以及Fragment的状态. 我们需要做的只是告诉这个控件, 每个标签长什么样, 标签对应的Fragment的类是什么, 需不需要参数等.
需求
下面我们试着实现以下UI效果:上述效果图在严格意义上来说不算是标签导航, 但是从开发来看, 实现的方式类似. 以下是具体需求:
有四个标签, 分别是心理测试, 心情圈, 健步走以及个人中心, 每个标签分别在当前页面内切换标签页. 点击中间的加号按钮则需要跳转到另一个页面, 当前页面则继续显示之前的标签页.
首先我们来分析一下重点:
- 中间加号的UI效果实现
- 点击加号跳转新的Activity, 并保持当前的标签页
如果没有中间的加号, 使用FragmentTabHost就能实现, 具体用法可以参考Android常用控件之FragmentTabHost的使用. 但是有了中间的加号FragmentTabHost就力不从心了, 因为FragmentTabHost的每一个标签都要对应一个Fragment, 不能对应Activity, 也无法对点击事件做拦截. 通过查看源码发现OnTabChangeListener也是在标签切换完成之后才会回调. 因此FragmentTabHost无法完成上述需求. 怎么办呢? 引入InterceptedFragmentTabHost.
InterceptedFragmentTabHost
InterceptedFragmentTabHost是CoreLibs中对FragmentTabHost的扩展, 实现了tab切换拦截功能. 一旦多了拦截功能, 就能实现上述需求了. 比如FragmentTabHost中实际上是有5个标签, 包含了中间的加号. 我可以通过设置拦截监听器, 检测一旦用户点击了第三个标签, 就不进行切换操作, 而是跳转至一个新的Activity.
由于通过继承FragmentTabHost没法很好的实现拦截功能, 并且FragmentTabHost内部只是引用了几个Android内部的id资源, 没有其他内部资源, 因此我们完全可以将FragmentTabHost的源码复制出来放到自己的项目中, 而不会出现编译不通过的后果. InterceptedFragmentTabHost就是通过这种方式对FragmentTabHost扩展.
接下来看看如何使用InterceptedFragmentTabHost. InterceptedFragmentTabHost中提供了如下方法来设置拦截监听器:
InterceptedFragmentTabHost的用法与FragmentTabHost基本完全一致, 唯一不同的就是多了setTabChangeInterceptor方法. 由于FragmentTabHost需要我们提供每个标签对应的tab, 因此我们可以声明一个String数组:
String[] tabTags = new String[] { getString(R.string.tab_test),
getString(R.string.tab_mood), getString(R.string.tab_add_mood),
getString(R.string.tab_pm), getString(R.string.tab_me) };
有了tabTags, 我们就可以跟据TabChangeInterceptor中传来的tabId来判断用户点击的是哪个标签了:
interceptedFragmentTabHost.setTabChangeInterceptor(new TabChangeInterceptor() {
@Override
public boolean canTab(String tabId) {
return !tabId.equals(tabTags[2]); // 判断点击的是否是第三个标签
}
@Override
public void onTabIntercepted(String tabId) {
toAddMood(); // 跳转Activity
}
});
TabChangeInterceptor的用法很简单, 每当用户点击一个非当前标签的标签时, InterceptedFragmentTabHost都会根据canTab方法的返回值来判断是否能做切换操作. 如果返回true, 意味着能切换, 返回false则意味不能切换. !tabId.equals(tabTags[2])这行代码意味着只要点击的不是第三个标签, canTab都会返回true, 如果是第三个, 则返回false. 一旦canTab返回false, InterceptedFragmentTabHost会紧接着调用onTabIntercepted方法, 我们可以在此方法中做一些其他的事情, 如跳转Activity.
TabNavigator
使用FragmentTabHost虽然不需要控制点击事件以及Fragment的切换, 但是还是需要做一些Tab页设置等工作. 这些逻辑也可以抽出一个公共类 - TabNavigator. TabNavigator只需实现一个接口, 加上一行代码, 就可以配置好FragmentTabHost:
private TabNavigator navigator = new TabNavigator();
navigator.setup(context, tabHost, content, getSupportFragmentManager(), R.id.real_tab_content);
实现
下面我们来看看具体如何使用TabNavigator加InterceptedFragmentTabHost来实现前面提到的UI效果图.
Activity布局
首先标签导航一般是位于主页, 因此我们来看看MainActivity的布局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/real_tab_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="55dp" />
<com.corelibs.views.tab.InterceptedFragmentTabHost
android:id="@android:id/tabhost"
android:layout_width="match_parent"
android:layout_alignParentBottom="true"
android:layout_height="75dp" />
</RelativeLayout>
完整的MainActivity代码:
public class MainActivity extends BaseActivity implements TabNavigator.TabNavigatorContent {
public static final int ADD_MOOD_POSITION = 2;
@Bind(android.R.id.tabhost) InterceptedFragmentTabHost tabHost;
private TabNavigator navigator = new TabNavigator();
private String[] tabTags;
private int[] imageResIds = new int[] { R.drawable.tab_test_icon, R.drawable.tab_mood_icon, 0,
R.drawable.tab_pm_icon, R.drawable.tab_me_icon };
public static Intent getLaunchIntent(Context context) {
return new Intent(context, MainActivity.class);
}
@Override
protected int getLayoutId() {
return R.layout.activity_main;
}
@Override
protected void init(Bundle savedInstanceState) {
tabTags = new String[] { getString(R.string.tab_test),
getString(R.string.tab_mood), getString(R.string.tab_add_mood),
getString(R.string.tab_pm), getString(R.string.tab_me) };
navigator.setup(this, tabHost, this, getSupportFragmentManager(), R.id.real_tab_content);
navigator.setTabChangeInterceptor(new TabChangeInterceptor() {
@Override
public boolean canTab(String tabId) {
return !tabId.equals(tabTags[ADD_MOOD_POSITION]);
}
@Override
public void onTabIntercepted(String tabId) {
toAddMood();
}
});
}
@Override
protected BasePresenter createPresenter() {
return null;
}
@Override
public View getTabView(int position) {
View view;
if (position == ADD_MOOD_POSITION) {
view = getLayoutInflater().inflate(R.layout.view_tab_add, null);
return view;
} else {
view = getLayoutInflater().inflate(R.layout.view_tab_content, null);
}
ImageView iv = (ImageView) view.findViewById(R.id.iv_tab_icon);
TextView tv = (TextView) view.findViewById(R.id.tv_tab_text);
iv.setImageResource(imageResIds[position]);
tv.setText(tabTags[position]);
return view;
}
@Override
public Bundle getArgs(int position) {
return null;
}
@Override
public Class[] getFragmentClasses() {
return new Class[] { TestFragment.class,
MoodFragment.class, AddMoodFragment.class,
PedometerFragment.class, MeFragment.class };
}
@Override
public String[] getTabTags() {
return tabTags;
}
private void toAddMood() {
startActivity(AddMoodActivity.getLaunchIntent(MainActivity.this));
}
}
其他功能:图片缩放、滑动选择、弹窗、自适应高度的Imageview、常用控件-CircularBar(圆形进度条)详情点这里