设计模式

设计模式-适配器模式

2020-10-17  本文已影响0人  h2coder

什么是适配器模式?什么时候用?

说到适配器模式,大家肯定会想到Android开发中的ListView和RecyclerView,这种列表型的View,为了解耦开多类型的条目,使用了适配器模式,列表View只需要子条目的View,其余创建条目和绑定条目数据都交给我们定义适配器子类进行配置。

而适配器一般是解决2个类不兼容的问题,例如期望得到一个方法调用结果,而目标类是在类库中并不能进行修改,这时则可以使用适配器模式进行适配。

其中适配器,分为以下几种:

  1. 类适配器模式,通过继承的方式拓展。
  2. 对象适配器模式,通过组合的方式拓展。
  3. 缺省适配器模式,为接口启动缺省实现(空实现)

怎么实现适配器模式?

要实现适配器模式,一般会以下对象:

  1. Target,目标对象,就是期望得到的接口。
  2. Adaptee,源接口,需要适配的接口。
  3. Adapter,适配器对象,将Adaptee源接口转换为Target目标接口。

User信息适配为Teacher信息

如果我们又User表和Teacher表,Teacher表是User表的从表(子表),老师信息相比用户信息只是多了一些字段。我们已提供了查询User信息的getUserInfo()方法,需要提供一个获取Teahcer信息的getTeacherInfo()方法。我们就可以使用适配器模式,将获取用户信息拓展为获取老师信息。

我们先来使用类适配器来实现,类适配中,适配器和源接口是继承关系。

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;
}
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的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就需要兼容不同的样式,这显然是不可取的!

这时候不难想到,我们使用适配器模式,可以这么实现:

  1. 移动弹幕View的为子View,动画调用是弹幕View的父View。

  2. 弹幕的添加和移除就在动画的开始时添加,结束时移除。

  3. 弹幕是显示完一条再显示另外一条,而进入直播间是一种并发行为,所以需要一个队列进行管理,再上一条弹幕没有消失前,新的弹幕信息需要堆积在队列汇中,这条弹幕展示结束后,再从队列中获取。

实现步骤

//基础弹幕消息模型
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方法
}
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();
}
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);
}
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);
        }
    }
}
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;
    }
}
/**
 * 用户进入房间,弹幕队列
 */
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的多样化。

上一篇下一篇

猜你喜欢

热点阅读