当前位置:   article > 正文

鸿蒙系统深度解读(二)_鸿蒙 应用流转 功能原理

鸿蒙 应用流转 功能原理
《鸿蒙系统深度解读》系列文章中,我们将针对鸿蒙系统分享基础知识、应用实例、前端JS/eTS部分、鸿蒙系统带给我们的思考、我们如何做这几部分内容,本篇文章介绍 应用实例(Java)

1 应用流转迁移

「 项目 」

harmonyos-tutorial\samples\ContinueRemoteFACollaboration

「 效果 」

图1

图2

如图,点击图1中 “迁移” 按钮,会打开 pad 设备应用,同时将数据迁移显示到 pad 对应 UI 处。

图2中可以继续编辑内容,点击 “迁移” 会再次迁移到图1设备中继续显示。

「 重点代码 」

  1. /**
  2. * 设备迁移与回迁
  3. */
  4. public class MainAbilitySlice extends AbilitySlice implements IAbilityContinuation {
  5. private static final String TAG = MainAbilitySlice.class.getSimpleName();
  6. private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0x00001, TAG);
  7. private static final String MESSAGE_KEY = "com.waylau.hmos.continueremotefacollaboration.slice.MESSAGE_KEY";
  8. private String message;
  9. private boolean isContinued;
  10. private TextField messageTextField;
  11. @Override
  12. public void onStart(Intent intent) {
  13. super.onStart(intent);
  14. super.setUIContent(ResourceTable.Layout_ability_main);
  15. HiLog.info(LABEL_LOG, "onStart");
  16. // 监听跨端迁移FA的事件
  17. Button buttonContinueRemoteFA = (
  18. Button) findComponentById(ResourceTable.Id_button_continue_remote_fa);
  19. buttonContinueRemoteFA.setClickedListener(listener -> continueRemoteFA());
  20. // 设置输入框内容
  21. messageTextField = (TextField) findComponentById(ResourceTable.Id_message_textfield);
  22. if (isContinued && message != null) {
  23. messageTextField.setText(message);
  24. }
  25. }
  26. private void continueRemoteFA() {
  27. HiLog.info(LABEL_LOG, "before startRemoteFA");
  28. String deviceId = DeviceUtils.getDeviceId();
  29. HiLog.info(LABEL_LOG, "get deviceId: %{public}s", deviceId);
  30. if (deviceId != null) {
  31. // 发起迁移流程
  32. // continueAbility()是不可回迁的
  33. // continueAbilityReversibly() 是可以回迁的
  34. continueAbility(deviceId);
  35. }
  36. }
  37. @Override
  38. public void onActive() {
  39. super.onActive();
  40. }
  41. @Override
  42. public void onForeground(Intent intent) {
  43. super.onForeground(intent);
  44. }
  45. @Override
  46. public boolean onStartContinuation() {
  47. HiLog.info(LABEL_LOG, "onStartContinuation");
  48. // 重写
  49. return true;
  50. }
  51. @Override
  52. public boolean onSaveData(IntentParams intentParams) {
  53. HiLog.info(LABEL_LOG, "onSaveData");
  54. // 重写
  55. // 保存回迁后恢复状态必须的数据
  56. intentParams.setParam(MESSAGE_KEY, messageTextField.getText());
  57. return true;
  58. }
  59. @Override
  60. public boolean onRestoreData(IntentParams intentParams) {
  61. HiLog.info(LABEL_LOG, "onRestoreData");
  62. // 重写
  63. // 传递此前保存的数据
  64. if (intentParams.getParam(MESSAGE_KEY) instanceof String) {
  65. message = (String) intentParams.getParam(MESSAGE_KEY);
  66. isContinued = true;
  67. }
  68. return true;
  69. }
  70. @Override
  71. public void onCompleteContinuation(int i) {
  72. HiLog.info(LABEL_LOG, "onCompleteContinuation");
  73. // 终止
  74. terminate();
  75. }
  76. }

「 原理 」

  1. 实现

IAbilityContinuation

接口,重写 onSaveData、onRestoreData、onCompleteContinuation 等方法。其中,onSaveData 用于保存迁移传递必须的数据;onRestoreData 恢复获取传递此前保存的数据。onCompleteContinuation 方法用于终止 Page。

  1. 点击事件中获取组网设备ID,DeviceUtils.getDeviceId() 。

  1. 调用迁移 API,continueAbility (deviceId) 后会触发 IAbilityContinuation 相关回调方法。

  1. 应用层不需要处理联网协议,底层基于软总线 COAP 协议完成数据传递,实现数据迁移。

「 特点 」

应用层不需要处理组网、联网等复杂过程降低开发难度,简单 API 调用就可以完成近场设备间数据 UI 迁移。

「 应用场景 」

  • 在外时手机上编辑邮件,到家后迁移到平板上继续编辑

  • 在外时手机玩游戏,到家后迁移到平板上继续玩

  • 在家里智慧屏上看视频,出门时迁移到手机上继续观看

  • 手机视频通话迁移到智慧屏,更沉浸地视频聊天

  • 听歌,看电影,玩游戏,应用流转数据迁移至至电视继续播放,玩游戏

2 分布式数据库

「 项目 」

codelabs\DistributeDatabaseDraw

「 效果 」

如图,在一组互联的设备中,在其中一个设备上写字画画会实时同步至其他设备,不是投屏。

「 重点代码 」

MainAbilitySlice.java

  1. public class MainAbilitySlice extends AbilitySlice {
  2. private static final String TAG = MainAbilitySlice.class.getName();
  3. private static final int PERMISSION_CODE = 20201203;
  4. private static final int DELAY_TIME = 10;
  5. private static final String STORE_ID_KEY = "storeId";
  6. private static final String POINTS_KEY = "points";
  7. private static final String COLOR_INDEX_KEY = "colorIndex";
  8. private static final String IS_FORM_LOCAL_KEY = "isFormLocal";
  9. private static String storeId;
  10. private DependentLayout canvas;
  11. private Image transform;
  12. private KvManager kvManager;
  13. private SingleKvStore singleKvStore;
  14. private Text title;
  15. private DrawPoint drawl;
  16. private Button back;
  17. @Override
  18. public void onStart(Intent intent) {
  19. super.onStart(intent);
  20. super.setUIContent(ResourceTable.Layout_ability_main);
  21. storeId = STORE_ID_KEY + System.currentTimeMillis();
  22. findComponentById();
  23. requestPermission();
  24. initView(intent);
  25. initDatabase();
  26. initDraw(intent);
  27. }
  28. private void initView(Intent intent) {
  29. boolean isLocal = !intent.getBooleanParam(IS_FORM_LOCAL_KEY, false);
  30. if (!isLocal) {
  31. storeId = intent.getStringParam(STORE_ID_KEY);
  32. }
  33. title.setText(isLocal ? "本地端" : "远程端");
  34. transform.setVisibility(isLocal ? Component.VISIBLE : Component.INVISIBLE);
  35. }
  36. private void requestPermission() {
  37. if (verifySelfPermission(DISTRIBUTED_DATASYNC) != IBundleManager.PERMISSION_GRANTED) {
  38. if (canRequestPermission(DISTRIBUTED_DATASYNC)) {
  39. requestPermissionsFromUser(new String[]{DISTRIBUTED_DATASYNC}, PERMISSION_CODE);
  40. }
  41. }
  42. }
  43. private void findComponentById() {
  44. if (findComponentById(ResourceTable.Id_canvas) instanceof DependentLayout) {
  45. canvas = (DependentLayout) findComponentById(ResourceTable.Id_canvas);
  46. }
  47. if (findComponentById(ResourceTable.Id_transform) instanceof Image) {
  48. transform = (Image) findComponentById(ResourceTable.Id_transform);
  49. }
  50. if (findComponentById(ResourceTable.Id_title) instanceof Text) {
  51. title = (Text) findComponentById(ResourceTable.Id_title);
  52. }
  53. if (findComponentById(ResourceTable.Id_back) instanceof Button) {
  54. back = (Button) findComponentById(ResourceTable.Id_back);
  55. }
  56. transform.setClickedListener(component -> {
  57. DeviceSelectDialog dialog = new DeviceSelectDialog(MainAbilitySlice.this);
  58. dialog.setListener(deviceIds -> {
  59. if (deviceIds != null && !deviceIds.isEmpty()) {
  60. // 启动远程页面
  61. startRemoteFas(deviceIds);
  62. // 同步远程数据库
  63. singleKvStore.sync(deviceIds, SyncMode.PUSH_ONLY);
  64. }
  65. dialog.hide();
  66. });
  67. dialog.show();
  68. });
  69. }
  70. /**
  71. * Initialize art boards
  72. *
  73. * @param intent Intent
  74. */
  75. private void initDraw(Intent intent) {
  76. int colorIndex = intent.getIntParam(COLOR_INDEX_KEY, 0);
  77. drawl = new DrawPoint(this, colorIndex);
  78. drawl.setWidth(MATCH_PARENT);
  79. drawl.setWidth(MATCH_PARENT);
  80. canvas.addComponent(drawl);
  81. drawPoints();
  82. drawl.setOnDrawBack(points -> {
  83. if (points != null && points.size() > 1) {
  84. String pointsString = GsonUtil.objectToString(points);
  85. LogUtils.info(TAG, "pointsString::" + pointsString);
  86. if (singleKvStore != null) {
  87. singleKvStore.putString(POINTS_KEY, pointsString);
  88. }
  89. }
  90. });
  91. back.setClickedListener(component -> {
  92. List<MyPoint> points = drawl.getPoints();
  93. if (points == null || points.size() <= 1) {
  94. return;
  95. }
  96. points.remove(points.size() - 1);
  97. for (int i = points.size() - 1; i >= 0; i--) {
  98. if (points.get(i).isLastPoint()) {
  99. break;
  100. }
  101. points.remove(i);
  102. }
  103. drawl.setDrawParams(points);
  104. String pointsString = GsonUtil.objectToString(points);
  105. if (singleKvStore != null) {
  106. singleKvStore.putString(POINTS_KEY, pointsString);
  107. }
  108. });
  109. }
  110. // 获取数据库中的点数据,并在画布上画出来
  111. private void drawPoints() {
  112. List<Entry> points = singleKvStore.getEntries(POINTS_KEY);
  113. for (Entry entry : points) {
  114. if (entry.getKey().equals(POINTS_KEY)) {
  115. List<MyPoint> remotePoints = GsonUtil.jsonToList(singleKvStore.getString(POINTS_KEY), MyPoint.class);
  116. getUITaskDispatcher().delayDispatch(() -> drawl.setDrawParams(remotePoints), DELAY_TIME);
  117. }
  118. }
  119. }
  120. /**
  121. * Receive database messages
  122. *
  123. * @since 2021-04-06
  124. */
  125. private class KvStoreObserverClient implements KvStoreObserver {
  126. @Override
  127. public void onChange(ChangeNotification notification) {
  128. LogUtils.info(TAG, "data changed......");
  129. drawPoints();
  130. }
  131. }
  132. private void initDatabase() {
  133. // 创建分布式数据库管理对象
  134. KvManagerConfig config = new KvManagerConfig(this);
  135. kvManager = KvManagerFactory.getInstance().createKvManager(config);
  136. // 创建分布式数据库
  137. Options options = new Options();
  138. options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION);
  139. singleKvStore = kvManager.getKvStore(options, storeId);
  140. // 订阅分布式数据变化
  141. KvStoreObserver kvStoreObserverClient = new KvStoreObserverClient();
  142. singleKvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_ALL, kvStoreObserverClient);
  143. }
  144. /**
  145. * Starting Multiple Remote Fas
  146. *
  147. * @param deviceIds deviceIds
  148. */
  149. private void startRemoteFas(List<String> deviceIds) {
  150. Intent[] intents = new Intent[deviceIds.size()];
  151. for (int i = 0; i < deviceIds.size(); i++) {
  152. Intent intent = new Intent();
  153. intent.setParam(IS_FORM_LOCAL_KEY, true);
  154. intent.setParam(COLOR_INDEX_KEY, i + 1);
  155. intent.setParam(STORE_ID_KEY, storeId);
  156. Operation operation = new Intent.OperationBuilder()
  157. .withDeviceId(deviceIds.get(i))
  158. .withBundleName(getBundleName())
  159. .withAbilityName(MainAbility.class.getName())
  160. .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
  161. .build();
  162. intent.setOperation(operation);
  163. intents[i] = intent;
  164. }
  165. startAbilities(intents);
  166. }
  167. @Override
  168. protected void onStop() {
  169. super.onStop();
  170. kvManager.closeKvStore(singleKvStore);
  171. }
  172. }

DrawPoint.java

  1. public class DrawPoint extends Component implements Component.DrawTask {
  2. private static final int STROKE_WIDTH = 15;
  3. private static final int TIME = 200;
  4. private static final int EVENT_MSG_STORE = 0x1000002;
  5. private final Color[] paintColors = new Color[]{Color.RED, Color.BLUE, Color.BLACK};
  6. private List<MyPoint> points = new ArrayList<>(0);
  7. private Paint paint;
  8. private OnDrawCallBack callBack;
  9. private Timer timer = null;
  10. private TimerTask timerTask = null;
  11. private final EventHandler handler = new EventHandler(EventRunner.current()) {
  12. @Override
  13. protected void processEvent(InnerEvent event) {
  14. if (EVENT_MSG_STORE == event.eventId) {
  15. callBack.callBack(points);
  16. }
  17. }
  18. };
  19. /**
  20. * Drawl constructor
  21. *
  22. * @param context context
  23. * @param colorIndex colorIndex
  24. */
  25. public DrawPoint(Context context, int colorIndex) {
  26. super(context);
  27. init(colorIndex);
  28. }
  29. public List<MyPoint> getPoints() {
  30. return points;
  31. }
  32. /**
  33. * SelectResult
  34. *
  35. * @since 2020-12-03
  36. */
  37. public interface OnDrawCallBack {
  38. /**
  39. * touchListener
  40. *
  41. * @param points points
  42. */
  43. void callBack(List<MyPoint> points);
  44. }
  45. /**
  46. * setPoints
  47. *
  48. * @param myPoints myPoints
  49. */
  50. public void setDrawParams(List<MyPoint> myPoints) {
  51. this.points = myPoints;
  52. invalidate();
  53. }
  54. /**
  55. * setOnDrawBack
  56. *
  57. * @param onCallBack onCallBack
  58. */
  59. public void setOnDrawBack(OnDrawCallBack onCallBack) {
  60. this.callBack = onCallBack;
  61. }
  62. private void init(int colorIndex) {
  63. paint = new Paint();
  64. paint.setAntiAlias(true);
  65. paint.setStyle(Paint.Style.STROKE_STYLE);
  66. paint.setStrokeWidth(STROKE_WIDTH);
  67. addDrawTask(this);
  68. Color color = getRandomColor(colorIndex);
  69. setTouchEventListener((component, touchEvent) -> {
  70. scheduledTask();
  71. int crtX = (int) touchEvent.getPointerPosition(touchEvent.getIndex()).getX();
  72. int crtY = (int) touchEvent.getPointerPosition(touchEvent.getIndex()).getY();
  73. MyPoint point = new MyPoint(crtX, crtY);
  74. point.setPaintColor(color);
  75. switch (touchEvent.getAction()) {
  76. case TouchEvent.POINT_MOVE:
  77. points.add(point);
  78. break;
  79. case TouchEvent.PRIMARY_POINT_UP:
  80. points.add(point);
  81. point.setLastPoint(true);
  82. callBack.callBack(points);
  83. onTimerFinish();
  84. break;
  85. default:
  86. break;
  87. }
  88. invalidate();
  89. return true;
  90. });
  91. }
  92. /**
  93. * scheduled task start
  94. */
  95. public void scheduledTask() {
  96. if (timer == null && timerTask == null) {
  97. timer = new Timer();
  98. timerTask = new TimerTask() {
  99. @Override
  100. public void run() {
  101. handler.sendEvent(EVENT_MSG_STORE);
  102. }
  103. };
  104. timer.schedule(timerTask, 0, TIME);
  105. }
  106. }
  107. /**
  108. * Canceling a Scheduled Task
  109. */
  110. public void onTimerFinish() {
  111. timer.cancel();
  112. timer = null;
  113. timerTask = null;
  114. }
  115. @Override
  116. public void onDraw(Component component, Canvas canvas) {
  117. draw(points, canvas);
  118. }
  119. private void draw(List<MyPoint> myPoints, Canvas canvas) {
  120. if (myPoints == null || myPoints.size() <= 1) {
  121. return;
  122. }
  123. Point first = null;
  124. Point last = null;
  125. for (MyPoint myPoint : myPoints) {
  126. paint.setColor(myPoint.getPaintColor());
  127. float finalX = myPoint.getPositionX();
  128. float finalY = myPoint.getPositionY();
  129. Point finalPoint = new Point(finalX, finalY);
  130. if (myPoint.isLastPoint()) {
  131. first = null;
  132. last = null;
  133. continue;
  134. }
  135. if (first == null) {
  136. first = finalPoint;
  137. } else {
  138. if (last != null) {
  139. first = last;
  140. }
  141. last = finalPoint;
  142. canvas.drawLine(first, last, paint);
  143. }
  144. }
  145. }
  146. private Color getRandomColor(int index) {
  147. return index > paintColors.length - 1 ? paintColors[0] : paintColors[index];
  148. }
  149. }

「 原理 」

  1. 初始化分布式数据库 initDatabase(),订阅消息监听

  1. 点击共享后,获取设备列表,启动远程页面 startRemoteFas(deviceIds)

  1. 绘制过程中在 DrowPoint 中 TouchEventListener 收集坐标点信息

  1. 每过200ms触发一次任务回调中在 drawl.setOnDrawBack 回调中将绘图轨迹坐标写入分布式数据库 singleKvStore.putString(POINTS_KEY, pointsString)

  1. 其他设备,在消息监听订阅回调 KvStoreObserver 回调函数 onchange 中,获取分布式数据库中数据并完成绘制

  1. 分布式数据库底层通过最终一致性方案,完成数据同步传递

「 特点 」

多设备实时协同操作(不是投屏)。

「 应用场景 」

师生课堂互动。

3 分布式任务调度

「 项目 」

codelabs\RemoteInputDemo

「 效果 」

图3 视频播放器

图4 手机输入法

如图,图3为电视视频筛选页面,通过图4的手机输入法输入文本同步到图3视频搜索输入框。

「 重点代码 」

MainAbilitySlice.java

  1. public class MainAbilitySlice extends AbilitySlice implements PermissionBridge.OnPermissionStateListener {
  2. private static final String ABILITY_NAME = "com.huawei.codelab.RemoteInputAbility";
  3. private static final String MOVIE_PLAY_ABILITY = "com.huawei.codelab.MoviePlayAbility";
  4. private static final String MAIN_ABILITY = "com.huawei.codelab.MainAbility";
  5. private static final int FOCUS_PADDING = 8;
  6. private static final int SEARCH_PADDING = 3;
  7. private static final int LIST_INIT_SIZE = 16;
  8. private static final String TAG = MainAbilitySlice.class.getName();
  9. private static final Lock MOVE_LOCK = new ReentrantLock();
  10. private MainAbilitySlice.MyCommonEventSubscriber subscriber;
  11. private TextField tvTextInput;
  12. private ScrollView scrollView;
  13. private DirectionalLayout keyBoardLayout;
  14. private TableLayout movieTableLayout;
  15. private Size size;
  16. private final AbilityMgr abilityMgr = new AbilityMgr(this);
  17. // 搜索的到的影片
  18. private final List<ComponentPointData> movieSearchList = new ArrayList<>(LIST_INIT_SIZE);
  19. // 当前焦点所在位置
  20. private ComponentPointData componentPointDataNow;
  21. // 注册流转任务管理服务后返回的Ability token
  22. private int abilityToken;
  23. // 用户在设备列表中选择设备后返回的设备ID
  24. private String selectDeviceId;
  25. // 获取流转任务管理服务管理类
  26. private IContinuationRegisterManager continuationRegisterManager;
  27. // 设置监听FA流转管理服务设备状态变更的回调
  28. private final IContinuationDeviceCallback callback = new IContinuationDeviceCallback() {
  29. @Override
  30. public void onDeviceConnectDone(String deviceId, String s1) {
  31. selectDeviceId = deviceId;
  32. abilityMgr.openRemoteAbility(selectDeviceId, getBundleName(), ABILITY_NAME);
  33. getUITaskDispatcher().asyncDispatch(() -> continuationRegisterManager.updateConnectStatus(abilityToken, selectDeviceId,
  34. DeviceConnectState.IDLE.getState(), null));
  35. }
  36. @Override
  37. public void onDeviceDisconnectDone(String deviceId) {
  38. }
  39. };
  40. // 设置注册FA流转管理服务服务回调
  41. private final RequestCallback requestCallback = new RequestCallback() {
  42. @Override
  43. public void onResult(int result) {
  44. abilityToken = result;
  45. }
  46. };
  47. @Override
  48. public void onStart(Intent intent) {
  49. super.onStart(intent);
  50. super.setUIContent(ResourceTable.Layout_ability_main);
  51. new PermissionBridge().setOnPermissionStateListener(this);
  52. // 全屏设置
  53. WindowManagerUtils.setWindows();
  54. // 初始化布局
  55. initComponent();
  56. // 初始化按钮组件
  57. initKeyBoardComponent(keyBoardLayout);
  58. // 初始化影片图片组件
  59. initMovieTableComponent(movieTableLayout);
  60. // 事件订阅
  61. subscribe();
  62. }
  63. private void registerTransService() {
  64. continuationRegisterManager = getContinuationRegisterManager();
  65. // 增加过滤条件
  66. ExtraParams params = new ExtraParams();
  67. String[] devTypes = new String[]{ExtraParams.DEVICETYPE_SMART_PAD, ExtraParams.DEVICETYPE_SMART_PHONE};
  68. params.setDevType(devTypes);
  69. // 注册FA流转管理服务
  70. continuationRegisterManager.register(getBundleName(), params, callback, requestCallback);
  71. }
  72. private void initMovieTableComponent(TableLayout tableLayout) {
  73. int index = 0;
  74. while (index < tableLayout.getChildCount()) {
  75. DirectionalLayout childLayout = null;
  76. if (tableLayout.getComponentAt(index) instanceof DirectionalLayout) {
  77. childLayout = (DirectionalLayout) tableLayout.getComponentAt(index);
  78. }
  79. ComponentPointData componentPointData = new ComponentPointData();
  80. Component component = null;
  81. int indexMovie = 0;
  82. while (childLayout != null && indexMovie < childLayout.getChildCount()) {
  83. Component comChild = childLayout.getComponentAt(indexMovie);
  84. indexMovie++;
  85. if (comChild instanceof Text) {
  86. componentPointData = ComponentPointDataMgr
  87. .getConstantMovie(((Text) comChild).getText()).orElse(null);
  88. continue;
  89. }
  90. component = findComponentById(comChild.getId());
  91. }
  92. if (componentPointData != null && component != null) {
  93. componentPointData.setComponentId(component.getId());
  94. }
  95. ComponentPointDataMgr.getComponentPointDataMgrs().add(componentPointData);
  96. index++;
  97. }
  98. }
  99. private void initKeyBoardComponent(DirectionalLayout directionalLayout) {
  100. int index = 0;
  101. while (index < directionalLayout.getChildCount()) {
  102. DirectionalLayout childLayout = null;
  103. if (directionalLayout.getComponentAt(index) instanceof DirectionalLayout) {
  104. childLayout = (DirectionalLayout) directionalLayout.getComponentAt(index);
  105. }
  106. int indexButton = 0;
  107. while (childLayout != null && indexButton < childLayout.getChildCount()) {
  108. if (childLayout.getComponentAt(indexButton) instanceof Button) {
  109. Button button = (Button) childLayout.getComponentAt(indexButton);
  110. buttonInit(button);
  111. indexButton++;
  112. }
  113. }
  114. index++;
  115. }
  116. }
  117. private void initComponent() {
  118. if (findComponentById(ResourceTable.Id_scrollview) instanceof ScrollView) {
  119. scrollView = (ScrollView) findComponentById(ResourceTable.Id_scrollview);
  120. }
  121. if (findComponentById(ResourceTable.Id_TV_input) instanceof TextField) {
  122. tvTextInput = (TextField) findComponentById(ResourceTable.Id_TV_input);
  123. // 初始化默认选中效果
  124. ComponentPointData componentPointData = new ComponentPointData();
  125. componentPointData.setComponentId(tvTextInput.getId());
  126. componentPointData.setPointX(0);
  127. componentPointData.setPointY(1);
  128. tvTextInput.requestFocus();
  129. componentPointDataNow = componentPointData;
  130. ComponentPointDataMgr.getComponentPointDataMgrs().add(componentPointDataNow);
  131. // 点击事件触发设备选取弹框
  132. tvTextInput.setClickedListener(component -> showDevicesDialog());
  133. }
  134. if (findComponentById(ResourceTable.Id_keyBoardComponent) instanceof DirectionalLayout) {
  135. keyBoardLayout = (DirectionalLayout) findComponentById(ResourceTable.Id_keyBoardComponent);
  136. }
  137. if (findComponentById(ResourceTable.Id_tableLayout) instanceof TableLayout) {
  138. movieTableLayout = (TableLayout) findComponentById(ResourceTable.Id_tableLayout);
  139. }
  140. if (findComponentById(ResourceTable.Id_image_ten) instanceof Image) {
  141. Image image = (Image) findComponentById(ResourceTable.Id_image_ten);
  142. size = image.getPixelMap().getImageInfo().size;
  143. }
  144. }
  145. private void showDevicesDialog() {
  146. ExtraParams extraParams = new ExtraParams();
  147. extraParams.setDevType(new String[]{ExtraParams.DEVICETYPE_SMART_TV,
  148. ExtraParams.DEVICETYPE_SMART_PHONE});
  149. extraParams.setDescription("远程遥控器");
  150. continuationRegisterManager.showDeviceList(abilityToken, extraParams, result -> LogUtils.info(TAG, "show devices success"));
  151. }
  152. private void buttonInit(Button button) {
  153. if (button.getId() == ResourceTable.Id_del) {
  154. findComponentById(button.getId()).setClickedListener(component -> {
  155. if (tvTextInput.getText().length() > 0) {
  156. tvTextInput.setText(tvTextInput.getText().substring(0, tvTextInput.getText().length() - 1));
  157. }
  158. });
  159. } else if (button.getId() == ResourceTable.Id_clear) {
  160. findComponentById(button.getId()).setClickedListener(component -> tvTextInput.setText(""));
  161. } else {
  162. findComponentById(button.getId()).setClickedListener(component -> tvTextInput.append(button.getText()));
  163. }
  164. }
  165. @Override
  166. public void onActive() {
  167. super.onActive();
  168. }
  169. @Override
  170. public void onForeground(Intent intent) {
  171. super.onForeground(intent);
  172. }
  173. @Override
  174. public void onPermissionGranted() {
  175. registerTransService();
  176. }
  177. @Override
  178. public void onPermissionDenied() {
  179. terminate();
  180. }
  181. /**
  182. * MyCommonEventSubscriber
  183. *
  184. * @since 2020-12-03
  185. */
  186. class MyCommonEventSubscriber extends CommonEventSubscriber {
  187. MyCommonEventSubscriber(CommonEventSubscribeInfo info) {
  188. super(info);
  189. }
  190. @Override
  191. public void onReceiveEvent(CommonEventData commonEventData) {
  192. Intent intent = commonEventData.getIntent();
  193. int requestType = intent.getIntParam("requestType", 0);
  194. String inputString = intent.getStringParam("inputString");
  195. if (requestType == ConnectManagerIml.REQUEST_SEND_DATA) {
  196. tvTextInput.setText(inputString);
  197. } else if (requestType == ConnectManagerIml.REQUEST_SEND_SEARCH) {
  198. // 如果当前选中的是文本框则搜索影片;如果当前选中的是影片则播放影片;X轴坐标为0当前选中文本框;X轴大于1当前选中影片
  199. if (componentPointDataNow.getPointX() == 0) {
  200. // 调用大屏的搜索方法
  201. searchMovies(tvTextInput.getText());
  202. return;
  203. }
  204. // 播放影片
  205. abilityMgr.playMovie(getBundleName(), MOVIE_PLAY_ABILITY);
  206. } else {
  207. // 移动方向
  208. String moveString = intent.getStringParam("move");
  209. MainCallBack.movePoint(MainAbilitySlice.this, moveString);
  210. }
  211. }
  212. }
  213. /**
  214. * goBack
  215. */
  216. public void goBack() {
  217. clearLastBackg();
  218. scrollView.scrollTo(0, 0);
  219. componentPointDataNow = ComponentPointDataMgr.getMoviePoint(0, 1).orElse(null);
  220. findComponentById(ResourceTable.Id_TV_input).requestFocus();
  221. abilityMgr.returnMainAbility(getBundleName(), MAIN_ABILITY);
  222. }
  223. /**
  224. * move
  225. *
  226. * @param pointX pointX
  227. * @param pointY pointY
  228. */
  229. public void move(int pointX, int pointY) {
  230. MOVE_LOCK.lock();
  231. try {
  232. // 设置焦点滚动
  233. if (pointX == 0 && componentPointDataNow.getPointX() > 0) {
  234. scrollView.fluentScrollByY(pointY * size.height);
  235. }
  236. if (componentPointDataNow.getPointX() == 0 && pointX == 1) {
  237. scrollView.scrollTo(0, 0);
  238. }
  239. // 设置背景
  240. if (componentPointDataNow.getPointX() + pointX == 0) {
  241. setBackGround(componentPointDataNow.getPointX() + pointX, 1);
  242. } else {
  243. setBackGround(componentPointDataNow.getPointX() + pointX, componentPointDataNow.getPointY() + pointY);
  244. }
  245. } finally {
  246. MOVE_LOCK.unlock();
  247. }
  248. }
  249. private void setBackGround(int pointX, int pointY) {
  250. ComponentPointData componentPointDataNew = ComponentPointDataMgr.getMoviePoint(pointX, pointY).orElse(null);
  251. if (componentPointDataNew == null) {
  252. return;
  253. }
  254. // 清除上次选中的效果
  255. clearLastBackg();
  256. componentPointDataNow = componentPointDataNew;
  257. if (findComponentById(componentPointDataNow.getComponentId()) instanceof Image) {
  258. Image newImage = (Image) findComponentById(componentPointDataNow.getComponentId());
  259. newImage.setPadding(FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING);
  260. } else {
  261. Component component = findComponentById(componentPointDataNow.getComponentId());
  262. component.requestFocus();
  263. }
  264. }
  265. private void clearLastBackg() {
  266. Component component = null;
  267. if (findComponentById(componentPointDataNow.getComponentId()) instanceof TextField) {
  268. TextField textField = (TextField) findComponentById(componentPointDataNow.getComponentId());
  269. textField.clearFocus();
  270. } else {
  271. component = findComponentById(componentPointDataNow.getComponentId());
  272. component.setPadding(0, 0, 0, 0);
  273. }
  274. // 如果是搜索出来的还是保持搜索到的背景
  275. for (ComponentPointData componentPointData : movieSearchList) {
  276. if (component != null && componentPointData.getComponentId() == component.getId()) {
  277. component.setPadding(SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING);
  278. }
  279. }
  280. }
  281. private void searchMovies(String text) {
  282. if (text == null || "".equals(text)) {
  283. return;
  284. }
  285. // 清空上次搜索结果及背景效果
  286. clearHistroyBackGround();
  287. for (ComponentPointData componentPointData : ComponentPointDataMgr.getComponentPointDataMgrs()) {
  288. if (MovieSearchUtils.isContainMovie(componentPointData.getMovieName(), text)
  289. || MovieSearchUtils.isContainMovie(componentPointData.getMovieFirstName(), text)) {
  290. movieSearchList.add(componentPointData);
  291. Component component = findComponentById(componentPointData.getComponentId());
  292. component.setPadding(SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING);
  293. }
  294. }
  295. if (movieSearchList.size() > 0) {
  296. componentPointDataNow = movieSearchList.get(0);
  297. Component component = findComponentById(componentPointDataNow.getComponentId());
  298. component.setPadding(FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING);
  299. } else {
  300. Component component = findComponentById(componentPointDataNow.getComponentId());
  301. component.requestFocus();
  302. }
  303. }
  304. private void clearHistroyBackGround() {
  305. // 清空上次搜索的结果
  306. for (ComponentPointData componentPointData : movieSearchList) {
  307. Component component = findComponentById(componentPointData.getComponentId());
  308. component.setPadding(0, 0, 0, 0);
  309. }
  310. movieSearchList.clear();
  311. // 去掉当前焦点背景
  312. clearLastBackg();
  313. }
  314. private void subscribe() {
  315. MatchingSkills matchingSkills = new MatchingSkills();
  316. matchingSkills.addEvent(EventConstants.SCREEN_REMOTE_CONTROLL_EVENT);
  317. matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);
  318. CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
  319. subscriber = new MainAbilitySlice.MyCommonEventSubscriber(subscribeInfo);
  320. try {
  321. CommonEventManager.subscribeCommonEvent(subscriber);
  322. } catch (RemoteException e) {
  323. LogUtils.error("", "subscribeCommonEvent occur exception.");
  324. }
  325. }
  326. private void unSubscribe() {
  327. try {
  328. CommonEventManager.unsubscribeCommonEvent(subscriber);
  329. } catch (RemoteException e) {
  330. LogUtils.error(TAG, "unSubscribe Exception");
  331. }
  332. }
  333. @Override
  334. protected void onStop() {
  335. super.onStop();
  336. unSubscribe();
  337. }
  338. }

RemoteInputAbilitySlice.java

  1. public class RemoteInputAbilitySlice extends AbilitySlice {
  2. private static final int SHOW_KEYBOARD_DELAY = 800;
  3. private static final int INIT_SIZE = 8;
  4. private ConnectManager connectManager;
  5. private TextField textField;
  6. private String deviceIdConn;
  7. private Component okButton;
  8. private Component leftButton;
  9. private Component rightButton;
  10. private Component upButton;
  11. private Component downButton;
  12. private Component goBackButton;
  13. private Component closeButton;
  14. @Override
  15. public void onStart(Intent intent) {
  16. super.onStart(intent);
  17. super.setUIContent(ResourceTable.Layout_ability_remote_input);
  18. // 获取大屏ID用于配对
  19. deviceIdConn = intent.getStringParam("localDeviceId");
  20. // 全屏设置
  21. WindowManagerUtils.setWindows();
  22. initView();
  23. initListener();
  24. showKeyBoard();
  25. initConnManager();
  26. }
  27. @Override
  28. public void onActive() {
  29. super.onActive();
  30. }
  31. @Override
  32. public void onForeground(Intent intent) {
  33. super.onForeground(intent);
  34. }
  35. private void initView() {
  36. if (findComponentById(ResourceTable.Id_remote_input) instanceof TextField) {
  37. textField = (TextField) findComponentById(ResourceTable.Id_remote_input);
  38. textField.requestFocus();
  39. }
  40. okButton = findComponentById(ResourceTable.Id_ok_button);
  41. leftButton = findComponentById(ResourceTable.Id_left_button);
  42. rightButton = findComponentById(ResourceTable.Id_right_button);
  43. upButton = findComponentById(ResourceTable.Id_up_button);
  44. downButton = findComponentById(ResourceTable.Id_down_button);
  45. goBackButton = findComponentById(ResourceTable.Id_go_back);
  46. closeButton = findComponentById(ResourceTable.Id_close_fa);
  47. }
  48. private void initListener() {
  49. // 监听文本变化,远程同步
  50. textField.addTextObserver((ss, ii, i1, i2) -> {
  51. Map<String, String> map = new HashMap<>(INIT_SIZE);
  52. map.put("inputString", ss);
  53. connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_DATA, map);
  54. });
  55. okButton.setClickedListener(component -> {
  56. // 点击OK按钮
  57. buttonClickSound();
  58. String searchString = textField.getText();
  59. Map<String, String> map = new HashMap<>(INIT_SIZE);
  60. map.put("inputString", searchString);
  61. connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_SEARCH, map);
  62. });
  63. leftButton.setClickedListener(component -> {
  64. // 点击左键按钮
  65. sendMoveRequest(Constants.MOVE_LEFT);
  66. });
  67. rightButton.setClickedListener(component -> {
  68. // 点击右键按钮
  69. sendMoveRequest(Constants.MOVE_RIGHT);
  70. });
  71. upButton.setClickedListener(component -> {
  72. // 点击向上按钮
  73. sendMoveRequest(Constants.MOVE_UP);
  74. });
  75. downButton.setClickedListener(component -> {
  76. // 点击向下按钮
  77. sendMoveRequest(Constants.MOVE_DOWN);
  78. });
  79. goBackButton.setClickedListener(component -> {
  80. // 返回大屏主页
  81. sendMoveRequest(Constants.GO_BACK);
  82. });
  83. closeButton.setClickedListener(component -> {
  84. // 返回主页
  85. sendMoveRequest(Constants.GO_BACK);
  86. terminateAbility();
  87. });
  88. }
  89. private void sendMoveRequest(String direction) {
  90. buttonClickSound();
  91. Map<String, String> map = new HashMap<>(INIT_SIZE);
  92. map.put("move", direction);
  93. connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_MOVE, map);
  94. }
  95. private void showKeyBoard() {
  96. getUITaskDispatcher().delayDispatch(() -> textField.simulateClick(), SHOW_KEYBOARD_DELAY);
  97. }
  98. private void initConnManager() {
  99. connectManager = ConnectManagerIml.getInstance();
  100. connectManager.connectPa(this, deviceIdConn);
  101. }
  102. private void buttonClickSound() {
  103. // 步骤1:实例化对象
  104. SoundPlayer soundPlayer = new SoundPlayer("packageName");
  105. // 步骤2:播放键盘敲击音,音量为1.0
  106. soundPlayer.playSound(SoundPlayer.SoundType.KEY_CLICK, 1.0f);
  107. }
  108. }

「 原理 」

  1. MainAbilitySlice#onStart 生命周期中会做事件注册,类似 Android 中注册广播接收 subscribe() 。

  1. 在事件接收者 onReceiveEvent 中处理来自手机输入法输入的数据/请求查询/播放等命令。如果是数据则显示在电视端文本输入框中,并搜索该影片。

  1. MainAbilitySlice#callback 当监听到设备组网连接成功后,打开 RemoteInputAbilitySlice 该页面就是手机端输入页面,在 initConnManager 中会打开 PA 类似 Android 中 service,在 IAbilityConnection 中回拿到远端电视端的引用。

IRemoteObject 接口,类似 Android 中拿到 Ibinder 接口。后续会用该接口给电视端发送数据。

  1. 手机端输入页面监听用户输入文本变化后会 connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_DATA, map),通过代理最终通过 IRemoteObject 接口给电视端发数据。后续到2电视 onReceiveEvent 接收到数据后处理对应命令。

  1. 按照华为官网描述该过程基于分布式任务调度和公共事件,将远端移动操作请求发送给TV端,类比 Android 通过 binder 机制完成跨进程 Service 通信,鸿蒙分布式调度底层通过极简D2D传输协议栈。

「 特点 」

跨设备分布式任务调度,硬件互助。

「 应用场景 」

冰箱通过手机输入法输入,家电使用手机相机作为输入等。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/231701
推荐阅读
相关标签
  

闽ICP备14008679号