开发经典的瀑布流

本案例主要介绍WaterFlow容器的使用。如图所示对高度不相等的子组件,实现如瀑布般的紧密布局。

1. 案例效果截图

2. 案例运用到的知识点

2.1. 核心知识点

  • WaterFlow:瀑布流容器,由“行”和“列”分割的单元格所组成,通过容器自身的排列规则,将不同大小的“项目”自上而下,如瀑布般紧密布局。
  • FlowItem:瀑布流容器的子组件。
  • LazyForEach:LazyForEach从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当LazyForEach在滚动容器中使用了,框架会根据滚动容器可视区域按需创建组件,当组件划出可视区域外时,框架会进行组件销毁回收以降低内存占用。

2.2. 其他知识点

  • ArkTS 语言基础
  • V2版状态管理:@ComponentV2/@Provider/@Consumer
  • 自定义组件和组件生命周期
  • 内置组件:Column/Button
  • 日志管理类的编写
  • 常量与资源分类的访问
  • MVVM模式

3. 代码结构

├──entry/src/main/ets                     // 代码区
│  ├──common
│  │  ├──constants
│  │  │  └──CommonConstants.ets           // 公共常量类
│  │  └──utils
│  │     └──Logger.ets                    // 日志打印类
│  ├──entryability
│  │  └──EntryAbility.ets                 // 程序入口类
│  ├──pages
│  │  └──HomePage.ets                     // 主界面
│  ├──view
│  │  ├──ClassifyComponent.ets            // 分类信息类
│  │  ├──FlowItemComponent.ets            // 瀑布流Item组件类
│  │  ├──SearchComponent.ets              // 查询组件类
│  │  ├──SwiperComponent.ets              // banner组件类
│  │  └──WaterFlowComponent.ets           // 自定义组件类
│  └──viewmodel
│     ├──HomeViewModel.ets                // 瀑布流数据类
│     ├──ProductItem.ets                  // 产品信息类
│     └──WaterFlowDataSource.ets          // 瀑布流数据类
└──entry/src/main/resources               // 资源文件目录

4. 公共文件与资源

本案例涉及到的常量类和工具类代码如下:

  1. 通用常量类
// entry/src/main/ets/common/constants/CommonConstants.ets
export class CommonConstants {
  static readonly FULL_OPACITY: number = 1
  static readonly SIXTY_OPACITY: number = 0.6
  static readonly EIGHTY_OPACITY: number = 0.8
  static readonly FONT_WEIGHT_FIVE: number = 500
  static readonly WATER_FLOW_LAYOUT_WEIGHT: number = 1
  static readonly WATER_FLOW_COLUMNS_TEMPLATE: string = '1fr 1fr'
  static readonly FULL_WIDTH: string = '100%'
  static readonly FULL_HEIGHT: string = '100%'
  static readonly INVALID_INDEX: number = -1
}
  1. 日志类
// entry/src/main/ets/common/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit'

export default class Logger {
  private static domain: number = 0xFF00
  private static prefix: string = 'WaterFlow'
  private static format: string = '%{public}s, %{public}s'

  static debug(...args: string[]): void {
    hilog.debug(Logger.domain, Logger.prefix, Logger.format, args)
  }

  static info(...args: string[]): void {
    hilog.info(Logger.domain, Logger.prefix, Logger.format, args)
  }

  static warn(...args: string[]): void {
    hilog.warn(Logger.domain, Logger.prefix, Logger.format, args)
  }

  static error(...args: string[]): void {
    hilog.error(Logger.domain, Logger.prefix, Logger.format, args)
  }
}

本案例涉及到的资源文件如下:

  1. string.json
// entry/main/resources/base/element/string.json
{
  "string": [
    {
      "name": "module_desc",
      "value": "模块描述"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "WaterFlow组件的使用"
    },
    {
      "name": "title_bar_homepage",
      "value": "首页"
    },
    {
      "name": "title_bar_phone",
      "value": "手机"
    },
    {
      "name": "title_bar_computer",
      "value": "电脑"
    },
    {
      "name": "title_bar_foods",
      "value": "食品"
    },
    {
      "name": "title_bar_men_wear",
      "value": "男装"
    },
    {
      "name": "title_bar_fresh",
      "value": "生鲜"
    },
    {
      "name": "title_bar_furniture_kitchenware",
      "value": "家具厨具"
    },
    {
      "name": "title_bar_classification",
      "value": "分类"
    },
    {
      "name": "search_text",
      "value": "儿童餐椅"
    },
    {
      "name": "footer_text",
      "value": "已经到底了"
    }
  ]
}
  1. float.json
// entry/main/resources/base/element/float.json
{
  "float": [
    {
      "name": "smaller_font_size",
      "value": "12fp"
    },
    {
      "name": "small_font_size",
      "value": "14fp"
    },
    {
      "name": "middle_font_size",
      "value": "16fp"
    },
    {
      "name": "split_line_width",
      "value": "1vp"
    },
    {
      "name": "split_line_height",
      "value": "18vp"
    },
    {
      "name": "more_width",
      "value": "16vp"
    },
    {
      "name": "more_height",
      "value": "16vp"
    },
    {
      "name": "more_margin_left",
      "value": "2vp"
    },
    {
      "name": "more_margin_right",
      "value": "2vp"
    },
    {
      "name": "classify_title_margin",
      "value": "12vp"
    },
    {
      "name": "search_width",
      "value": "20vp"
    },
    {
      "name": "search_height",
      "value": "20vp"
    },
    {
      "name": "search_radius",
      "value": "20vp"
    },
    {
      "name": "search_margin_left",
      "value": "12vp"
    },
    {
      "name": "search_margin_right",
      "value": "8vp"
    },
    {
      "name": "search_swiper_height",
      "value": "40vp"
    },
    {
      "name": "search_margin_top",
      "value": "16vp"
    },
    {
      "name": "swiper_image_height",
      "value": "150vp"
    },
    {
      "name": "swiper_radius",
      "value": "16vp"
    },
    {
      "name": "swiper_margin_top",
      "value": "12vp"
    },
    {
      "name": "swiper_margin_bottom",
      "value": "12vp"
    },
    {
      "name": "image_background_height",
      "value": "222vp"
    },
    {
      "name": "home_margin_left",
      "value": "12vp"
    },
    {
      "name": "home_margin_right",
      "value": "12vp"
    },
    {
      "name": "product_layout_radius",
      "value": "8vp"
    },
    {
      "name": "product_layout_margin_left",
      "value": "12vp"
    },
    {
      "name": "product_layout_margin_right",
      "value": "12vp"
    },
    {
      "name": "product_layout_margin_bottom",
      "value": "12vp"
    },
    {
      "name": "product_image_size",
      "value": "132vp"
    },
    {
      "name": "water_flow_image_top",
      "value": "12vp"
    },
    {
      "name": "water_flow_image_bottom",
      "value": "8vp"
    },
    {
      "name": "discount_text_bottom",
      "value": "4vp"
    },
    {
      "name": "piece_line_height",
      "value": "19vp"
    },
    {
      "name": "promotion_font_size",
      "value": "10fp"
    },
    {
      "name": "promotion_radius",
      "value": "4vp"
    },
    {
      "name": "promotion_text_height",
      "value": "16vp"
    },
    {
      "name": "promotion_padding_left",
      "value": "4vp"
    },
    {
      "name": "promotion_padding_right",
      "value": "4vp"
    },
    {
      "name": "promotion_margin_top",
      "value": "6vp"
    },
    {
      "name": "promotion_margin_right",
      "value": "8vp"
    },
    {
      "name": "bonus_points_radius_width",
      "value": "0.5vp"
    },
    {
      "name": "bonus_points_margin_top",
      "value": "6vp"
    },
    {
      "name": "bonus_points_padding_left",
      "value": "8vp"
    },
    {
      "name": "bonus_points_padding_right",
      "value": "8vp"
    },
    {
      "name": "water_flow_columns_gap",
      "value": "8vp"
    },
    {
      "name": "water_flow_row_gap",
      "value": "8vp"
    },
    {
      "name": "footer_text_size",
      "value": "10vp"
    },
    {
      "name": "footer_text_height",
      "value": "20vp"
    }
  ]
}
  1. color.json
// entry/main/resources/base/element/color.json
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    },
    {
      "name": "indicator_select",
      "value": "#F74E42"
    },
    {
      "name": "focus_color",
      "value": "#E92F4F"
    },
    {
      "name": "home_background_color",
      "value": "#F1F3F5"
    }
  ]
}

其他资源请到源码中获取。

5. 首页

// entry/src/main/est/pages/HomePage.ets
import { CommonConstants as Const } from '../common/constants/CommonConstants'
import ClassifyComponent from '../view/ClassifyComponent'
import SearchComponent from '../view/SearchComponent'
import SwiperComponent from '../view/SwiperComponent'
import WaterFlowComponent from '../view/WaterFlowComponent'

@Entry
@Component
struct HomePage {
  build() {
    Stack({ alignContent: Alignment.Top }) {
      Image($r('app.media.ic_app_background'))
        .width(Const.FULL_WIDTH)
        .height($r('app.float.image_background_height'))
        .objectFit(ImageFit.Cover)
      
      Column() {
        SearchComponent()
        ClassifyComponent()
        SwiperComponent()
        WaterFlowComponent()
      }
      .padding({
        left: $r('app.float.home_margin_left'),
        right: $r('app.float.home_margin_right')
      })
    }
    .backgroundColor($r('app.color.home_background_color'))
  }
}

6. 搜索组件

// entry/src/main/est/view/SearchComponent.ets
import { CommonConstants as Const } from '../common/constants/CommonConstants'

@Component
export default struct SearchComponent {
  build() {
    Row() {
      Image($r('app.media.ic_search'))
        .width($r('app.float.search_width'))
        .height($r('app.float.search_height'))
        .margin({
          left: $r('app.float.search_margin_left'),
          right: $r('app.float.search_margin_right')
        })
      
      Text($r('app.string.search_text'))
        .fontSize($r('app.float.small_font_size'))
        .fontColor(Color.Black)
        .opacity(Const.SIXTY_OPACITY)
        .fontWeight(FontWeight.Normal)
    }
    .width(Const.FULL_WIDTH)
    .height($r('app.float.search_swiper_height'))
    .borderRadius($r('app.float.search_radius'))
    .backgroundColor(Color.White)
    .margin({ top: $r('app.float.search_margin_top') })
  }
}

7. 分类导航组件

// entry/src/main/view/ClassifyComponent.ets
import { classifyTitle } from '../viewmodel/HomeViewModel'
import { CommonConstants as Const } from '../common/constants/CommonConstants'

@Component
export default struct ClassifyComponent {
  @State titleIndex: number = 0

  build() {
    Flex({ justifyContent: FlexAlign.SpaceBetween }) {
      ForEach(classifyTitle, (item: Resource, index: number | undefined) => {
        if (index !== undefined) {
          Text(item)
            .fontSize($r('app.float.middle_font_size'))
            .opacity(this.titleIndex === index 
                     ? Const.FULL_OPACITY : Const.EIGHTY_OPACITY)
            .fontWeight(this.titleIndex === index 
                        ? Const.FONT_WEIGHT_FIVE : FontWeight.Normal)
            .fontColor(Color.White)
            .onClick(() => {
              this.titleIndex = index
            })
        }
      }, (item: Resource) => JSON.stringify(item))

      Row() {
        Image($r('app.media.ic_split_line'))
          .width($r('app.float.split_line_width'))
          .height($r('app.float.split_line_height'))
        Image($r('app.media.ic_more'))
          .width($r('app.float.more_width'))
          .height($r('app.float.more_height'))
          .margin({
            left: $r('app.float.more_margin_left'),
            right: $r('app.float.more_margin_right')
          })
        Text($r('app.string.title_bar_classification'))
          .fontSize($r('app.float.middle_font_size'))
          .fontColor(Color.White)
          .opacity(this.titleIndex === Const.INVALID_INDEX 
                   ? Const.FULL_OPACITY : Const.EIGHTY_OPACITY)
          .fontWeight(this.titleIndex === Const.INVALID_INDEX 
                      ? Const.FONT_WEIGHT_FIVE : FontWeight.Normal)
      }
      .onClick(() => {
        this.titleIndex = Const.INVALID_INDEX
      })
    }
    .width(Const.FULL_WIDTH)
    .margin({ top: $r('app.float.classify_title_margin') })
  }
}
// entry/src/main/ets/viewmodel/HomeViewModel.ets
const classifyTitle: Resource[] = [
  $r('app.string.title_bar_homepage'),
  $r('app.string.title_bar_phone'),
  $r('app.string.title_bar_computer'),
  $r('app.string.title_bar_foods'),
  $r('app.string.title_bar_men_wear'),
  $r('app.string.title_bar_fresh'),
  $r('app.string.title_bar_furniture_kitchenware')
]

export { classifyTitle }

8. 轮播图组件

// entry/src/main/ets/view/SwiperComponent.ets
import { CommonConstants as Const } from '../common/constants/CommonConstants'
import { swiperImage } from '../viewmodel/HomeViewModel'

@Component
export default struct SwiperComponent {
  private dotIndicator: DotIndicator = new DotIndicator()

  aboutToAppear(){
    this.dotIndicator.selectedColor($r('app.color.indicator_select'))
  }

  build() {
    Swiper() {
      ForEach(swiperImage, (item: Resource) => {
        Image(item)
          .width(Const.FULL_WIDTH)
          .height($r('app.float.swiper_image_height'))
          .borderRadius($r('app.float.swiper_radius'))
          .backgroundColor(Color.White)
      }, (item: Resource) => JSON.stringify(item))
    }
    .indicator(this.dotIndicator)
    .autoPlay(true)
    .itemSpace(0)
    .width(Const.FULL_WIDTH)
    .displayCount(1)
    .margin({
      top: $r('app.float.swiper_margin_top'),
      bottom: $r('app.float.swiper_margin_bottom')
    })
  }
}
// entry/src/main/ets/viewmodel/HomeViewModel.ets
const classifyTitle: Resource[] = [
  $r('app.string.title_bar_homepage'),
  $r('app.string.title_bar_phone'),
  $r('app.string.title_bar_computer'),
  $r('app.string.title_bar_foods'),
  $r('app.string.title_bar_men_wear'),
  $r('app.string.title_bar_fresh'),
  $r('app.string.title_bar_furniture_kitchenware')
]

const swiperImage: Resource[] = [
  $r('app.media.ic_home_appliances_special'),
  $r('app.media.ic_coupons'),
  $r('app.media.ic_internal_purchase_price')
]

export { classifyTitle, swiperImage }

9. 瀑布流组件

// entry/src/main/ets/view/WaterFlowComponent.ets
import ProductItem from '../viewmodel/ProductItem'
import { WaterFlowDataSource } from '../viewmodel/WaterFlowDataSource'
import { CommonConstants as Const } from '../common/constants/CommonConstants'
import { waterFlowData } from '../viewmodel/HomeViewModel'
import FlowItemComponent from '../view/FlowItemComponent'

@Component
export default struct WaterFlowComponent {
  private datasource: WaterFlowDataSource = new WaterFlowDataSource()

  aboutToAppear() {
    this.datasource.setDataArray(waterFlowData)
  }

  build() {
    WaterFlow({ footer: (): void => this.itemFoot() }) {
      LazyForEach(this.datasource, (item: ProductItem) => {
        FlowItem() {
          FlowItemComponent({ item: item })
        }
      }, (item: ProductItem) => JSON.stringify(item))
    }
    .layoutWeight(Const.WATER_FLOW_LAYOUT_WEIGHT)
    .layoutDirection(FlexDirection.Column)
    .columnsTemplate(Const.WATER_FLOW_COLUMNS_TEMPLATE)
    .columnsGap($r('app.float.water_flow_columns_gap'))
    .rowsGap($r('app.float.water_flow_row_gap'))
  }

  @Builder
  itemFoot() {
    Column() {
      Text($r('app.string.footer_text'))
        .fontColor(Color.Gray)
        .fontSize($r('app.float.footer_text_size'))
        .width(Const.FULL_WIDTH)
        .height($r('app.float.footer_text_height'))
        .textAlign(TextAlign.Center)
    }
  }
}
// entry/src/main/ets/viewmodel/ProductItem.ets
export interface IProductItem {
  image_url: Resource
  name: string
  discount: string
  price: string
  promotion: string
  bonus_points: string
}

export default class ProductItem implements IProductItem {
  image_url: Resource
  name: string
  discount: string
  price: string
  promotion: string
  bonus_points: string

  constructor(props: ProductItem) {
    this.image_url = props.image_url
    this.name = props.name
    this.discount = props.discount
    this.price = props.price
    this.promotion = props.promotion
    this.bonus_points = props.bonus_points
  }
}
// entry/src/main/ets/viewmodel/WaterFlowDataSource.ets
import ProductItem from './ProductItem'

export class WaterFlowDataSource implements IDataSource {
  private dataArray: ProductItem[] = []
  private listeners: DataChangeListener[] = []

  public setDataArray(productDataArray: ProductItem[]): void {
    this.dataArray = productDataArray
  }

  public totalCount(): number {
    return this.dataArray.length
  }

  public getData(index: number): ProductItem {
    return this.dataArray[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    let pos = this.listeners.indexOf(listener)
    if (pos >= 0) {
      this.listeners.splice(pos, 1)
    }
  }
}
// entry/src/main/ets/viewmodel/HomeViewModel.ets
import { IProductItem } from './ProductItem'

const classifyTitle: Resource[] = [
  $r('app.string.title_bar_homepage'),
  $r('app.string.title_bar_phone'),
  $r('app.string.title_bar_computer'),
  $r('app.string.title_bar_foods'),
  $r('app.string.title_bar_men_wear'),
  $r('app.string.title_bar_fresh'),
  $r('app.string.title_bar_furniture_kitchenware')
]

const swiperImage: Resource[] = [
  $r('app.media.ic_home_appliances_special'),
  $r('app.media.ic_coupons'),
  $r('app.media.ic_internal_purchase_price')
]

const waterFlowData: IProductItem[] = [
  {
    image_url: $r('app.media.ic_holder_50e'),
    name: 'XXX50E',
    discount: '',
    price: '¥4088',
    promotion: '',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_xs2'),
    name: 'XXXPadMate Xs 2 \n8GB+256GB (雅黑)',
    discount: '',
    price: '¥9999',
    promotion: '限时',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_computer'),
    name: 'XX设备  新品优惠!新品优惠!机不可失!失不再来!快来购买!快来购买!',
    discount: '限时省200',
    price: '¥10099',
    promotion: '商品',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_mouse'),
    name: '送给亲人  快速购买!',
    discount: '限时省200',
    price: '¥199',
    promotion: '',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_pad'),
    name: 'XXXPad Pro',
    discount: '',
    price: '¥3499',
    promotion: '',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_mate50'),
    name: 'XXXMate 50 8GB+256GB',
    discount: '',
    price: '¥5499',
    promotion: '',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_60pro'),
    name: 'XXX60 Pro 新品上市!\n你值得拥有!\n限时折扣!\n速速购买!',
    discount: '限时省200',
    price: '¥1299',
    promotion: '限时',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_50e'),
    name: 'XXX50E',
    discount: '',
    price: '¥4088',
    promotion: '',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_xs2'),
    name: 'XXXPadMate Xs 2 \n8GB+256GB (雅黑)',
    discount: '限时省200',
    price: '¥9999',
    promotion: '限时',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_computer'),
    name: 'XX设备  新品优惠!新品优惠!',
    discount: '限时省200',
    price: '¥10099',
    promotion: '商品',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_mouse'),
    name: '送给亲人  快速购买!',
    discount: '限时省200',
    price: '¥199',
    promotion: '限时',
    bonus_points: '赠送积分'
  },
  {
    image_url: $r('app.media.ic_holder_pad'),
    name: 'XXXPad Pro\n限时折扣!\n速速购买!\n机不可失!失不再来!',
    discount: '限时省200',
    price: '¥3499',
    promotion: '',
    bonus_points: '赠送积分'
  },
  {
    image_url: $r('app.media.ic_holder_mate50'),
    name: 'XXXMate 50 \n8GB+256GB',
    discount: '',
    price: '¥5499',
    promotion: '',
    bonus_points: ''
  },
  {
    image_url: $r('app.media.ic_holder_60pro'),
    name: 'XXX60 Pro',
    discount: '限时省200',
    price: '¥1299',
    promotion: '限时',
    bonus_points: '',
  }
];

export { classifyTitle, swiperImage, waterFlowData }
// entry/src/main/ets/view/FlowItemComponent.ets
import { CommonConstants as Const } from '../common/constants/CommonConstants'
import ProductItem from '../viewmodel/ProductItem'
import { waterFlowData } from '../viewmodel/HomeViewModel'

@Component
export default struct FlowItemComponent {
  item: ProductItem = waterFlowData[0]

  build() {
    Column() {
      Image(this.item?.image_url)
        .width($r('app.float.product_image_size'))
        .height($r('app.float.product_image_size'))
        .objectFit(ImageFit.Contain)
        .margin({
          top: $r('app.float.water_flow_image_top'),
          bottom: $r('app.float.water_flow_image_bottom')
        })
      Text(this.item?.name)
        .fontSize($r('app.float.small_font_size'))
        .fontColor(Color.Black)
        .fontWeight(FontWeight.Normal)
        .alignSelf(ItemAlign.Start)
      Text(this.item?.discount)
        .fontSize($r('app.float.smaller_font_size'))
        .fontColor(Color.Black)
        .fontWeight(FontWeight.Normal)
        .opacity(Const.SIXTY_OPACITY)
        .alignSelf(ItemAlign.Start)
        .margin({
          bottom: $r('app.float.discount_text_bottom')
        })
      Text(this.item?.price)
        .fontSize($r('app.float.middle_font_size'))
        .fontColor($r('app.color.focus_color'))
        .fontWeight(FontWeight.Normal)
        .alignSelf(ItemAlign.Start)
        .lineHeight($r('app.float.piece_line_height'))
      Row() {
        if (this.item?.promotion) {
          Text(`${this.item?.promotion}`)
            .height($r('app.float.promotion_text_height'))
            .fontSize($r('app.float.promotion_font_size'))
            .fontColor(Color.White)
            .borderRadius($r('app.float.promotion_radius'))
            .backgroundColor($r('app.color.focus_color'))
            .padding({
              left: $r('app.float.promotion_padding_left'),
              right: $r('app.float.promotion_padding_right')
            })
            .margin({
              top: $r('app.float.promotion_margin_top'),
              right: $r('app.float.promotion_margin_right')
            })
        }
        if (this.item?.bonus_points) {
          Text(`${this.item?.bonus_points}`)
            .height($r('app.float.promotion_text_height'))
            .fontSize($r('app.float.promotion_font_size'))
            .fontColor($r('app.color.focus_color'))
            .borderRadius($r('app.float.promotion_radius'))
            .borderWidth($r('app.float.bonus_points_radius_width'))
            .borderColor($r('app.color.focus_color'))
            .padding({
              left: $r('app.float.bonus_points_padding_left'),
              right: $r('app.float.bonus_points_padding_right')
            })
            .margin({ top: $r('app.float.bonus_points_margin_top') })
        }
      }
      .width(Const.FULL_WIDTH)
      .justifyContent(FlexAlign.Start)
    }
    .borderRadius($r('app.float.product_layout_radius'))
    .backgroundColor(Color.White)
    .padding({
      left: $r('app.float.product_layout_margin_left'),
      right: $r('app.float.product_layout_margin_right'),
      bottom: $r('app.float.product_layout_margin_bottom')
    })
  }
}

10. 代码与视频教程

完整案例代码与视频教程请参见:

代码:Code-06-02.zip。

视频:《开发经典的瀑布流》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值