提供一种简单实现动态布局的思路,即每次布局页面发生变化,前端不用更新版本,让后端来控制布局的展示效果

大概1周的时间,终于完成了思路设计到Demo的实现

什么是动态布局

  • 我们经常的做法是每次产品有新的需求,我们都要根据新的页面样式
    1. 画布局
    2. 解析后端数据为model
    3. 找到每个控件
    4. 分别设置值.
  • 而动态布局,就是前端不固定布局样式,根据后端下发的数据映射成本地控件.再根据后端数据进行展示.有点抽象,脑壳疼+_+~~大胸弟,莫要慌,抱紧我!

为什么要做动态布局

  • 没有压迫就没有战争,没有需求就没有innovation.每次的需求都是跟具体业务嘻嘻相关的.为了让移动app能适应广告投放素材的多样性, 并在投放过程中减少人力的介入 ,实现告展示视图的高效,自适应页面动态布局
  • 业务需求是这样的,以banner页面,和瀑布流布局为例.我们要投放广告,而广告的来源与页面设计是根据广告商的需求来定制的.不同的广告商对于不同广告的页面设计需求是不同的.而对于大流量的app而言,一个广告可能就只是展示一天,几天,甚至几个小时.假如一个小时换一个广告,我们前端是不可能每小时更新一个版本的,Android还好一点,对于iOS来讲,appstore审核就要一到两周.如果真的这样发版本了.用户:你tm确定这不是在逗我?因此,对于这种需求,动态布局势在必行!

实现动态布局的思路

  • 前面说了一大堆废话.你可能看了篇假博客?
  • 动态布局主要是要确定一下三点:
    1. 布局大小
    2. 布局位置
    3. 控件类型
  • 当然还有一下小的注意点,比如margin,图片圆角,点击事件等等,当然本文并实现这些,这都是小问题,不是吗?可能也不是,对于我若是的话,我这一篇不写完了吗?菜鸡啊~
  • 思路(根据后端下发的json数据按比例和做本地映射展示出布局)
    1. 先把我们动态布局的容器即我们想在那一块动态展示布局的区域,比如banner位置的长宽分成若干等分
    2. 让后端api数据中返回我们需要的1.长宽,2起始坐标,3控件类型(我们Android&iOS做本地映射成我们自身的控件类型).4总的长宽各分了多少份
    3. 上面说的长宽,起始左边,和总的长宽都是一个相对值.后边会有细说呢

文档

  1. json数据格式定义
   {
       "data": {
           "cols": "4",
           "rows": "4",
           "views": [
               {
                   "type": "text",
                   "value": "这是一个TextView",
                   "width": "4",
                   "height": "1",
                   "x": "0",
                   "y": "3",
                   "font": "17",
                   "other": "其他"//比如一些margin,是否可点击,背景等等.是一些待定字段
               },
               {
                   "type": "image",
                   "value": "URL",
                   "width": "4",
                   "height": "3",
                   "x": "0",
                   "y": "0",
                   "other": "其他"//比如一些margin,是否可点击,背景等等.是一些待定字段
               }
           ]
       }
   }
  1. 通过cols/rows对广告容器视图进行等分.简单坐标系如下

  2. 子视图的类型定义

    目前的type:

    {
        "text"  : TextView,
        "image" : Imageview,
        "gif"   : gif图片,
        "video" : 视频view
    }
    
  3. 根据x/y/width/height四个字段来确定投放子视图在第一步中划分的左边系中的位置和大小

    • 依照示例数据得到如下布局:

      • 图片view左上角点坐标:(0, 0), 宽高: (4, 3)
      • 文本view左上角点坐标:(0, 3), 宽高: (4, 1)

  4. 根据"type"映射成本第相应控件,根据"value"给相应的控件设置内容

  5. 再举个具体例子

    • 有如下布局

    • 后端投放比例如下:

    • 坐标:

      • 文本view左上角点坐标:(0, 0), 宽高: (2, 1)
      • 左边图片view左上角点坐标:(0, 1), 宽高: (1, 4)
      • 右边图片view左上角点坐标:(1, 1), 宽高: (1, 4)
    • json:

      {
          "data" : {
              "cols" : "2",
              "rows" : "5",
              "views" : [
                  {
                      "type" : "text",
                      "value" : "【烈火屠龙】3P顶级BOSS 猛爆绝品装备",
                      "width" : "2",
                      "height" : "1",
                      "x" : "0",
                      "y" : "0",
                      "other" : "其他"
                  }, {
                      "type" : "image",
                      "value" : "URL1", // 图片URL
                      "width" : "1",
                      "height" : "4",
                      "x" : "0",
                      "y" : "1",
                      "other" : "其他"
                  }, {
                      "type" : "image",
                      "value" : "URL2", // 图片URL
                      "width" : "1",
                      "height" : "4",
                      "x" : "1",
                      "y" : "1",
                      "other" : "其他"
                  }
              ]
          }
      }
      
      

核心代码

  • 出于方便我直接吧json放到本地assets下

    • Model

      //定义枚举,目前有4种类型.
          public enum TYPE {
              TEXT,
              IMAGE,
              GIF,
              VIDEO
          }
          private String type;
          private String value;
          private float width;
          private float height;
          private float x;
          private float y;
          private float font;
          private String background;
      //这里省略了get.set
      

  • Activity#onCreate()

     @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mFrameLayout = (FrameLayout) findViewById(R.id.activity_main);
    
            String s = readFile("sample.json", this);
            String data = JSON.parseObject(s).getString("data");
            mDataModel = JSON.parseObject(data, DataBean.class);
            if (mDataModel == null) {
                Log.e("zyk", "parse,error");
                return;
            }
            Log.d("zyk", mDataModel.toString());
    
            //create the dynamicView
            mFrameLayout.post(new Runnable() {
              //这里post是为了在DynamicView中拿到mFrameLayout的宽高信息,
              //因为在Activity中的onCreate()方法中传的,你懂得😏
                @Override
                public void run() {
                    View view = DynamicView.createView(MainActivity.this, mDataModel, mFrameLayout);//核心代码
                    if (view == null) {
                        return;
                    }
                    //add Layout Parameters in just created view
                    ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
                    view.setLayoutParams(layoutParams);
                    //add view to Activity'layout
                    mFrameLayout.addView(view);
                }
            });
        }
    
  • DynamicView

//拿到屏幕的宽高信息
               sWidthPixels = DensityUtil.getDevicesWidthPixels(context);
               sHeightPixels = DensityUtil.getDevicesHeightPixels(context);
               Log.d("zyk", "宽:" + sWidthPixels + ","+ "高" + sHeightPixels + ","
 + "密度" + DensityUtil.getDensityDpi(context));

//根据model数据计算出宽高
int x = (int) (data.getX() / cols) * parent.getWidth();
int y = (int) (data.getY() / rows) * parent.getHeight();
int width = (int) ((data.getWidth() / cols) * parent.getWidth());
int height = (int)((data.getHeight() / rows) * parent.getHeight());
//根据计算出的宽高和type,创建view
switch (data.getType()) {
            case TEXT:
                TextView textView = new TextView(context);
                textView.setX(x);//起始点坐标
                textView.setY(y);
                textView.setWidth(width);//宽高信息
                textView.setHeight(height);
                if (data.getValue() != null) {
                    textView.setText(data.getValue());//设置值
                }
                if (data.getFont() > 0) {
                    textView.setTextSize(data.getFont());//字体大小
                }
                if (data.getBackground() != null) {//背景颜色
                    textView.setBackgroundColor(Color.parseColor(data.getBackground()));
                }
    			....一些其他待补充属性
                view = textView;//待会返回这个view
                break;
            case IMAGE:
                ImageView imageView = new ImageView(context);
    			//下面的代码很挫,应该有更好的实现方式
                imageView.setX(x);
                imageView.setY(y);
                imageView.setMaxWidth(width);
                imageView.setMaxHeight(height);
                imageView.setAdjustViewBounds(true); 
                Glide.with(context).load(url).into(imageview);
                view = imageView;
                break;
            case GIF://gif要支持一些其他的特殊业务属性
    			.........
    

以上就是一些核心的代码.

  1. 主要在Activity的第20行.把view的创建交给DynamicView这个类处理,并把data数据和这个动态view的parent传递.
  2. 获取屏幕的宽高信息,获取parent的LayoutParams
  3. 根据data里面的数据按比例计算出真实的宽高信息和view坐上角坐标
  4. 根据model中定义的枚举type new 出具体的view并设置相应的宽高信息,及其属性.perfect~

待处理/正在做的功能(╯﹏╰)

  • 单个子视图view的展示属性细节完善
  • 逻辑事件处理
    • 点击
    • 图片是否支持手势缩放,图片圆角的处理
  • 布局都有padding, 将padding纳入坐标系划分将会增加坐标系复杂度
    • 通过给容器view以及子视图view增加"padding"字段, 控制位置大小微调, 可以较少坐标划分的复杂度
    • "padding" : [上, 右, 下, 左]
  • 其他....(老大一直在强调做计划要量化,这个词以后是不敢用了)
  • 合并成一个单独的库,方便外部调用

Demo目前已经完成.思路思想是最重要的.这种方案是可行的,同时支持iOS和Android.iOS那哥们还在埋头吭哧吭哧搞事情.他已经做到比我要细花很多.当然肯定会有更好的实现方式,在此仅仅是抛船引欲.(水手,快上船,船长已经迫不及待了~)

马上要过年了,2018年新年快乐哟,mua~