鸿蒙UI学习(一)对Java布局模板News_Ability的解析(上)

开发 后端
相信对于IDE模板的解析相信能够帮助我更好的运用这些组件。我第一个解析的模板选择了News_Ability模板。下面将分享我对这个模板的解析思路以及学习到的东西。

[[420493]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

前言

学习鸿蒙已经一个月了,这一个月学到了不少东西:服务卡片的制作,分布式数据库,分布式任务调度等等。。。但是这一个月来都是一些碎片化的学习为多,需要什么才去学习什么。我想来一次更为系统的学习,于是选择从UI的制作开始一步步地学习。对于UI学习,以为相比于文档上面枯燥的说教(当然文档还是得好好看,毕竟基础很重要),解析IDE里面所自带的UI模板肯定是更加有趣的方式。事实上,解析模板比我想象中学习的东西要多,很多我以为是这样做的,却发现别人是那样做的。相信对于IDE模板的解析相信能够帮助我更好的运用这些组件。我第一个解析的模板选择了News_Ability模板。下面将分享我对这个模板的解析思路以及学习到的东西。如果想要查看更多详细的学习笔记,

News_Ability模板中的布局与组件

布局:Directional布局

组件:Image,Text,TextField,ListContainer,ScrollView

布局分析

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

第一个页面的分析:由图片可见,第一个页面主要就是两个部分,一是新闻种类的选择栏,二是新闻概览的栏。一开始我天真的认为新闻种类选择栏是通过TabList实现的,但通过查看代码发现这两个板块都是通过ListContainer实现的。

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

第二个页面分析:页面的顶部是文章的题目,阅读量和点赞数,显然都是用Text组件实现,并且猜测是用一个水平方向得Directionnal组件装起来得。接下来是一个ScrollView组件,使用这个组件的理由也很容易理解,文章的内容无法保障在当前屏幕就全部展示完毕,需要有一个把内容下拉的组件。最底部就是评论输入栏,以及点赞,转发,收藏(疯狂暗示)等操作,查看代码得知:分别是使用TextField,Image组件来实现的。

组件ListContainer的使用

组件ListContainer的使用相对比较复杂,我觉得还是有必要把文档里面的东西搬运一下。先呈上官方文档官网文档链接

概括地来说,LisiContainer的使用有如下几步

1.在layout目录下,AbilitySlice对应的布局文件page_listcontainer.xml文件中创建ListContainer。

  1. <ListContainer 
  2.     ohos:id="$+id:list_container" 
  3.     ohos:height="200vp" 
  4.     ohos:width="300vp" 
  5.     ohos:layout_alignment="horizontal_center"/> 

 2.在layout目录下新建xml文件(例:item_sample.xml),作为ListContainer的子布局。

  1. <?xml version="1.0" encoding="utf-8"?> 
  2. <DirectionalLayout 
  3.     xmlns:ohos="http://schemas.huawei.com/res/ohos" 
  4.     ohos:height="match_content" 
  5.     ohos:width="match_parent" 
  6.     ohos:left_margin="16vp" 
  7.     ohos:right_margin="16vp" 
  8.     ohos:orientation="vertical"
  9.     <Text 
  10.         ohos:id="$+id:item_index" 
  11.         ohos:height="match_content" 
  12.         ohos:width="match_content" 
  13.         ohos:padding="4vp" 
  14.         ohos:text="Item0" 
  15.         ohos:text_size="20fp" 
  16.         ohos:layout_alignment="center"/> 
  17. </DirectionalLayout> 

 3.创建SampleItem.java,作为ListContainer的数据包装类

  1. public class SampleItem { 
  2.     private String name
  3.     public SampleItem(String name) { 
  4.         this.name = name
  5.     } 
  6.     public String getName() { 
  7.         return name
  8.     } 
  9.     public void setName(String name) { 
  10.         this.name = name
  11.     } 

 4.ListContainer每一行可以为不同的数据,因此需要适配不同的数据结构,使其都能添加到ListContainer上。创建SampleItemProvider.java,继承自BaseItemProvider。必须重写的方法如下:

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

示例代码如下:

  1. // 请根据实际工程/包名引入 
  2. import com.example.myapplication.ResourceTable; 
  3. import ohos.aafwk.ability.AbilitySlice; 
  4. import ohos.agp.components.*; 
  5. import java.util.List; 
  6. public class SampleItemProvider extends BaseItemProvider { 
  7.     private List<SampleItem> list; 
  8.     private AbilitySlice slice; 
  9.     public SampleItemProvider(List<SampleItem> list, AbilitySlice slice) { 
  10.         this.list = list; 
  11.         this.slice = slice; 
  12.     } 
  13.     @Override 
  14.     public int getCount() { 
  15.         return list == null ? 0 : list.size(); 
  16.     } 
  17.     @Override 
  18.     public Object getItem(int position) { 
  19.         if (list != null && position >= 0 && position < list.size()){ 
  20.             return list.get(position); 
  21.         } 
  22.         return null
  23.     } 
  24.     @Override 
  25.     public long getItemId(int position) { 
  26.         return position; 
  27.     } 
  28.     @Override 
  29.     public Component getComponent(int position, Component convertComponent, ComponentContainer componentContainer) { 
  30.         final Component cpt; 
  31.         if (convertComponent == null) { 
  32.             cpt = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_item_sample, nullfalse); 
  33.         } else {  
  34.             cpt = convertComponent; 
  35.         } 
  36.         SampleItem sampleItem = list.get(position); 
  37.         Text text = (Text) cpt.findComponentById(ResourceTable.Id_item_index); 
  38.         text.setText(sampleItem.getName()); 
  39.         return cpt; 
  40.     } 

5.在Java代码中添加ListContainer的数据,并适配其数据结构。

  1. @Override 
  2.  public void onStart(Intent intent) { 
  3.      super.onStart(intent); 
  4.      super.setUIContent(ResourceTable.Layout_page_listcontainer); 
  5.      initListContainer(); 
  6.  } 
  7.  private void initListContainer() { 
  8.      ListContainer listContainer = (ListContainer) findComponentById(ResourceTable.Id_list_container); 
  9.      List<SampleItem> list = getData(); 
  10.      SampleItemProvider sampleItemProvider = new SampleItemProvider(list, this); 
  11.      listContainer.setItemProvider(sampleItemProvider); 
  12.  } 
  13.  private ArrayList<SampleItem> getData() { 
  14.      ArrayList<SampleItem> list = new ArrayList<>(); 
  15.      for (int i = 0; i <= 8; i++) { 
  16.          list.add(new SampleItem("Item" + i)); 
  17.      } 
  18.      return list; 
  19.  } 

 6.listContainer在sampleItemProvider 初始化后修改数据。

  1. private void initListContainer() { 
  2.      ListContainer listContainer = (ListContainer) findComponentById(ResourceTable.Id_list_container); 
  3.      List<SampleItem> list = getData(); 
  4.      SampleItemProvider sampleItemProvider = new SampleItemProvider(list, this); 
  5.      listContainer.setItemProvider(sampleItemProvider); 
  6.      list.add(new SampleItem("Item" + sampleItemProvider.getCount())); 
  7.      listContainer.setBindStateChangedListener(new Component.BindStateChangedListener() { 
  8.          @Override 
  9.          public void onComponentBoundToWindow(Component component) { 
  10.              // ListContainer初始化时数据统一在provider中创建,不直接调用这个接口; 
  11.              // 建议在onComponentBoundToWindow监听或者其他事件监听中调用。 
  12.              sampleItemProvider.notifyDataChanged(); 
  13.          } 
  14.  
  15.          @Override 
  16.          public void onComponentUnboundFromWindow(Component component) {} 
  17.      }); 
  18.  } 

新闻列表界面的解析

xml文件解析

新闻列表界面的UI效果如下图:

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

打开文件一看,里面有很多布局文件,但是想要知道呈现在我们眼前的这一画面究竟是哪一个文件其实很简单。因为程序首先运行的是MainAbility类,所以打开MainAbility类,可以看见下图:

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

上面这幅图中setMainRoute方法指向了MainAbilityListSlice类,即路由到MainAbilityListSlice类上了,所以显示在我们面前的画面是MainAbilitySlice所加载的布局。于是,我们就继续到MainAbilityListSlice类的代码中去找设置布局的语句。

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

哎,这不就是找到了这个布局到底是谁了吗?接下来我们就可以到resources包里的layout文件夹中解析xml布局代码了!我们点开这个xml布局文件:

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

这是一个DirectionalLayout,方向是垂直的(vertical)。里面有两个列表,一个是水平方向的,应该就是新闻种类的选择栏了,另一个在这里并没有在这里显式设定方向,不过显然可以推断这个就是新闻内容概览的列表。中间的Component我们暂时还不知道它的作用,不过,我们会知道的。既然已经有了两个ListContainer,根据上文我们对这个组件的认识,可以推断肯定有两个xml文件分别用来编辑这两个ListContainer里面内容的样式的,查看layout文件夹里面的xml文件,根据文件名我们就可以推测哪两个文件是用来编辑ListContainer里的内容的样式的。

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

当然,文件名只是起到帮助我们寻找的作用,想要更加严谨的话,还是得到java代码中去寻找语句,打开NewsTypeProvider类,可以看见如下语句,这便验证了我们的猜测了。(同理,另一个ListContainer也是这样操作)

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

接下来就打开这些编辑样式,打开之前,出于学习的目的,我想我们要先有一个自己大概的猜想,把打开文件作为一种验证的方法。

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

新闻种类选择栏很简单,我猜测只有一个Text组件,事实也是这样的。新闻概览的样式如上图,由图中我们猜测,这是由一个水平方向的Directional布局组织起来的额组件,里面放着一个用来显示文章标题的Text组件和一个用来显示文章图片的Image组件。接下来,让我们来看看是不是这样的吧!这里由于截图大小的问题,就选择把代码复制下来。

  1. <DirectionalLayout 
  2.     xmlns:ohos="http://schemas.huawei.com/res/ohos" 
  3.     ohos:height="110vp" 
  4.     ohos:width="match_parent" 
  5.     ohos:orientation="vertical"
  6.  
  7.     <!--水平的方向布局,是概览的每一条新闻的样式--> 
  8.     <DirectionalLayout 
  9.         ohos:height="109.5vp" 
  10.         ohos:width="match_parent" 
  11.         ohos:orientation="horizontal" 
  12.         ohos:padding="10vp"
  13.  
  14.         <!--文章标题--> 
  15.         <Text 
  16.             ohos:id="$+id:item_news_title" 
  17.             ohos:height="match_content" 
  18.             ohos:width="0vp" 
  19.             ohos:max_text_lines="3" 
  20.             ohos:multiple_lines="true" 
  21.             ohos:right_padding="20vp" 
  22.             ohos:text_size="18vp" 
  23.             ohos:weight="3"/> 
  24.         <!--文章图片--> 
  25.         <Image 
  26.             ohos:id="$+id:item_news_image" 
  27.             ohos:height="match_parent" 
  28.             ohos:width="0vp" 
  29.             ohos:scale_mode="stretch" 
  30.             ohos:weight="2"/> 
  31.  
  32.     </DirectionalLayout> 
  33.  
  34.     <Component 
  35.         ohos:height="0.5vp" 
  36.         ohos:width="match_parent" 
  37.         ohos:background_element="#FF162AAB" 
  38.         /> 
  39. </DirectionalLayout> 

We are almost right.但是,细心点就会发现这里有两个问题:1.父布局是一个垂直方向的Directional布局,里面才包含一个水平方向的Directional子布局。2.除了我们的猜测之外,Component组件又出现了,并且它的底色是白色。也许到了有必要搞清楚Component组件是什么东西的时候了。在我学习布局的时候会经常用到一个方法:改颜色。默认颜色都是白色导致我们有时候很难分辨我们每个组件的大小,位置等,把颜色改成便于区分,花里胡哨的,会有利于我们这些初学者学习UI布局。

这里把Component的颜色改一下,就会发现它的作用类似于一个分界线,并且它是一条很细的线,这是因为它的height只有0.5vp。那我们就改一改它的参数,验证一下我们的猜想。让我们整活起来:把底色改成喜庆的大红,再把高度给他调成20vp。

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

这里有一点需要注意的,因为根布局的高度以及其中一个子Directional布局(并且这个布局在Component的上面)的高度已经确定了,所以只更改一个参数是不够的,再作如下更改:

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

顺便也把前面的Component组件的参数改一下,运行效果如下:

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

好家伙,有点过年那味儿了,当然我的心情也跟这颜色一样喜庆,因为这不单验证了我的猜想,我还学会了新的布局方法:加一个Component组件来使各组件有一个分隔的效果!至此,xml文件我们已经搞得清清楚楚了!接下来就去看看java代码里面有什么可以学习的地方吧!

Java代码解析

根据上面我们对ListContainer组件的初步认识,在完成了对xml文件的编辑之后的一步是创建数据包装类。这个数据包装类放在beans包里面,里面有NewsType和NewsInfo两个类,前者很简单,就不拆开看了,我们粗略地看一下后者。

【木棉花】UI学习(一)对Java布局模板News_Ability的解析(上)-鸿蒙HarmonyOS技术社区

数据封装类中的属性分别为:标题,种类,图片路径,阅读量,点赞量,内容。剩下的是一些设置或者获得这些属性的方法。

下一步呢?是的,我们去看看Provider类吧,打开provider文件夹,挑选更为复杂一点的NewsListProvider类打开看看吧,这个类继承自BaseItemProvider(BaseItemProvider文档链接),且必须重写四个方法。这个类有两个属性,一个是存放新闻信息的list,一个是上下文。类里面还藏有一个静态内部类ViewHolder。代码如下

  1. public class NewsListProvider extends BaseItemProvider { 
  2.     private List<NewsInfo> newsInfoList; 
  3.     private Context context; 
  4.  
  5.     /** 
  6.      * constructor function 
  7.      * 
  8.      * @param listBasicInfo list info 
  9.      * @param context       context 
  10.      */ 
  11.     public NewsListProvider(List<NewsInfo> listBasicInfo, Context context) { 
  12.         this.newsInfoList = listBasicInfo; 
  13.         this.context = context; 
  14.     } 
  15.     //必须重写的方法1:返回填充的表项个数 
  16.     @Override 
  17.     public int getCount() { 
  18.         return newsInfoList == null ? 0 : newsInfoList.size(); 
  19.     } 
  20.     //必须重写的方法2:根据id(表项的位置)返回表项 
  21.     @Override 
  22.     public Object getItem(int position) { 
  23.         return newsInfoList.get(position); 
  24.     } 
  25.     //必须重写的方法3:返回表项的id 
  26.     @Override 
  27.     public long getItemId(int position) { 
  28.         return position; 
  29.     } 
  30.     //必须重写的方法4:根据id返回组件 
  31.     @Override 
  32.     public Component getComponent(int position, Component component, ComponentContainer componentContainer) { 
  33.         ViewHolder viewHolder; 
  34.         Component temp = component; 
  35.         //如果还没有component,那么就进行如下操作: 
  36.         if (temp == null) { 
  37.             //1.获得组件的布局 
  38.             temp = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_item_news_layout, nullfalse); 
  39.             //2.创建一个viewHolder,并设置它的属性 
  40.             viewHolder = new ViewHolder(); 
  41.             //设置它的标题:从temp中的布局找到里面的Text组件,并将其设置成这一个viewHolder的标题 
  42.             viewHolder.title = (Text) temp.findComponentById(ResourceTable.Id_item_news_title); 
  43.             //设置它的图片:从temp中的布局找到里面的Image组件,并将其设置成这一个viewHolder的图片 
  44.             viewHolder.image = (Image) temp.findComponentById(ResourceTable.Id_item_news_image); 
  45.             //设置这个组件的标签,viewHolder就是这个组件的标签 
  46.             temp.setTag(viewHolder); 
  47.         } 
  48.         //如果已经有component了,通过组件的标签来获得viewHolder里面的内容 
  49.         else { 
  50.             viewHolder = (ViewHolder) temp.getTag(); 
  51.         } 
  52.         viewHolder.title.setText(newsInfoList.get(position).getTitle()); 
  53.         viewHolder.image.setPixelMap(CommonUtils.getPixelMapFromPath(context, newsInfoList.get(position).getImgUrl())); 
  54.         //返回这个子组件 
  55.         return temp
  56.     } 
  57.  private static class ViewHolder 
  58.     { 
  59.         Text title; 
  60.         Image image; 
  61.     } 

每一语句的内容都注释到上面的代码里面了,因此不再赘述。准备工作都做好了,下面回到MainAbilityListSlice类中继续看代码。接下来一步,根据上面的ListContainer组件的使用方法,我们要在Java代码中添加ListContainer的数据,并适配其数据结构。详细的解析在代码中,如下:

  1. private void initData() 
  2.    { 
  3.        //得到新闻类型的数据,并创建一个类型为NewsType的列表来存放这些数据,由于这些数据都放在路径为"entry/resources/rawfile/news_type_datas.json"的json文件中,所以用如下语句提取 
  4.        List<NewsType> newsTypeList = 
  5.                ZSONArray.stringToClassList( 
  6.                        CommonUtils.getStringFromJsonPath(this, "entry/resources/rawfile/news_type_datas.json"), 
  7.                        NewsType.class); 
  8.        //得到所有新闻概览的数据,并创建一个类型为NewsInfo的列表来存放这些数据,由于这些数据都放在路径为"entry/resources/rawfile/news_datas.json"的json文件中,所以用如下语句提取 
  9.        totalNewsDataList = 
  10.                ZSONArray.stringToClassList( 
  11.                        CommonUtils.getStringFromJsonPath(this, "entry/resources/rawfile/news_datas.json"), 
  12.                        NewsInfo.class); 
  13.  
  14.        //创建一个用来存储新闻数据的数据结构ArrayList 
  15.        newsDataList = new ArrayList<>(); 
  16.        newsDataList.addAll(totalNewsDataList); 
  17.  
  18.        //实例化下面两个类 
  19.        newsTypeProvider = new NewsTypeProvider(newsTypeList, this); 
  20.        newsListProvider = new NewsListProvider(newsDataList, this); 
  21.    } 

然后就是设置响应点击的监听器,在这个模板中,点击新闻种类栏的项可以筛选新闻,并且新闻种类项的样式会发生变化,点击新闻概览的项可以跳转到详细页面。由已经有的代码可以猜测前者是通过改变Listcontainer的项目的内容来实现,而后者则是通过带参数的页面跳转来实现的。代码及详细注释如下:

  1. private void initListener() { 
  2.        //新闻种类选择栏的项被点击时的回调事件 
  3.        selectorListContainer.setItemClickedListener( 
  4.                // 
  5.                (listContainer, component, position, id) -> { 
  6.                    //当被点击到,不着急变样式,先获得这个Text 
  7.                    setCategorizationFocus(false); 
  8.                    selectText = (Text) component.findComponentById(ResourceTable.Id_news_type_text); 
  9.                    //获得完了,再进行样式变换 
  10.                    setCategorizationFocus(true);//样式变换 
  11.                    //清除现有的newsDataList,为的是为下面更新newsDataList作准备,这样就可以做到跟新闻概览框的内容与新闻种类相对应 
  12.                    newsDataList.clear(); 
  13.                    for (NewsInfo mTotalNewsData : totalNewsDataList) { 
  14.                        //把属于当前被点击的新闻种类的新闻数据加进newsDataList中,做到同步的效果 
  15.                        if (selectText.getText().equals(mTotalNewsData.getType()) || position == 0) { 
  16.                            newsDataList.add(mTotalNewsData); 
  17.                        } 
  18.                    } 
  19.                    //更新列表内容 
  20.                    updateListView(); 
  21.                }); 
  22.        //新闻概览表中的元素被点击的时候,进行跳转操作 
  23.        newsListContainer.setItemClickedListener( 
  24.                (listContainer, component, position, id) -> { 
  25.                    Intent intent = new Intent(); 
  26.                    Operation operation = 
  27.                            new Intent.OperationBuilder() 
  28.                                    .withBundleName(getBundleName()) 
  29.                                    .withAbilityName(MainAbility.class.getName()) 
  30.                                    .withAction("action.detail"
  31.                                    .build(); 
  32.                    intent.setOperation(operation); 
  33.  
  34.                    //设置跳转携带的参数 
  35.                    intent.setParam(MainAbilityDetailSlice.INTENT_TITLE, newsDataList.get(position).getTitle()); 
  36.                    intent.setParam(MainAbilityDetailSlice.INTENT_READ, newsDataList.get(position).getReads()); 
  37.                    intent.setParam(MainAbilityDetailSlice.INTENT_LIKE, newsDataList.get(position).getLikes()); 
  38.                    intent.setParam(MainAbilityDetailSlice.INTENT_CONTENT, newsDataList.get(position).getContent()); 
  39.                    intent.setParam(MainAbilityDetailSlice.INTENT_IMAGE, newsDataList.get(position).getImgUrl()); 
  40.                    startAbility(intent); 
  41.                }); 
  42.    } 

到此,第一个页面的实现思路已经被我们剖析得很清楚了。不知不觉已经码了8000字,如果再把第二个页面在本贴中剖析,恐沦为“又长又臭”之作,因此决定把这个模板的解析分为两篇文章,这样大家读起来也清晰一点,我写起来也轻松一点。那么就先对本文来个小结吧!

小结

1.在解析这个模板之前,我一直以为新闻种类栏使用的是Tablist组件来实现的。使用Tablist会出现的问题就是如果标签过多,多到溢出屏幕,无法做到滑动就能够查看的效果(如果可以做到,各位大佬能否指点一二?)。

2.解析过程中我学习到了ListContainer的使用方法,并且对其有了较为深刻的认识。

3.解析思路,从MainAbility出发,顺藤摸瓜,遇到陌生的组件就去查看文档,然后配合模板的实战例子使用,效果杠杠的。点开详细代码前,须有自己的想法或者是大概的思路。

4.不了解的功能的组件可以通过设置一个辣眼睛的背景颜色来凸显它,这个方法在自己写布局的时候也很好用。

5.Component可作为分界的手段。

更多资料请关注我们的项目 :Awesome-HarmonyOS_木棉花

本项目会长期更新 ,希望随着鸿蒙一同成长变强的既有我们,也有正在看着这个项目的你。明年3月,深大校园内的木棉花会盛开,那时,鸿蒙也会变的更好,愿这花开,有你我的一份。

文章相关附件可以点击下面的原文链接前往下载

UI_template_news_ability.zip

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-07-01 09:19:56

鸿蒙HarmonyOS应用

2021-06-28 14:41:36

鸿蒙HarmonyOS应用

2021-06-18 14:55:57

鸿蒙HarmonyOS应用

2021-05-28 17:01:49

鸿蒙HarmonyOS应用

2014-01-15 10:06:49

YahooNews Digest新闻客户端

2020-11-17 11:48:44

HarmonyOS

2022-02-17 21:05:26

AbilityJS FAJava PA

2011-05-11 14:23:07

路由IS-IS

2011-04-22 11:00:17

运维

2010-02-05 14:54:56

Android UI

2020-11-25 12:02:02

TableLayout

2021-01-20 13:50:36

鸿蒙HarmonyOS应用开发

2010-08-05 13:14:16

Flex布局

2010-09-02 13:53:58

CSS Sprites

2017-05-24 10:12:54

前端FlexboxCSS3

2009-07-21 17:31:39

iBATIS一对多映射

2021-09-18 14:40:37

鸿蒙HarmonyOS应用

2021-08-12 15:01:09

鸿蒙HarmonyOS应用

2010-02-01 10:40:13

Python Djan

2010-09-14 08:53:06

DIVTable
点赞
收藏

51CTO技术栈公众号