package libs import ( "encoding/json" "fmt" "time" ) // UiObject 表示一个 Android UI 控件对象 // 通过 Selector 定位,支持点击、输入、滑动等操作 // 对应 Python 版本的 UiObject 类 type UiObject struct { device *Device selector *Selector jsonrpc *JsonRpcWrapper } // NewUiObject 创建一个新的 UiObject func NewUiObject(device *Device, selector *Selector) *UiObject { return &UiObject{ device: device, selector: selector, jsonrpc: device.JsonRpc(), } } // Selector 返回当前 UiObject 的选择器 func (u *UiObject) Selector() *Selector { return u.selector } // ---------- 等待和存在性 ---------- // Exists 检查 UI 元素是否存在于当前窗口 func (u *UiObject) Exists() (bool, error) { raw, err := u.jsonrpc.Call("objInfo", []interface{}{u.selector.ToMap()}, 10) if err != nil { // UiObjectNotFoundError 意味着不存在 if _, ok := err.(*UiObjectNotFoundError); ok { return false, nil } return false, err } return raw != nil, nil } // Wait 等待 UI 元素出现或消失 // exists: true 等待出现,false 等待消失 // timeout: 超时时间(秒),0 使用默认值 // // 通过 JSON-RPC 调用服务端的 waitForExists/waitUntilGone 实现 // 使用 adb forward 端口转发,连接稳定,与 Python 版本行为一致 func (u *UiObject) Wait(exists bool, timeout float64) (bool, error) { if timeout <= 0 { timeout = u.device.WaitTimeout() } if timeout <= 0 { timeout = 10.0 } httpWait := timeout + 10 if exists { raw, err := u.jsonrpc.Call("waitForExists", []interface{}{u.selector.ToMap(), int(timeout * 1000)}, httpWait) if err != nil { // HTTP 超时时回退到 Exists 检查 if _, ok := err.(*HTTPError); ok { ex, _ := u.Exists() return ex, nil } return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // 等待消失 raw, err := u.jsonrpc.Call("waitUntilGone", []interface{}{u.selector.ToMap(), int(timeout * 1000)}, httpWait) if err != nil { if _, ok := err.(*HTTPError); ok { ex, _ := u.Exists() return !ex, nil } return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // WaitGone 等待 UI 元素消失 func (u *UiObject) WaitGone(timeout float64) (bool, error) { return u.Wait(false, timeout) } // MustWait 等待元素出现,不存在则返回 UiObjectNotFoundError func (u *UiObject) MustWait(timeout float64) error { found, err := u.Wait(true, timeout) if err != nil { return err } if !found { return &UiObjectNotFoundError{ Code: -32002, Message: fmt.Sprintf("等待超时: %s", u.selector.String()), Params: u.selector.ToMap(), } } return nil } // ---------- 元素信息 ---------- // ObjInfo 包含 UI 元素的详细信息 type ObjInfo struct { Text string `json:"text"` ClassName string `json:"className"` ContentDescription string `json:"contentDescription"` PackageName string `json:"packageName"` ResourceName string `json:"resourceName"` Checkable bool `json:"checkable"` Checked bool `json:"checked"` Clickable bool `json:"clickable"` Enabled bool `json:"enabled"` Focusable bool `json:"focusable"` Focused bool `json:"focused"` LongClickable bool `json:"longClickable"` Scrollable bool `json:"scrollable"` Selected bool `json:"selected"` Bounds map[string]int `json:"bounds"` VisibleBounds map[string]int `json:"visibleBounds"` ChildCount int `json:"childCount"` Extra map[string]interface{} `json:"-"` // 额外字段 } // Info 获取 UI 元素信息 func (u *UiObject) Info() (*ObjInfo, error) { raw, err := u.jsonrpc.Call("objInfo", []interface{}{u.selector.ToMap()}) if err != nil { return nil, err } var info ObjInfo if err := json.Unmarshal(raw, &info); err != nil { return nil, err } return &info, nil } // InfoRaw 获取 UI 元素的原始 map 信息 func (u *UiObject) InfoRaw() (map[string]interface{}, error) { raw, err := u.jsonrpc.Call("objInfo", []interface{}{u.selector.ToMap()}) if err != nil { return nil, err } var info map[string]interface{} if err := json.Unmarshal(raw, &info); err != nil { return nil, err } return info, nil } // InfoList 获取所有匹配元素的信息列表 func (u *UiObject) InfoList() ([]map[string]interface{}, error) { raw, err := u.jsonrpc.Call("objInfoOfAllInstances", []interface{}{u.selector.ToMap()}) if err != nil { return nil, err } var list []map[string]interface{} if err := json.Unmarshal(raw, &list); err != nil { return nil, err } return list, nil } // ---------- 边界和坐标 ---------- // Bounds 获取元素的边界坐标 (left, top, right, bottom) func (u *UiObject) Bounds() (int, int, int, int, error) { info, err := u.InfoRaw() if err != nil { return 0, 0, 0, 0, err } // 优先使用 visibleBounds bounds, ok := info["visibleBounds"].(map[string]interface{}) if !ok { bounds, ok = info["bounds"].(map[string]interface{}) if !ok { return 0, 0, 0, 0, fmt.Errorf("无法获取元素边界") } } lx := int(bounds["left"].(float64)) ly := int(bounds["top"].(float64)) rx := int(bounds["right"].(float64)) ry := int(bounds["bottom"].(float64)) return lx, ly, rx, ry, nil } // Center 获取元素中心坐标 // offset: [xoff, yoff],(0,0) 表示左上角,(0.5,0.5) 表示中心 func (u *UiObject) Center(offset ...float64) (int, int, error) { xoff, yoff := 0.5, 0.5 if len(offset) >= 2 { xoff, yoff = offset[0], offset[1] } lx, ly, rx, ry, err := u.Bounds() if err != nil { return 0, 0, err } width := rx - lx height := ry - ly x := lx + int(float64(width)*xoff) y := ly + int(float64(height)*yoff) return x, y, nil } // ---------- 点击操作 ---------- // Click 点击 UI 元素 // timeout: 等待元素出现的超时时间(秒),0 使用默认值 func (u *UiObject) Click(timeout ...float64) error { t := 0.0 if len(timeout) > 0 { t = timeout[0] } if err := u.MustWait(t); err != nil { return err } x, y, err := u.Center() if err != nil { return err } return u.device.Click(x, y) } // ClickWithOffset 带偏移量点击 UI 元素 func (u *UiObject) ClickWithOffset(xoff, yoff float64, timeout ...float64) error { t := 0.0 if len(timeout) > 0 { t = timeout[0] } if err := u.MustWait(t); err != nil { return err } x, y, err := u.Center(xoff, yoff) if err != nil { return err } return u.device.Click(x, y) } // ClickExists 如果元素存在则点击,返回是否成功 func (u *UiObject) ClickExists(timeout ...float64) bool { t := 0.0 if len(timeout) > 0 { t = timeout[0] } err := u.Click(t) return err == nil } // ClickGone 持续点击直到元素消失 // maxRetry: 最大重试次数 // interval: 重试间隔(秒) func (u *UiObject) ClickGone(maxRetry int, interval float64) bool { if maxRetry <= 0 { maxRetry = 10 } if interval <= 0 { interval = 1.0 } u.ClickExists(0) for i := 0; i < maxRetry; i++ { time.Sleep(time.Duration(interval * float64(time.Second))) exists, _ := u.Exists() if !exists { return true } u.ClickExists(0) } return false } // LongClick 长按 UI 元素 // duration: 按住时间(秒),默认 0.5 func (u *UiObject) LongClick(duration float64, timeout ...float64) error { if duration <= 0 { duration = 0.5 } t := 0.0 if len(timeout) > 0 { t = timeout[0] } if err := u.MustWait(t); err != nil { return err } x, y, err := u.Center() if err != nil { return err } return u.device.LongClick(x, y, duration) } // ---------- 文本操作 ---------- // GetText 获取元素文本内容 func (u *UiObject) GetText(timeout ...float64) (string, error) { t := 0.0 if len(timeout) > 0 { t = timeout[0] } if err := u.MustWait(t); err != nil { return "", err } raw, err := u.jsonrpc.Call("getText", []interface{}{u.selector.ToMap()}) if err != nil { return "", err } var text string json.Unmarshal(raw, &text) return text, nil } // SetText 设置元素文本内容 // 如果 text 为空,则清除文本 func (u *UiObject) SetText(text string, timeout ...float64) error { t := 0.0 if len(timeout) > 0 { t = timeout[0] } if err := u.MustWait(t); err != nil { return err } if text == "" { _, err := u.jsonrpc.Call("clearTextField", []interface{}{u.selector.ToMap()}) return err } _, err := u.jsonrpc.Call("setText", []interface{}{u.selector.ToMap(), text}) return err } // ClearText 清除元素文本 func (u *UiObject) ClearText(timeout ...float64) error { return u.SetText("", timeout...) } // SendKeys SetText 的别名 func (u *UiObject) SendKeys(text string, timeout ...float64) error { return u.SetText(text, timeout...) } // ---------- 滑动操作 ---------- // UiSwipe 在元素范围内滑动 // direction: 方向 "left"/"right"/"up"/"down" // steps: 滑动步数 func (u *UiObject) UiSwipe(direction string, steps int) error { if steps <= 0 { steps = 10 } if err := u.MustWait(0); err != nil { return err } lx, ly, rx, ry, err := u.Bounds() if err != nil { return err } cx := (lx + rx) / 2 cy := (ly + ry) / 2 switch direction { case "up": return u.device.Swipe(cx, cy, cx, ly, steps) case "down": return u.device.Swipe(cx, cy, cx, ry-1, steps) case "left": return u.device.Swipe(cx, cy, lx, cy, steps) case "right": return u.device.Swipe(cx, cy, rx-1, cy, steps) default: return fmt.Errorf("不支持的方向: %s", direction) } } // DragTo 将元素拖拽到指定坐标 func (u *UiObject) DragTo(x, y int, duration float64, timeout ...float64) error { if duration <= 0 { duration = 0.5 } t := 0.0 if len(timeout) > 0 { t = timeout[0] } if err := u.MustWait(t); err != nil { return err } steps := int(duration * 200) _, err := u.jsonrpc.Call("dragTo", []interface{}{u.selector.ToMap(), x, y, steps}) return err } // ---------- 手势操作 ---------- // PinchIn 向内捏合(缩小) func (u *UiObject) PinchIn(percent, steps int) error { if percent <= 0 { percent = 100 } if steps <= 0 { steps = 50 } _, err := u.jsonrpc.Call("pinchIn", []interface{}{u.selector.ToMap(), percent, steps}) return err } // PinchOut 向外捏合(放大) func (u *UiObject) PinchOut(percent, steps int) error { if percent <= 0 { percent = 100 } if steps <= 0 { steps = 50 } _, err := u.jsonrpc.Call("pinchOut", []interface{}{u.selector.ToMap(), percent, steps}) return err } // ---------- 子/兄弟元素 ---------- // Child 查找子元素 func (u *UiObject) Child(params map[string]interface{}) (*UiObject, error) { sel := u.selector.Clone() if _, err := sel.Child(params); err != nil { return nil, err } return NewUiObject(u.device, sel), nil } // Sibling 查找兄弟元素 func (u *UiObject) Sibling(params map[string]interface{}) (*UiObject, error) { sel := u.selector.Clone() if _, err := sel.Sibling(params); err != nil { return nil, err } return NewUiObject(u.device, sel), nil } // ChildByText 通过文本查找子元素 func (u *UiObject) ChildByText(text string, params map[string]interface{}) (*UiObject, error) { childSel, err := New(params) if err != nil { return nil, err } raw, err := u.jsonrpc.Call("childByText", []interface{}{u.selector.ToMap(), childSel.ToMap(), text}) if err != nil { return nil, err } var resultMap map[string]interface{} if err := json.Unmarshal(raw, &resultMap); err != nil { return nil, err } resultSel, err := FromMap(resultMap) if err != nil { return nil, err } return NewUiObject(u.device, resultSel), nil } // ChildByDescription 通过描述查找子元素 func (u *UiObject) ChildByDescription(desc string, params map[string]interface{}) (*UiObject, error) { childSel, err := New(params) if err != nil { return nil, err } raw, err := u.jsonrpc.Call("childByDescription", []interface{}{u.selector.ToMap(), childSel.ToMap(), desc}) if err != nil { return nil, err } var resultMap map[string]interface{} if err := json.Unmarshal(raw, &resultMap); err != nil { return nil, err } resultSel, err := FromMap(resultMap) if err != nil { return nil, err } return NewUiObject(u.device, resultSel), nil } // ---------- 数量和索引 ---------- // Count 获取匹配元素的数量 func (u *UiObject) Count() (int, error) { raw, err := u.jsonrpc.Call("count", []interface{}{u.selector.ToMap()}) if err != nil { return 0, err } var count int json.Unmarshal(raw, &count) return count, nil } // Instance 获取指定索引的元素 func (u *UiObject) Instance(index int) *UiObject { sel := u.selector.Clone() if index < 0 { // 负数索引需要先获取总数 count, err := u.Count() if err == nil && index+count >= 0 { index = index + count } else { index = 0 } } sel.UpdateInstance(index) return NewUiObject(u.device, sel) } // ---------- 滚动操作 ---------- // ScrollForward 向前滚动 func (u *UiObject) ScrollForward(vertical bool, steps int) (bool, error) { if steps <= 0 { steps = ScrollSteps } raw, err := u.jsonrpc.Call("scrollForward", []interface{}{u.selector.ToMap(), vertical, steps}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // ScrollBackward 向后滚动 func (u *UiObject) ScrollBackward(vertical bool, steps int) (bool, error) { if steps <= 0 { steps = ScrollSteps } raw, err := u.jsonrpc.Call("scrollBackward", []interface{}{u.selector.ToMap(), vertical, steps}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // ScrollToBeginning 滚动到开头 func (u *UiObject) ScrollToBeginning(vertical bool, maxSwipes, steps int) (bool, error) { if maxSwipes <= 0 { maxSwipes = 500 } if steps <= 0 { steps = ScrollSteps } raw, err := u.jsonrpc.Call("scrollToBeginning", []interface{}{u.selector.ToMap(), vertical, maxSwipes, steps}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // ScrollToEnd 滚动到末尾 func (u *UiObject) ScrollToEnd(vertical bool, maxSwipes, steps int) (bool, error) { if maxSwipes <= 0 { maxSwipes = 500 } if steps <= 0 { steps = ScrollSteps } raw, err := u.jsonrpc.Call("scrollToEnd", []interface{}{u.selector.ToMap(), vertical, maxSwipes, steps}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // ScrollTo 滚动到指定元素可见 func (u *UiObject) ScrollTo(targetParams map[string]interface{}, vertical bool) (bool, error) { targetSel, err := New(targetParams) if err != nil { return false, err } raw, err := u.jsonrpc.Call("scrollTo", []interface{}{u.selector.ToMap(), targetSel.ToMap(), vertical}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // ---------- Fling 操作 ---------- // FlingForward 向前快速滑动 func (u *UiObject) FlingForward(vertical bool) (bool, error) { raw, err := u.jsonrpc.Call("flingForward", []interface{}{u.selector.ToMap(), vertical}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // FlingBackward 向后快速滑动 func (u *UiObject) FlingBackward(vertical bool) (bool, error) { raw, err := u.jsonrpc.Call("flingBackward", []interface{}{u.selector.ToMap(), vertical}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // FlingToBeginning 快速滑动到开头 func (u *UiObject) FlingToBeginning(vertical bool, maxSwipes int) (bool, error) { if maxSwipes <= 0 { maxSwipes = 500 } raw, err := u.jsonrpc.Call("flingToBeginning", []interface{}{u.selector.ToMap(), vertical, maxSwipes}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil } // FlingToEnd 快速滑动到末尾 func (u *UiObject) FlingToEnd(vertical bool, maxSwipes int) (bool, error) { if maxSwipes <= 0 { maxSwipes = 500 } raw, err := u.jsonrpc.Call("flingToEnd", []interface{}{u.selector.ToMap(), vertical, maxSwipes}) if err != nil { return false, err } var result bool json.Unmarshal(raw, &result) return result, nil }