上一篇讲述如何优化Padding使其在树形布局内部拖拽时也能生效。本篇讲解如何通过两种设计模式来极大地增加布局的拓展性。
前期准备
在真正开始之前我们首先要熟悉两种设计模式。
1.策略模式
策略模式可以将一些具有相似特征的算法提取出来变成一个接口,这样就可以根据需要动态地实现算法的替换。说得有些抽象,举个例子。
比如一个类要用到排序,根据排序量大小、初始状态的不同,我们可以在快排、冒泡、归并等等算法进行选择。但是常规的方式是通过if-else分支来实现。问题就来了,如果现在要添加一种排序算法,就不得不在代码里再添加一个else代码块,这是与开闭原则相悖的。代码如下:
class ClassA{
void sort(int arr[]){
if(...){
//快排
}else if(...){
//冒泡
}else if(...){
//归并
}else if(...){
//新添加的排序算法
}
}
}
想想看,如果大多数情况代码都要跳到第四种算法来进行排序,那就避免不了大量的比较判断。利用策略模式就能为了解决上述两种问题。
无论是快排、冒泡还是归并,它们都有共同操作那就是给数组排序,所以可以抽象出一个排序类(或接口)。
public interface SortStrategy{
void sort(int arr[]);
}
具体是怎么排的就看它们自己的实现方式了。
public class QuickSort extends SortStrategy{
public void sort(int arr[]){
...
//算法用时方恨少,哎...忘的差不多了
}
}
public class BubbleSort extends SortStrategy{
public void sort(int arr[]){
...
}
}
public class MergerSort extends SortStrategy{
public void sort(int arr[]){
...
}
}
根据需要使用具体的策略就行了,就算要添加新的排序算法,也不需要再修改ClassA了,而且还能避免大量的if-else分支判断。
class ClassA{
void sort(int arr[],SortStrategy sortStrategy){
sortStrategy.sort(arr);
}
}
2.装饰模式
装饰模式是一种结构型设计模式,这种设计模式可以动态地拓展原有对象的功能,代替类的继承。下面举个例子。
//车辆类,抽象类
abstract class Vehicle{
abstract void run();
}
//汽车类
class Car extends Vehicle{
@Override
void run(){
System.out.print("run as a car");
}
}
//越野车类
class SUV extends Vehicle{
@Override
void run(){
System.out.print("run as a suv");
}
}
//加装涡轮增压器的车辆类
class JetVehicle extends Vehicle{
Vehicle mVehicle ;
public JetVehicle(Vehicle sourceVehicle ){
mVehicle = sourceVehicle ;
}
@Override
void run(){
mVehicle.run();
System.out.print(" with jet engine");
}
}
//全自动车辆类
class AutoVehicle extends Vehicle{
Vehicle mVehicle ;
public AutoVehicle (Vehicle sourceVehicle ){
mVehicle = sourceVehicle ;
}
@Override
void run(){
mVehicle.run();
System.out.print(" which is automatic");
}
}
现在有如下三种客户需求。A客户想要一辆加装涡轮增压器的汽车,B客户想要一辆能自动驾驶的越野车,C客户想要一辆即加装涡轮增压器又能自动驾驶的汽车。看看装饰模式下的代码实现。
//A客户的需求
Vehicle vehicleA = new JetVehicle(new Car());
//B客户的需求
Vehicle vehicleB = new AutoVehicle (new SUV());
//C客户的需求
Vehicle vehicleC = new AutoVehicle (new JetVehicle(new Car()));
其实我们发现,ABC客户的需求无非就是在 [汽车,SUV] 和 [涡轮增压器,自动驾驶]中自由搭配,可如果使用继承的方式来实现,就不得不为每一种不同的需求创建一个新的类,随着需求多了,继承方式就会变得很不灵活。
3.工厂方法模式
回到刚刚的例子,现在需要制造多辆带有涡轮增压器且能自动驾驶的汽车。原先的做法如下:
Vehicle vehicle1 = new Car();
Vehicle vehicle1WithJet = new JetVehicle(vehicle1);
Vehicle autoVehicle1 = new AutoVehicle(vehicle1WithJet );
Vehicle vehicle2 = new Car();
Vehicle vehicle2WithJet = new JetVehicle(vehicle2);
Vehicle autoVehicle2 = new AutoVehicle(vehicle2WithJet );
...
Vehicle vehicleN = new Car();
Vehicle vehicleNWithJet = new JetVehicle(vehicleN);
Vehicle autoVehicleN = new AutoVehicle(vehicleNWithJet);
每次创建带有涡轮增压器且能自动驾驶的汽车都要进行复杂的改装(初始化),这就是装饰模式的不足之处,把创建过程变复杂了。为了能够简化创建流程,这里可以使用工厂方法模式。工厂方法模式是一种创建型设计模式,它适用于创建过程复杂的对象,调用者不必过于关注对象的创建过程。如下是工厂模式的代码。
abstract class AbsVehicleFactory{
abstract Vehicle createVehicle();
}
class AutoJetVehicleFactory extends AbsVehicleFactory{
Vehicle createVehicle(){
Vehicle vehicle = new Car();
Vehicle vehicleWithJet = new JetVehicle(vehicle);
Vehicle autoVehicle = new AutoVehicle(vehicleWithJet );
return autoVehicle;
}
}
创建一个专门制造 带涡轮增压器且能自动驾驶汽车 的工厂后,创建这种类型的车辆就简单多了。
AutoJetVehicleFactory factory = new AutoJetVehicleFactory();
Vehicle vehicle1 = factory.createVehicle();
Vehicle vehicle2 = factory.createVehicle();
...
Vehicle vehicleN = factory.createVehicle();
利用策略模式和装饰模式改写连接线绘制类
现在想要给树形布局添加绘制结点外框的功能。想想看,无论是绘制连接线还是绘制结点的外框,其实它们都是树形布局的点缀。既然是这样,我们就可以把它们都抽象成点缀类,点缀类无非是在布局上画点东西,至于画什么,就看子类的具体实现了。再看看下面这张图片。
不同的连接线可以搭配不同的结点外框,例如第一种线搭配第一种外框,就得到如下的效果。
代码实现如下。
/**
* TreeLayout专用的外间距参数类,用于记录布局参数
*/
public static class LayoutParams extends MarginLayoutParams{
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
/**
* 点缀绘制器,TreeLayout通过这个类的子类来绘制点缀内容
*/
public static abstract class NodeDectorDrawer {
private NodeDectorDrawer mSourceDector;
public NodeDectorDrawer(NodeDectorDrawer sourceDector){
mSourceDector = sourceDector;
}
/**
* 绘制自身树布局的点缀
* @param canvas 绘制点缀的画布
* @param paint 绘制点缀的画笔
* @param start 点缀的起点控件的区域,即父结点控件所在区域
* @param end 点缀的终点控件的区域,即子结点控件所在区域
* @param direction 树的方向
* 参考{@link #DIRECTION_LEFT_TO_RIGHT,
* @link #DIRECTION_RIGHT_TO_LEFT,
* @link #DIRECTION_UP_TO_DOWN,
* @link #DIRECTION_DOWN_TO_UP}
*/
protected abstract void onDrawDecorator(Canvas canvas, Paint paint, Rect start, Rect end, int direction);
/**
* 绘制自身树布局的点缀
* @param canvas 绘制点缀的画布
* @param paint 绘制点缀的画笔
* @param start 点缀的起点控件的区域,即父结点控件所在区域
* @param end 点缀的终点控件的区域,即子结点控件所在区域
* @param startView 点缀的起点控件
* @param endView 点缀的终点控件
* @param direction 树的方向
*/
public void drawDecorator(Canvas canvas, Paint paint, Rect start, Rect end,View startView,View endView,int direction){
if(mSourceDector != null){
mSourceDector.drawDecorator(canvas,paint,start,end,startView,endView,direction);
}
if(skipThisDraw(startView,endView)){
return;
}
onDrawDecorator(canvas,paint,start,end,direction);
}
/**
* 跳过本树布局的点缀绘制
* @param startView 点缀的起点控件
* @param endView 点缀的终点控件
* @return 是否跳过本树布局的点缀绘制
*/
public boolean skipThisDraw(View startView,View endView){
return false;
}
}
由于篇幅问题,点缀绘制器的实现类代码这里就不贴出了,文章最后有Github项目地址。自带的实现类有如下几个。
DirectLineDrawer:直线绘制器
DocumentLineDrawer:折线绘制器
CurveDrawer:曲线绘制器
RectFrameDrawer:方框绘制器
ConnectPointDrawer:连接点绘制器
来看看具体使用,通过装饰模式搭配出一种绘制树形布局的点缀内容。
...
DocumentLineDrawer documentLineDrawer = new DocumentLineDrawer(6,treeView.getLevelInterval(),Color.WHITE);
RectFrameDrawer rectFrameDrawer = new RectFrameDrawer(documentLineDrawer,12,Color.WHITE);
ConnectPointDrawer connectPointDrawer = new ConnectPointDrawer(rectFrameDrawer,12,Color.WHITE);
DocumentLineDrawer documentLineDrawer1 = new DocumentLineDrawer(6,treeView1.getLevelInterval(),Color.WHITE);
RectFrameDrawer rectFrameDrawer1 = new RectFrameDrawer(documentLineDrawer1,12,Color.WHITE);
ConnectPointDrawer connectPointDrawer1 = new ConnectPointDrawer(rectFrameDrawer1,12,Color.WHITE);
treeView.setDecorDrawer(connectPointDrawer);
treeView1.setDecorDrawer(connectPointDrawer1);
...
换个搭配试试。
再写几种连接线类和外框类,搭配上的选择顿时就变多了(这时突然想起了高中学的排列组合)。
利用工厂方法模式简化绘制器创建
随着我们的绘制类越来越多,要搭配出一个我们想要的绘制器就变得复杂起来了。为了简化这些复杂的创建流程,我们可以采用工厂方法模式。
首先写一个绘制器工厂的抽象类。
public abstract class AbsDecoratorFactory {
public abstract TreeLayout.NodeDecoratorDrawer createDecorator();
}
接下来根据实际需要写一个对应的实现类即可。
public class ClassicDecoratorFactory extends AbsDecoratorFactory {
private int mLineWidth;
private int mRadius;
private int mColor;
private int mInterval;
public ClassicDecoratorFactory(int lineWidth, int radius, int interval, int color) {
mLineWidth = lineWidth;
mRadius = radius;
mColor = color;
mInterval = interval;
}
@Override
public TreeLayout.NodeDecoratorDrawer createDecorator() {
DirectLineDrawer directLineDrawer = new DirectLineDrawer(mLineWidth,mColor);
RectFrameDrawer rectFrameDrawer = new RectFrameDrawer(directLineDrawer,mLineWidth,mColor);
ConnectPointDrawer connectPointDrawer = new ConnectPointDrawer(rectFrameDrawer,mRadius, mColor);
return connectPointDrawer;
}
}
可以看到,复杂的绘制器组合搭配过程就被藏在了工厂类里,调用者只需少量代码即可生成目标绘制器。
ClassicDecoratorFactory factory = new ClassicDecoratorFactory(6,12,treeView.getLevelInterval(),Color.WHITE);
treeView.setDecorDrawer(factory.createDecorator());
treeView1.setDecorDrawer(factory.createDecorator());
最后
利用装饰模式和工厂方法模式来优化代码确实是很好的选择,装饰模式增加了树形布局的拓展性,工厂方法模式简化了装饰模式的复杂搭配过程。
由于代码重构,前面几篇文章讲到的连接线绘制的代码被改动了。由于篇幅原因,文章很遗憾没能贴出全部代码,感兴趣的朋友可以到Github项目中阅读完整代码。喜欢这个布局控件的话可以star一下以示鼓励。
下一篇讲解如何给树形布局添加遍历监听功能。