设计模式-适配器模式
什么是适配器模式?什么时候用?
说到适配器模式,大家肯定会想到Android开发中的ListView和RecyclerView,这种列表型的View,为了解耦开多类型的条目,使用了适配器模式,列表View只需要子条目的View,其余创建条目和绑定条目数据都交给我们定义适配器子类进行配置。
而适配器一般是解决2个类不兼容的问题,例如期望得到一个方法调用结果,而目标类是在类库中并不能进行修改,这时则可以使用适配器模式进行适配。
其中适配器,分为以下几种:
- 类适配器模式,通过继承的方式拓展。
- 对象适配器模式,通过组合的方式拓展。
- 缺省适配器模式,为接口启动缺省实现(空实现)
怎么实现适配器模式?
要实现适配器模式,一般会以下对象:
- Target,目标对象,就是期望得到的接口。
- Adaptee,源接口,需要适配的接口。
- Adapter,适配器对象,将Adaptee源接口转换为Target目标接口。
User信息适配为Teacher信息
如果我们又User表和Teacher表,Teacher表是User表的从表(子表),老师信息相比用户信息只是多了一些字段。我们已提供了查询User信息的getUserInfo()方法,需要提供一个获取Teahcer信息的getTeacherInfo()方法。我们就可以使用适配器模式,将获取用户信息拓展为获取老师信息。
我们先来使用类适配器来实现,类适配中,适配器和源接口是继承关系。
- 步骤一:定义UserInfo和TeacherInfo模型。
public class UserInfo {
/**
* id
*/
private int id;
/**
* 名字
*/
private String nickname;
/**
* 年龄
*/
private int age;
...省略get、set方法
}
public class TeacherInfo extends UserInfo {
/**
* 职称
*/
private String jobTitle;
/**
* 介绍
*/
private String introduce;
}
- 步骤二:定义需要适配的Adaptee类,User信息的业务类(这里为了演示,就模拟获取信息了)
public class UserOperater {
/**
* 获取用户信息
*/
public UserInfo getUserInfo(int id) {
//伪装查询用户信息
UserInfo info = new UserInfo();
info.setId(id);
info.setNickname("Wally");
info.setAge(22);
return info;
}
}
- 步骤三:定义适配后的目标接口。
public interface ITeacherOperater {
/**
* 获取指定id的老师信息
*
* @param id 老师id
*/
TeacherInfo getTeacherInfo(int id);
}
- 步骤四:定义适配器(类适配器是使用继承的方法实现的)
public class ClassAdapter extends UserOperater implements ITeacherOperater {
@Override
public TeacherInfo getTeacherInfo(int id) {
UserInfo userInfo = getUserInfo(id);
TeacherInfo teacherInfo = new TeacherInfo();
//信息转移
mapperInfo(userInfo, teacherInfo);
//增加老师信息
teacherInfo.setJobTitle("Android开发工程师");
teacherInfo.setIntroduce("好饿,早知不做安卓了~");
return teacherInfo;
}
/**
* 映射信息
*
* @param sourceInfo 源信息
* @param outInfo 输出信息
*/
private void mapperInfo(UserInfo sourceInfo, TeacherInfo outInfo) {
outInfo.setId(sourceInfo.getId());
outInfo.setNickname(sourceInfo.getNickname());
outInfo.setAge(sourceInfo.getAge());
}
}
- 步骤五:调用
public class Main {
public static void main(String[] args) {
//使用继承方式,类适配器模式
ClassAdapter classAdapter = new ClassAdapter();
TeacherInfo teacherInfo = classAdapter.getTeacherInfo(10086);
//输出:TeacherInfo{jobTitle='Android开发工程师', introduce='好饿,早知不做安卓了~'}
System.out.println(teacherInfo);
}
}
类适配器的缺点
类适配器由于使用的是继承,所以只能适配具体源接口(UserOperater),而不能适配源接口的子类。
改用对象适配器实现
既然类适配器具有不能适配源接口子类的缺点,那么有什么解决方案呢?方案就是使用对象适配器,对象适配器和类适配器的区别就是,从使用继承改为组合。
由于实体类、源接口和目标接口都是和上面类适配器中一致的,所以不再给出,只给出对象适配器的代码。
public class InterfaceAdapter implements ITeacherOperater {
private UserOperater mUserOperater;
/**
* 重点在这里:通过注入方式,注入用户操作器
*/
public InterfaceAdapter(UserOperater userOperater) {
this.mUserOperater = userOperater;
}
@Override
public TeacherInfo getTeacherInfo(int id) {
UserInfo userInfo = mUserOperater.getUserInfo(id);
TeacherInfo teacherInfo = new TeacherInfo();
//信息转移
mapperInfo(userInfo, teacherInfo);
//增加老师信息
teacherInfo.setJobTitle("Android开发工程师");
teacherInfo.setIntroduce("好饿,早知不做安卓了~");
return teacherInfo;
}
/**
* 映射信息
*
* @param sourceInfo 源信息
* @param outInfo 输出信息
*/
private void mapperInfo(UserInfo sourceInfo, TeacherInfo outInfo) {
outInfo.setId(sourceInfo.getId());
outInfo.setNickname(sourceInfo.getNickname());
outInfo.setAge(sourceInfo.getAge());
}
}
public class Main {
public static void main(String[] args) {
//使用组合方法,对象适配器模式
InterfaceAdapter interfaceAdapter = new InterfaceAdapter(new UserOperater());
TeacherInfo teacherInfo2 = interfaceAdapter.getTeacherInfo(10086);
//输出:TeacherInfo{jobTitle='Android开发工程师', introduce='好饿,早知不做安卓了~'}
System.out.println(teacherInfo2);
}
}
缺省适配器(空实现)
一般用到缺省适配器的场景,就是一个接口有多个回调方法,而并不是所有的回调我们都需要时,就可以使用一个类实现接口,空实现部分或所有方法,我们使用的时候则使用这个空实现的适配器。
- 缺省ListView滚动监听
我们给ListView设置滚动监听时,我们需要调用ListView的setOnScrollListener()方法,传入OnScrollListener接口实例,而OnScrollListener中有onScrollStateChanged()和onScroll()2个回调方法,而我们一般只需要onScrollStateChanged()方法获取滚动状态即可。那么我们可以新建一个叫SimpleScrollAdapter。
public class SimpleScrollAdapter implements AbsListView.OnScrollListener {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
}
}
//使用缺省适配器
vListView. setOnScrollListener(new SimpleScrollAdapter() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
super. onScrollStateChanged(view, scrollState)
}
})
适配器模式实现弹幕
在写直播模式时,需要实现弹幕功能,不过并不是视频大幕那么复杂。当用户进入直播间时发送,是一个不断从屏幕右侧进入,左侧消失的View动画,而View的内容可以有多种,弹幕弹道View如果需要关注多种不同样式的弹幕,弹道View就需要兼容不同的样式,这显然是不可取的!
这时候不难想到,我们使用适配器模式,可以这么实现:
-
移动弹幕View的为子View,动画调用是弹幕View的父View。
-
弹幕的添加和移除就在动画的开始时添加,结束时移除。
-
弹幕是显示完一条再显示另外一条,而进入直播间是一种并发行为,所以需要一个队列进行管理,再上一条弹幕没有消失前,新的弹幕信息需要堆积在队列汇中,这条弹幕展示结束后,再从队列中获取。
实现步骤
- 弹幕消息实体(为了统一弹幕数据的结构和后续拓展,内部泛型包裹一个弹幕实体)
//基础弹幕消息模型
public class DanMuMessage<T> {
/**
* 弹幕需要的模型,这里将弹幕View需要的模型和队列消息的字段拆开,方便以后拓展
*/
private T danMuModel;
public DanMuMessage(T danMuModel) {
this.danMuModel = danMuModel;
}
...省略get、set方法
}
//用户进入房间的弹幕模型
public class UserEnterDanMuModel {
/**
* 用户名
*/
private String userName;
public UserEnterDanMuModel() {
}
public UserEnterDanMuModel(String userName) {
this.userName = userName;
}
...省略get、set方法
}
- 弹幕队列,采用LinkedList,一个是因为他是队列Queue接口的实现,也是一个列表。即有队列操作,也有列表查询操作。
public class DanMuQueue {
/**
* 消息队列
*/
private LinkedList<DanMuMessage<?>> mMsgQueue;
public DanMuQueue() {
mMsgQueue = new LinkedList<>();
}
/**
* 消息进队
*/
public void enQueue(DanMuMessage<?> message) {
mMsgQueue.offer(message);
}
/**
* 将第一条消息,出队
*/
public DanMuMessage<?> dequeue() {
return mMsgQueue.poll();
}
/**
* 拿出队头的消息,但不会移除
*/
public DanMuMessage<?> peekMessage() {
return mMsgQueue.peek();
}
/**
* 是否有消息在队列中
*/
public boolean hasMessageInQueue() {
return mMsgQueue.size() > 0;
}
}
- 弹幕操作回调,用于从队列中获取下一条弹幕信息进行展示使用。
public interface OnDanMuActionCallback {
/**
* 弹幕展示时回调
*/
void onDanMuShow();
/**
* 弹幕显示完毕,隐藏时回调
*/
void onDanMuHide();
}
- 弹幕适配器基类,处理多类型的进入弹幕View,不同的弹幕的适配器需要继承该适配器(是不是和ListView的Adapter很像)
public abstract class BaseDanMuAdapter<T> {
/**
* 创建弹幕布局
*
* @param inflater 填充器
* @param parent 弹幕父布局
* @param viewType 类型
*/
public abstract View onCreateDanMuView(LayoutInflater inflater, ViewGroup parent, int viewType);
/**
* 复用时,重新绑定弹幕View的数据
*
* @param view 弹幕View
* @param viewType 类型
* @param model 弹幕数据模型
*/
public abstract void onBindDanMuView(View view, int viewType, T model);
/**
* 获取View类型
*/
public abstract int getViewType(T model);
}
- 用户进入房间弹幕适配器,继承基础弹幕适配器,指定弹幕数据。不同的弹幕类型,需要在getViewType()中返回对应的类型,一般都在Model类中指定类型或提供判断类型的方法。
public class UserEnterDanMuAdapter extends BaseDanMuAdapter<UserEnterDanMuModel> {
@Override
public View onCreateDanMuView(LayoutInflater inflater, ViewGroup parent, int viewType) {
return inflater.inflate(R.layout.live_room_dynamic_user_enter_room_dan_mu_view, parent, false);
}
@Override
public void onBindDanMuView(View view, int viewType, UserEnterDanMuModel model) {
Object tag = view.getTag();
//缓存ViewHolder
ViewHolder holder;
if (tag == null) {
holder = new ViewHolder(view);
view.setTag(holder);
} else {
//绑定控件
holder = (ViewHolder) tag;
}
holder.bindView(model);
}
@Override
public int getViewType(UserEnterDanMuModel model) {
// TODO: 2019-07-21 后续需要添加不同类型的弹幕,判断模型内的字段返回对应类型即可
return 0;
}
//缓存View控件,避免每次都findView
private static class ViewHolder {
private TextView vTip;
private final Context mContext;
ViewHolder(View view) {
mContext = view.getContext();
vTip = view.findViewById(R.id.tip);
}
public void bindView(UserEnterDanMuModel model) {
String enterRoomUserName = model.getUserName();
//超过了截断,并添加省略
if (enterRoomUserName.length() > 8) {
String name = enterRoomUserName.substring(0, 5);
enterRoomUserName = mContext.getResources().getString(R.string.live_user_enter_user_name_omit, name);
}
String tip = mContext.getResources().getString(R.string.live_room_user_enter_room_tip, enterRoomUserName);
vTip.setText(tip);
}
}
}
- 用户进入房间弹幕的弹道(移动的弹幕View的父View)
public class UserEnterRoomDanMuRoadView extends FrameLayout {
/**
* 布局填充器
*/
private LayoutInflater mLayoutInflater;
/**
* 要移动的弹幕View
*/
private View vRealDanMuView;
/**
* 弹道布局宽度
*/
private int mViewWidth;
/**
* 展示回调
*/
private OnDanMuActionCallback mActionCallback;
/**
* 是否展示中
*/
private boolean isShowing;
/**
* 弹幕适配器
*/
private BaseDanMuAdapter<UserEnterDanMuModel> mAdapter;
/**
* 弹幕View缓存
*/
private Map<Integer, View> mDanMuViewCacheViews;
public UserEnterRoomDanMuRoadView(@NonNull Context context) {
super(context);
init();
}
public UserEnterRoomDanMuRoadView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public UserEnterRoomDanMuRoadView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mLayoutInflater = LayoutInflater.from(getContext());
mDanMuViewCacheViews = new HashMap<>(5);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
}
/**
* 显示弹幕
*/
public void show() {
//...移动和渐变动画,就不贴了,只要是下面的绑定数据方法
//简单说下,动画使用AnimatorSet,动画开始前调用mActionCallback的onDanMuShow()方法,结束后调用mActionCallback的onDanMuHide()方法。
}
/**
* 绑定数据
*/
public void bindData(UserEnterDanMuModel model) {
int viewType = mAdapter.getViewType(model);
//先从缓存充拿,如果没有才创建,并保存到缓存中
vRealDanMuView = mDanMuViewCacheViews.get(viewType);
if (vRealDanMuView == null) {
vRealDanMuView = mAdapter.onCreateDanMuView(mLayoutInflater, this, viewType);
mDanMuViewCacheViews.put(viewType, vRealDanMuView);
}
//由于复用时,父控件是存在的,所以需要先移除
if (vRealDanMuView.getParent() != null) {
((ViewGroup) vRealDanMuView.getParent()).removeView(vRealDanMuView);
}
ViewGroup.LayoutParams layoutParams = vRealDanMuView.getLayoutParams();
if (layoutParams == null) {
layoutParams = new UserEnterRoomDanMuRoadView.MarginLayoutParams(MarginLayoutParams.MATCH_PARENT,
MarginLayoutParams.WRAP_CONTENT);
}
addView(vRealDanMuView, layoutParams);
mAdapter.onBindDanMuView(vRealDanMuView, viewType, model);
}
public boolean isShowing() {
return isShowing;
}
public void setOnDanMuActionCallback(OnDanMuActionCallback actionCallback) {
mActionCallback = actionCallback;
}
public void setAdapter(BaseDanMuAdapter<UserEnterDanMuModel> adapter) {
mAdapter = adapter;
}
}
- 弹幕组件,提供发送弹幕的Api。内部保存弹幕队列和配置弹幕的适配器。
/**
* 用户进入房间,弹幕队列
*/
private final DanMuQueue mUserEnterDanMuQueue;
public DynamicComponent() {
mUserEnterDanMuQueue = new DanMuQueue();
}
//公开Api,发送用户进入房间的弹幕
public void sendUserEnterDynamic(DanMuMessage<UserEnterDanMuModel> message) {
//弹幕信息入队
mUserEnterDanMuQueue.enQueue(message);
//没有正在展示的弹幕,则马上展示
if (!vUserEnterDanMuRoadView.isShowing()) {
nextUserEnterDanMu();
}
}
/**
* 展示下一条用户进入的弹幕消息
*/
private void nextUserEnterDanMu() {
if (mUserEnterDanMuQueue.hasMessageInQueue()) {
DanMuMessage<?> nextMsg = mUserEnterDanMuQueue.dequeue();
if (nextMsg == null) {
return;
}
Object danMuModel = nextMsg.getDanMuModel();
//用户进入房间弹幕
if (danMuModel instanceof UserEnterDanMuModel) {
UserEnterDanMuModel model = (UserEnterDanMuModel) danMuModel;
vUserEnterDanMuRoadView.bindData(model);
vUserEnterDanMuRoadView.show();
}
}
}
总结
适配器模式将不兼容的接口进行兼容处理,也可以将不能融合的类进行融合处理,熟悉ListView和RecyclerView的我们,可以仿照他们做出属于我们的适配器,例如上面的弹幕弹道View,并不需要知道弹幕的样式和数据,只需要知道操作弹幕View在适当时机显示和结束动画即可,实现了容器View和子View的多样化。