Popup Issues, Transparent Background Setting, any Type Problem, Combination Gestures for Swipe and Drag



This content originally appeared on DEV Community and was authored by kouwei qing

[Daily HarmonyOS Next Knowledge]

Image description

1. Problems with HarmonyOS bindPopup?

Added a Popup for long-pressing each ListItem.

Problem 1: When using a @state variable at the outer level, long-pressing triggers multiple Popups. How to listen and display the Popup for the specific item pressed?

Problem 2: After clicking a Popup to delete the current item, how to listen for Popup clicks and make the Popup disappear after deletion?

Refer to the following demo:

import { router } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@Component
export struct Index {
  @State list: stockListData = new stockListData([])

  aboutToAppear(): void {
    const list: ItemInfo[] = []
    for (let i = 0; i < 50; ++i) {
      let stockName = "item" + i
      let bgState = 0
      list.push(new ItemInfo(stockName, bgState))
    }
    this.list.modifyAllData(list)

  }

  build() {
    Column() {
      Text("Start").height('50')
        .width('100%').onClick(() => {
      })
      List({ space: 10 }) {
        LazyForEach(this.list, (info: ItemInfo, index: number) => {
          ListItem() {
            ItemView({
              info: info, itemFun: (name) => {
                let itemIndex = 0
                for (let im of this.list.getAllData()) {
                  if (im.stockName === name) {
                    break
                  }
                  itemIndex++
                }
                console.info("animate--ItemView--aboutToReuse---delete Name=" + name)
                this.list.getAllData().splice(itemIndex, 1)
                this.list.notifyDataDelete(itemIndex)
              }
            })
              .onClick(() => {
              })
          }.height('80')
        }, (info: ItemInfo, index: number) => info.stockName + "type" + index)
      }
      .height('100%')
      .width('100%')
    }
    .height('100%')
    .width('100%')
  }
}

@Reusable
@Component
struct ItemView {
  @Prop info: ItemInfo
  private lastName: string = ""

  aboutToAppear(): void {
    this.lastName = this.info.stockName
  }

  aboutToReuse(params: Record<string, Object>): void {
    console.info("animate--ItemView--aboutToReuse---name=" + this.info.stockName + ",lastName=" + this.lastName)

    this.lastName = this.info.stockName
  }

  @State customPopup: boolean = false
  itemFun: (name: string) => void = (name) => {
  }

  @Builder
  popItemsView() {
    Text("qipao pop").onClick(() => {
      this.customPopup = !this.customPopup
      this.itemFun(this.info.stockName)
    })
  }

  build() {
    Column() {
      Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) {
        Column() {
          Text(this.info.stockName)
            .maxFontSize('16')
            .minFontSize('8')
            .maxLines(1)
            .margin({ bottom: 2 })
        }
        .width('36.27%')
        .alignItems(HorizontalAlign.Start)
        .padding({ left: '16', right: 8 })
      }
      .backgroundColor("#00ffea")
      .height('100%')
    }
    .bindPopup(this.customPopup, {
      builder: this.popItemsView, // Popup content
      placement: Placement.Top, // Popup position
      offset: { x: 0, y: 30 },
      radius: 12,
      popupColor: Color.Pink, // Popup background color
      onStateChange: (e) => {
        console.info('tag', JSON.stringify(e.isVisible))
        if (!e.isVisible) {
          this.customPopup = false
          // this.bgColor = this.info.topStatus == 1 ? $r('app.color.dialog_background') : $r('app.color.start_window_background')
        } else {
          this.customPopup = true
          // this.bgColor = $r('app.color.dialog_button')
        }
      }
    })
    .gesture(
      LongPressGesture({ repeat: false })// Setting repeat to true triggers continuous long-press events at intervals of duration (default 500ms)
        .onAction((event?: GestureEvent) => {
          console.log("tag", "LongPressGesture start event--customPopup=" + this.customPopup)
          if (event) {
            this.customPopup = !this.customPopup
          }
          // if (event && event.repeat) {
          // }
        })// Triggers when the long-press action ends
        .onActionEnd((event?: GestureEvent) => {
          if (event) {
          }
        })
    )
  }
}

@Observed
class ItemInfo {
  @Track
  stockName: string = "--"
  @Track
  bgState: number = 0

  constructor(name: string, bg: number) {
    this.stockName = name
    this.bgState = bg
  }
}


class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];

  public totalCount(): number {
    return 0;
  }

  public getData(index: number): ItemInfo | undefined {
    return undefined;
  }

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

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const position = this.listeners.indexOf(listener);
    if (position >= 0) {
      this.listeners.splice(position, 1);
    }
  }

  notifyDataReload(): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataReloaded();
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataAdd(index);
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataChange(index);
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataDelete(index);
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataMove(from, to);
    })
  }
}

export class stockListData extends BasicDataSource {
  private listData: ItemInfo[] = [];

  constructor(dataArray: ItemInfo[]) {
    super()
    this.listData = dataArray;
  }

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

  public getAllData(): ItemInfo[] {
    return this.listData;
  }

  public getData(index: number): ItemInfo {
    return this.listData[index];
  }

  public modifyAllData(data: ItemInfo[]): void {
    this.listData = data
    this.notifyDataReload()
  }
}

2. How to set a transparent background for a HarmonyOS @Entry page?

For the router routing mode, refer to the following demo:

// Page1.ets
import window from '@ohos.window';

@Entry
@Component
struct Page2 {
  @State message: string = 'page Page2';

  onPageHide() {
    console.log("pageHide")
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        Button("pageB").onClick(() => {
          let windowStege: window.WindowStage = AppStorage.get("windowStage") as window.WindowStage;
          windowStege.createSubWindow("hello", (err, win) => {
            win.setUIContent('pages/Page2');
            win.showWindow();
          })
        })
      }
      .width('100%')
    }
    .height('100%')
    .backgroundColor(Color.Pink)
  }
}

// Page2.ets
import window from '@ohos.window';

@Entry
@Component
struct Index {
  @State message: string = 'page Index';

  aboutToAppear() {
    window.findWindow("hello").setWindowBackgroundColor("#00000000")
  }

  onBackPress() {
    window.findWindow("hello").destroyWindow().then((res) => {
      console.log("destroyWindow success")
    }).catch(() => {
      console.log("destroyWindow fail")
    })
    return true
  }

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
      }
      .width('100%')
      .backgroundColor(Color.Red)
    }
    .alignItems(VerticalAlign.Top)
    .height('100%')
    .backgroundColor("#80ffffff")
  }
}

// Note: Add the key code in EntryAbility
onWindowStageCreate(windowStage: window.WindowStage): void {
  // Main window is created, set main page for this ability
  hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

  windowStage.loadContent('pages/Page231206095213071', (err, data) => {
  if (err.code) {
  hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
  return;
}
AppStorage.setAndLink("windowStage", windowStage);// Key code
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}

AppStorage.setAndLink(“windowStage”, windowStage);// This key code configures windowStage.loadContent(‘pages/page1’). The demo shows Page1 jumping to Page2, where Page2 has a transparent effect.

3. In HarmonyOS ArkTS ArkUI, how to define generics when any, null, etc., cannot be used, and parameters may be arrays, objects, or multi-level structures?

ArkTS does not support any, undefined, or unknown types. Explicitly specify specific types.

Modification solutions:

  1. All variables should explicitly specify their specific types.
  2. For literals, use Record<> with as to specify types.

How to define generics for component parameters:

Refer to the demo:

let strArr: string[] = ["java", "python", "C++"] 
let intArr: number[] = [1, 2, 3]
@Entry
@Component struct Index {
  build() {
    Row() {
      Column() {
        this.printArray(strArr) 
        Text('Divider')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.Green) 
        this.printArray(intArr)
      }
      .width('100%') 
    }
    .height('100%') 
  }
  @Builder printArray<T>(arr: T[]) {
    Column() { 
      ForEach(arr, (item: T) => {
        Text(String(item)) 
          .fontSize(50)
          .fontWeight(FontWeight.Bold) 
      }, (item: string) => item) 
    } 
  } 
}

4. How to write combination gestures for a control to recognize swipe and drag in HarmonyOS, and how to determine the swipe direction?

A control listens for gestures, directly operating on swipe and moving according to the drag distance on drag.

Refer to the following demo:

// xxx.ets
@Entry
@Component
struct Index {
  @State offsetX: number = 0;
  @State offsetY: number = 0;
  @State count: number = 0;
  @State positionX: number = 0;
  @State positionY: number = 0;
  @State borderStyles: BorderStyle = BorderStyle.Solid
  build() {
    Column() {
      Text('sequence gesture\n' + 'LongPress onAction:' + this.count + '\nPanGesture offset:\nX: ' + this.offsetX + '\n' + 'Y: ' + this.offsetY)
        .fontSize(28)
    }
    // Binding the translate property enables component position movement
    .translate({ x: this.offsetX, y: this.offsetY, z: 0 })
    .height(250)
    .width(300)
    // The following combination gestures are recognized in sequence; drag gestures will not trigger if long-press events do not fire normally
    .gesture(
      // Declare the combination gesture type as Sequence
      GestureGroup(GestureMode.Sequence,
        // The first trigger in the combination gesture is a long-press gesture, which can respond multiple times
        LongPressGesture({ repeat: true })
          // When the long-press gesture is recognized, increment the count displayed on the Text component
          .onAction((event: GestureEvent|undefined) => {
            if(event){
              if (event.repeat) {
                this.count++;
              }
            }
            console.info('LongPress onAction');
          })
          .onActionEnd(() => {
            console.info('LongPress end');
          }),
        // When dragging after a long-press, the PanGesture is triggered
        PanGesture()
          .onActionStart(() => {
            this.borderStyles = BorderStyle.Dashed;
            console.info('pan start');
          })
            // When the gesture is triggered, modify the component's displacement based on the drag distance from the callback
          .onActionUpdate((event: GestureEvent|undefined) => {
            if(event){
              this.offsetX = this.positionX + event.offsetX;
              this.offsetY = this.positionY + event.offsetY;
            }
            console.info('pan update');
          })
          .onActionEnd(() => {
            this.positionX = this.offsetX;
            this.positionY = this.offsetY;
            this.borderStyles = BorderStyle.Solid;
          })
      )
    )
  }
}

5. Are there specifications or documentation for HarmonyOS desktop icons?

For desktop icon and launch page icon resolution specifications, refer to: https://developer.huawei.com/consumer/cn/doc/design-guides-V1/app-icon-0000001188065088-V1

HarmonyOS app icon design aims to return to basics, accurately conveying functions, services, and branding through modern semantic expression. Visually, it balances aesthetics and recognizability; formally, it is inclusive and harmonious yet distinct.

Image description

HarmonyOS app icons follow these design principles:

  1. Conciseness and Elegance: Simple element icons with elegant line expressions convey design aesthetics.
  2. Rapid Meaning Conveyance: Icon graphics accurately communicate functions, services, and branding, with readability and recognizability.
  3. Emotional Expression: Summarize emotional expression through graphics and colors to convey brand visual identity.


This content originally appeared on DEV Community and was authored by kouwei qing