《鸿蒙系统深度解读》系列文章中,我们将针对鸿蒙系统分享基础知识、应用实例、前端JS/eTS部分、鸿蒙系统带给我们的思考、我们如何做这几部分内容,本篇文章介绍 应用实例(Java)。
1 应用流转迁移
「 项目 」
harmonyos-tutorial\samples\ContinueRemoteFACollaboration
「 效果 」
![](https://img-blog.csdnimg.cn/img_convert/7849ef6f7add400185c5d1b678afb33e.png)
图1
![](https://img-blog.csdnimg.cn/img_convert/d30ee05034df4a9ebef902b5bb3544e3.png)
图2
如图,点击图1中 “迁移” 按钮,会打开 pad 设备应用,同时将数据迁移显示到 pad 对应 UI 处。
图2中可以继续编辑内容,点击 “迁移” 会再次迁移到图1设备中继续显示。
「 重点代码 」
/**
* 设备迁移与回迁
*/
public class MainAbilitySlice extends AbilitySlice implements IAbilityContinuation {
private static final String TAG = MainAbilitySlice.class.getSimpleName();
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0x00001, TAG);
private static final String MESSAGE_KEY = "com.waylau.hmos.continueremotefacollaboration.slice.MESSAGE_KEY";
private String message;
private boolean isContinued;
private TextField messageTextField;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
HiLog.info(LABEL_LOG, "onStart");
// 监听跨端迁移FA的事件
Button buttonContinueRemoteFA = (
Button) findComponentById(ResourceTable.Id_button_continue_remote_fa);
buttonContinueRemoteFA.setClickedListener(listener -> continueRemoteFA());
// 设置输入框内容
messageTextField = (TextField) findComponentById(ResourceTable.Id_message_textfield);
if (isContinued && message != null) {
messageTextField.setText(message);
}
}
private void continueRemoteFA() {
HiLog.info(LABEL_LOG, "before startRemoteFA");
String deviceId = DeviceUtils.getDeviceId();
HiLog.info(LABEL_LOG, "get deviceId: %{public}s", deviceId);
if (deviceId != null) {
// 发起迁移流程
// continueAbility()是不可回迁的
// continueAbilityReversibly() 是可以回迁的
continueAbility(deviceId);
}
}
@Override
public void onActive() {
super.onActive();
}
@Override
public void onForeground(Intent intent) {
super.onForeground(intent);
}
@Override
public boolean onStartContinuation() {
HiLog.info(LABEL_LOG, "onStartContinuation");
// 重写
return true;
}
@Override
public boolean onSaveData(IntentParams intentParams) {
HiLog.info(LABEL_LOG, "onSaveData");
// 重写
// 保存回迁后恢复状态必须的数据
intentParams.setParam(MESSAGE_KEY, messageTextField.getText());
return true;
}
@Override
public boolean onRestoreData(IntentParams intentParams) {
HiLog.info(LABEL_LOG, "onRestoreData");
// 重写
// 传递此前保存的数据
if (intentParams.getParam(MESSAGE_KEY) instanceof String) {
message = (String) intentParams.getParam(MESSAGE_KEY);
isContinued = true;
}
return true;
}
@Override
public void onCompleteContinuation(int i) {
HiLog.info(LABEL_LOG, "onCompleteContinuation");
// 终止
terminate();
}
}
「 原理 」
实现
IAbilityContinuation
接口,重写 onSaveData、onRestoreData、onCompleteContinuation 等方法。其中,onSaveData 用于保存迁移传递必须的数据;onRestoreData 恢复获取传递此前保存的数据。onCompleteContinuation 方法用于终止 Page。
点击事件中获取组网设备ID,DeviceUtils.getDeviceId() 。
调用迁移 API,continueAbility (deviceId) 后会触发 IAbilityContinuation 相关回调方法。
应用层不需要处理联网协议,底层基于软总线 COAP 协议完成数据传递,实现数据迁移。
「 特点 」
应用层不需要处理组网、联网等复杂过程降低开发难度,简单 API 调用就可以完成近场设备间数据 UI 迁移。
「 应用场景 」
在外时手机上编辑邮件,到家后迁移到平板上继续编辑
在外时手机玩游戏,到家后迁移到平板上继续玩
在家里智慧屏上看视频,出门时迁移到手机上继续观看
手机视频通话迁移到智慧屏,更沉浸地视频聊天
听歌,看电影,玩游戏,应用流转数据迁移至至电视继续播放,玩游戏
2 分布式数据库
「 项目 」
codelabs\DistributeDatabaseDraw
「 效果 」
![](https://img-blog.csdnimg.cn/img_convert/d871f403e94dbcea74fea1e9b6101ab4.gif)
如图,在一组互联的设备中,在其中一个设备上写字画画会实时同步至其他设备,不是投屏。
「 重点代码 」
MainAbilitySlice.java
public class MainAbilitySlice extends AbilitySlice {
private static final String TAG = MainAbilitySlice.class.getName();
private static final int PERMISSION_CODE = 20201203;
private static final int DELAY_TIME = 10;
private static final String STORE_ID_KEY = "storeId";
private static final String POINTS_KEY = "points";
private static final String COLOR_INDEX_KEY = "colorIndex";
private static final String IS_FORM_LOCAL_KEY = "isFormLocal";
private static String storeId;
private DependentLayout canvas;
private Image transform;
private KvManager kvManager;
private SingleKvStore singleKvStore;
private Text title;
private DrawPoint drawl;
private Button back;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
storeId = STORE_ID_KEY + System.currentTimeMillis();
findComponentById();
requestPermission();
initView(intent);
initDatabase();
initDraw(intent);
}
private void initView(Intent intent) {
boolean isLocal = !intent.getBooleanParam(IS_FORM_LOCAL_KEY, false);
if (!isLocal) {
storeId = intent.getStringParam(STORE_ID_KEY);
}
title.setText(isLocal ? "本地端" : "远程端");
transform.setVisibility(isLocal ? Component.VISIBLE : Component.INVISIBLE);
}
private void requestPermission() {
if (verifySelfPermission(DISTRIBUTED_DATASYNC) != IBundleManager.PERMISSION_GRANTED) {
if (canRequestPermission(DISTRIBUTED_DATASYNC)) {
requestPermissionsFromUser(new String[]{DISTRIBUTED_DATASYNC}, PERMISSION_CODE);
}
}
}
private void findComponentById() {
if (findComponentById(ResourceTable.Id_canvas) instanceof DependentLayout) {
canvas = (DependentLayout) findComponentById(ResourceTable.Id_canvas);
}
if (findComponentById(ResourceTable.Id_transform) instanceof Image) {
transform = (Image) findComponentById(ResourceTable.Id_transform);
}
if (findComponentById(ResourceTable.Id_title) instanceof Text) {
title = (Text) findComponentById(ResourceTable.Id_title);
}
if (findComponentById(ResourceTable.Id_back) instanceof Button) {
back = (Button) findComponentById(ResourceTable.Id_back);
}
transform.setClickedListener(component -> {
DeviceSelectDialog dialog = new DeviceSelectDialog(MainAbilitySlice.this);
dialog.setListener(deviceIds -> {
if (deviceIds != null && !deviceIds.isEmpty()) {
// 启动远程页面
startRemoteFas(deviceIds);
// 同步远程数据库
singleKvStore.sync(deviceIds, SyncMode.PUSH_ONLY);
}
dialog.hide();
});
dialog.show();
});
}
/**
* Initialize art boards
*
* @param intent Intent
*/
private void initDraw(Intent intent) {
int colorIndex = intent.getIntParam(COLOR_INDEX_KEY, 0);
drawl = new DrawPoint(this, colorIndex);
drawl.setWidth(MATCH_PARENT);
drawl.setWidth(MATCH_PARENT);
canvas.addComponent(drawl);
drawPoints();
drawl.setOnDrawBack(points -> {
if (points != null && points.size() > 1) {
String pointsString = GsonUtil.objectToString(points);
LogUtils.info(TAG, "pointsString::" + pointsString);
if (singleKvStore != null) {
singleKvStore.putString(POINTS_KEY, pointsString);
}
}
});
back.setClickedListener(component -> {
List<MyPoint> points = drawl.getPoints();
if (points == null || points.size() <= 1) {
return;
}
points.remove(points.size() - 1);
for (int i = points.size() - 1; i >= 0; i--) {
if (points.get(i).isLastPoint()) {
break;
}
points.remove(i);
}
drawl.setDrawParams(points);
String pointsString = GsonUtil.objectToString(points);
if (singleKvStore != null) {
singleKvStore.putString(POINTS_KEY, pointsString);
}
});
}
// 获取数据库中的点数据,并在画布上画出来
private void drawPoints() {
List<Entry> points = singleKvStore.getEntries(POINTS_KEY);
for (Entry entry : points) {
if (entry.getKey().equals(POINTS_KEY)) {
List<MyPoint> remotePoints = GsonUtil.jsonToList(singleKvStore.getString(POINTS_KEY), MyPoint.class);
getUITaskDispatcher().delayDispatch(() -> drawl.setDrawParams(remotePoints), DELAY_TIME);
}
}
}
/**
* Receive database messages
*
* @since 2021-04-06
*/
private class KvStoreObserverClient implements KvStoreObserver {
@Override
public void onChange(ChangeNotification notification) {
LogUtils.info(TAG, "data changed......");
drawPoints();
}
}
private void initDatabase() {
// 创建分布式数据库管理对象
KvManagerConfig config = new KvManagerConfig(this);
kvManager = KvManagerFactory.getInstance().createKvManager(config);
// 创建分布式数据库
Options options = new Options();
options.setCreateIfMissing(true).setEncrypt(false).setKvStoreType(KvStoreType.SINGLE_VERSION);
singleKvStore = kvManager.getKvStore(options, storeId);
// 订阅分布式数据变化
KvStoreObserver kvStoreObserverClient = new KvStoreObserverClient();
singleKvStore.subscribe(SubscribeType.SUBSCRIBE_TYPE_ALL, kvStoreObserverClient);
}
/**
* Starting Multiple Remote Fas
*
* @param deviceIds deviceIds
*/
private void startRemoteFas(List<String> deviceIds) {
Intent[] intents = new Intent[deviceIds.size()];
for (int i = 0; i < deviceIds.size(); i++) {
Intent intent = new Intent();
intent.setParam(IS_FORM_LOCAL_KEY, true);
intent.setParam(COLOR_INDEX_KEY, i + 1);
intent.setParam(STORE_ID_KEY, storeId);
Operation operation = new Intent.OperationBuilder()
.withDeviceId(deviceIds.get(i))
.withBundleName(getBundleName())
.withAbilityName(MainAbility.class.getName())
.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
.build();
intent.setOperation(operation);
intents[i] = intent;
}
startAbilities(intents);
}
@Override
protected void onStop() {
super.onStop();
kvManager.closeKvStore(singleKvStore);
}
}
DrawPoint.java
public class DrawPoint extends Component implements Component.DrawTask {
private static final int STROKE_WIDTH = 15;
private static final int TIME = 200;
private static final int EVENT_MSG_STORE = 0x1000002;
private final Color[] paintColors = new Color[]{Color.RED, Color.BLUE, Color.BLACK};
private List<MyPoint> points = new ArrayList<>(0);
private Paint paint;
private OnDrawCallBack callBack;
private Timer timer = null;
private TimerTask timerTask = null;
private final EventHandler handler = new EventHandler(EventRunner.current()) {
@Override
protected void processEvent(InnerEvent event) {
if (EVENT_MSG_STORE == event.eventId) {
callBack.callBack(points);
}
}
};
/**
* Drawl constructor
*
* @param context context
* @param colorIndex colorIndex
*/
public DrawPoint(Context context, int colorIndex) {
super(context);
init(colorIndex);
}
public List<MyPoint> getPoints() {
return points;
}
/**
* SelectResult
*
* @since 2020-12-03
*/
public interface OnDrawCallBack {
/**
* touchListener
*
* @param points points
*/
void callBack(List<MyPoint> points);
}
/**
* setPoints
*
* @param myPoints myPoints
*/
public void setDrawParams(List<MyPoint> myPoints) {
this.points = myPoints;
invalidate();
}
/**
* setOnDrawBack
*
* @param onCallBack onCallBack
*/
public void setOnDrawBack(OnDrawCallBack onCallBack) {
this.callBack = onCallBack;
}
private void init(int colorIndex) {
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE_STYLE);
paint.setStrokeWidth(STROKE_WIDTH);
addDrawTask(this);
Color color = getRandomColor(colorIndex);
setTouchEventListener((component, touchEvent) -> {
scheduledTask();
int crtX = (int) touchEvent.getPointerPosition(touchEvent.getIndex()).getX();
int crtY = (int) touchEvent.getPointerPosition(touchEvent.getIndex()).getY();
MyPoint point = new MyPoint(crtX, crtY);
point.setPaintColor(color);
switch (touchEvent.getAction()) {
case TouchEvent.POINT_MOVE:
points.add(point);
break;
case TouchEvent.PRIMARY_POINT_UP:
points.add(point);
point.setLastPoint(true);
callBack.callBack(points);
onTimerFinish();
break;
default:
break;
}
invalidate();
return true;
});
}
/**
* scheduled task start
*/
public void scheduledTask() {
if (timer == null && timerTask == null) {
timer = new Timer();
timerTask = new TimerTask() {
@Override
public void run() {
handler.sendEvent(EVENT_MSG_STORE);
}
};
timer.schedule(timerTask, 0, TIME);
}
}
/**
* Canceling a Scheduled Task
*/
public void onTimerFinish() {
timer.cancel();
timer = null;
timerTask = null;
}
@Override
public void onDraw(Component component, Canvas canvas) {
draw(points, canvas);
}
private void draw(List<MyPoint> myPoints, Canvas canvas) {
if (myPoints == null || myPoints.size() <= 1) {
return;
}
Point first = null;
Point last = null;
for (MyPoint myPoint : myPoints) {
paint.setColor(myPoint.getPaintColor());
float finalX = myPoint.getPositionX();
float finalY = myPoint.getPositionY();
Point finalPoint = new Point(finalX, finalY);
if (myPoint.isLastPoint()) {
first = null;
last = null;
continue;
}
if (first == null) {
first = finalPoint;
} else {
if (last != null) {
first = last;
}
last = finalPoint;
canvas.drawLine(first, last, paint);
}
}
}
private Color getRandomColor(int index) {
return index > paintColors.length - 1 ? paintColors[0] : paintColors[index];
}
}
「 原理 」
初始化分布式数据库 initDatabase(),订阅消息监听
点击共享后,获取设备列表,启动远程页面 startRemoteFas(deviceIds)
绘制过程中在 DrowPoint 中 TouchEventListener 收集坐标点信息
每过200ms触发一次任务回调中在 drawl.setOnDrawBack 回调中将绘图轨迹坐标写入分布式数据库 singleKvStore.putString(POINTS_KEY, pointsString)
其他设备,在消息监听订阅回调 KvStoreObserver 回调函数 onchange 中,获取分布式数据库中数据并完成绘制
分布式数据库底层通过最终一致性方案,完成数据同步传递
「 特点 」
多设备实时协同操作(不是投屏)。
「 应用场景 」
师生课堂互动。
3 分布式任务调度
「 项目 」
「 效果 」
![](https://img-blog.csdnimg.cn/img_convert/f40d04914e40419e8c52dd4d3d6cc352.png)
图3 视频播放器
![](https://img-blog.csdnimg.cn/img_convert/d98640974bc34d478bbf2ec2050ba88e.png)
图4 手机输入法
如图,图3为电视视频筛选页面,通过图4的手机输入法输入文本同步到图3视频搜索输入框。
「 重点代码 」
MainAbilitySlice.java
public class MainAbilitySlice extends AbilitySlice implements PermissionBridge.OnPermissionStateListener {
private static final String ABILITY_NAME = "com.huawei.codelab.RemoteInputAbility";
private static final String MOVIE_PLAY_ABILITY = "com.huawei.codelab.MoviePlayAbility";
private static final String MAIN_ABILITY = "com.huawei.codelab.MainAbility";
private static final int FOCUS_PADDING = 8;
private static final int SEARCH_PADDING = 3;
private static final int LIST_INIT_SIZE = 16;
private static final String TAG = MainAbilitySlice.class.getName();
private static final Lock MOVE_LOCK = new ReentrantLock();
private MainAbilitySlice.MyCommonEventSubscriber subscriber;
private TextField tvTextInput;
private ScrollView scrollView;
private DirectionalLayout keyBoardLayout;
private TableLayout movieTableLayout;
private Size size;
private final AbilityMgr abilityMgr = new AbilityMgr(this);
// 搜索的到的影片
private final List<ComponentPointData> movieSearchList = new ArrayList<>(LIST_INIT_SIZE);
// 当前焦点所在位置
private ComponentPointData componentPointDataNow;
// 注册流转任务管理服务后返回的Ability token
private int abilityToken;
// 用户在设备列表中选择设备后返回的设备ID
private String selectDeviceId;
// 获取流转任务管理服务管理类
private IContinuationRegisterManager continuationRegisterManager;
// 设置监听FA流转管理服务设备状态变更的回调
private final IContinuationDeviceCallback callback = new IContinuationDeviceCallback() {
@Override
public void onDeviceConnectDone(String deviceId, String s1) {
selectDeviceId = deviceId;
abilityMgr.openRemoteAbility(selectDeviceId, getBundleName(), ABILITY_NAME);
getUITaskDispatcher().asyncDispatch(() -> continuationRegisterManager.updateConnectStatus(abilityToken, selectDeviceId,
DeviceConnectState.IDLE.getState(), null));
}
@Override
public void onDeviceDisconnectDone(String deviceId) {
}
};
// 设置注册FA流转管理服务服务回调
private final RequestCallback requestCallback = new RequestCallback() {
@Override
public void onResult(int result) {
abilityToken = result;
}
};
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);
new PermissionBridge().setOnPermissionStateListener(this);
// 全屏设置
WindowManagerUtils.setWindows();
// 初始化布局
initComponent();
// 初始化按钮组件
initKeyBoardComponent(keyBoardLayout);
// 初始化影片图片组件
initMovieTableComponent(movieTableLayout);
// 事件订阅
subscribe();
}
private void registerTransService() {
continuationRegisterManager = getContinuationRegisterManager();
// 增加过滤条件
ExtraParams params = new ExtraParams();
String[] devTypes = new String[]{ExtraParams.DEVICETYPE_SMART_PAD, ExtraParams.DEVICETYPE_SMART_PHONE};
params.setDevType(devTypes);
// 注册FA流转管理服务
continuationRegisterManager.register(getBundleName(), params, callback, requestCallback);
}
private void initMovieTableComponent(TableLayout tableLayout) {
int index = 0;
while (index < tableLayout.getChildCount()) {
DirectionalLayout childLayout = null;
if (tableLayout.getComponentAt(index) instanceof DirectionalLayout) {
childLayout = (DirectionalLayout) tableLayout.getComponentAt(index);
}
ComponentPointData componentPointData = new ComponentPointData();
Component component = null;
int indexMovie = 0;
while (childLayout != null && indexMovie < childLayout.getChildCount()) {
Component comChild = childLayout.getComponentAt(indexMovie);
indexMovie++;
if (comChild instanceof Text) {
componentPointData = ComponentPointDataMgr
.getConstantMovie(((Text) comChild).getText()).orElse(null);
continue;
}
component = findComponentById(comChild.getId());
}
if (componentPointData != null && component != null) {
componentPointData.setComponentId(component.getId());
}
ComponentPointDataMgr.getComponentPointDataMgrs().add(componentPointData);
index++;
}
}
private void initKeyBoardComponent(DirectionalLayout directionalLayout) {
int index = 0;
while (index < directionalLayout.getChildCount()) {
DirectionalLayout childLayout = null;
if (directionalLayout.getComponentAt(index) instanceof DirectionalLayout) {
childLayout = (DirectionalLayout) directionalLayout.getComponentAt(index);
}
int indexButton = 0;
while (childLayout != null && indexButton < childLayout.getChildCount()) {
if (childLayout.getComponentAt(indexButton) instanceof Button) {
Button button = (Button) childLayout.getComponentAt(indexButton);
buttonInit(button);
indexButton++;
}
}
index++;
}
}
private void initComponent() {
if (findComponentById(ResourceTable.Id_scrollview) instanceof ScrollView) {
scrollView = (ScrollView) findComponentById(ResourceTable.Id_scrollview);
}
if (findComponentById(ResourceTable.Id_TV_input) instanceof TextField) {
tvTextInput = (TextField) findComponentById(ResourceTable.Id_TV_input);
// 初始化默认选中效果
ComponentPointData componentPointData = new ComponentPointData();
componentPointData.setComponentId(tvTextInput.getId());
componentPointData.setPointX(0);
componentPointData.setPointY(1);
tvTextInput.requestFocus();
componentPointDataNow = componentPointData;
ComponentPointDataMgr.getComponentPointDataMgrs().add(componentPointDataNow);
// 点击事件触发设备选取弹框
tvTextInput.setClickedListener(component -> showDevicesDialog());
}
if (findComponentById(ResourceTable.Id_keyBoardComponent) instanceof DirectionalLayout) {
keyBoardLayout = (DirectionalLayout) findComponentById(ResourceTable.Id_keyBoardComponent);
}
if (findComponentById(ResourceTable.Id_tableLayout) instanceof TableLayout) {
movieTableLayout = (TableLayout) findComponentById(ResourceTable.Id_tableLayout);
}
if (findComponentById(ResourceTable.Id_image_ten) instanceof Image) {
Image image = (Image) findComponentById(ResourceTable.Id_image_ten);
size = image.getPixelMap().getImageInfo().size;
}
}
private void showDevicesDialog() {
ExtraParams extraParams = new ExtraParams();
extraParams.setDevType(new String[]{ExtraParams.DEVICETYPE_SMART_TV,
ExtraParams.DEVICETYPE_SMART_PHONE});
extraParams.setDescription("远程遥控器");
continuationRegisterManager.showDeviceList(abilityToken, extraParams, result -> LogUtils.info(TAG, "show devices success"));
}
private void buttonInit(Button button) {
if (button.getId() == ResourceTable.Id_del) {
findComponentById(button.getId()).setClickedListener(component -> {
if (tvTextInput.getText().length() > 0) {
tvTextInput.setText(tvTextInput.getText().substring(0, tvTextInput.getText().length() - 1));
}
});
} else if (button.getId() == ResourceTable.Id_clear) {
findComponentById(button.getId()).setClickedListener(component -> tvTextInput.setText(""));
} else {
findComponentById(button.getId()).setClickedListener(component -> tvTextInput.append(button.getText()));
}
}
@Override
public void onActive() {
super.onActive();
}
@Override
public void onForeground(Intent intent) {
super.onForeground(intent);
}
@Override
public void onPermissionGranted() {
registerTransService();
}
@Override
public void onPermissionDenied() {
terminate();
}
/**
* MyCommonEventSubscriber
*
* @since 2020-12-03
*/
class MyCommonEventSubscriber extends CommonEventSubscriber {
MyCommonEventSubscriber(CommonEventSubscribeInfo info) {
super(info);
}
@Override
public void onReceiveEvent(CommonEventData commonEventData) {
Intent intent = commonEventData.getIntent();
int requestType = intent.getIntParam("requestType", 0);
String inputString = intent.getStringParam("inputString");
if (requestType == ConnectManagerIml.REQUEST_SEND_DATA) {
tvTextInput.setText(inputString);
} else if (requestType == ConnectManagerIml.REQUEST_SEND_SEARCH) {
// 如果当前选中的是文本框则搜索影片;如果当前选中的是影片则播放影片;X轴坐标为0当前选中文本框;X轴大于1当前选中影片
if (componentPointDataNow.getPointX() == 0) {
// 调用大屏的搜索方法
searchMovies(tvTextInput.getText());
return;
}
// 播放影片
abilityMgr.playMovie(getBundleName(), MOVIE_PLAY_ABILITY);
} else {
// 移动方向
String moveString = intent.getStringParam("move");
MainCallBack.movePoint(MainAbilitySlice.this, moveString);
}
}
}
/**
* goBack
*/
public void goBack() {
clearLastBackg();
scrollView.scrollTo(0, 0);
componentPointDataNow = ComponentPointDataMgr.getMoviePoint(0, 1).orElse(null);
findComponentById(ResourceTable.Id_TV_input).requestFocus();
abilityMgr.returnMainAbility(getBundleName(), MAIN_ABILITY);
}
/**
* move
*
* @param pointX pointX
* @param pointY pointY
*/
public void move(int pointX, int pointY) {
MOVE_LOCK.lock();
try {
// 设置焦点滚动
if (pointX == 0 && componentPointDataNow.getPointX() > 0) {
scrollView.fluentScrollByY(pointY * size.height);
}
if (componentPointDataNow.getPointX() == 0 && pointX == 1) {
scrollView.scrollTo(0, 0);
}
// 设置背景
if (componentPointDataNow.getPointX() + pointX == 0) {
setBackGround(componentPointDataNow.getPointX() + pointX, 1);
} else {
setBackGround(componentPointDataNow.getPointX() + pointX, componentPointDataNow.getPointY() + pointY);
}
} finally {
MOVE_LOCK.unlock();
}
}
private void setBackGround(int pointX, int pointY) {
ComponentPointData componentPointDataNew = ComponentPointDataMgr.getMoviePoint(pointX, pointY).orElse(null);
if (componentPointDataNew == null) {
return;
}
// 清除上次选中的效果
clearLastBackg();
componentPointDataNow = componentPointDataNew;
if (findComponentById(componentPointDataNow.getComponentId()) instanceof Image) {
Image newImage = (Image) findComponentById(componentPointDataNow.getComponentId());
newImage.setPadding(FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING);
} else {
Component component = findComponentById(componentPointDataNow.getComponentId());
component.requestFocus();
}
}
private void clearLastBackg() {
Component component = null;
if (findComponentById(componentPointDataNow.getComponentId()) instanceof TextField) {
TextField textField = (TextField) findComponentById(componentPointDataNow.getComponentId());
textField.clearFocus();
} else {
component = findComponentById(componentPointDataNow.getComponentId());
component.setPadding(0, 0, 0, 0);
}
// 如果是搜索出来的还是保持搜索到的背景
for (ComponentPointData componentPointData : movieSearchList) {
if (component != null && componentPointData.getComponentId() == component.getId()) {
component.setPadding(SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING);
}
}
}
private void searchMovies(String text) {
if (text == null || "".equals(text)) {
return;
}
// 清空上次搜索结果及背景效果
clearHistroyBackGround();
for (ComponentPointData componentPointData : ComponentPointDataMgr.getComponentPointDataMgrs()) {
if (MovieSearchUtils.isContainMovie(componentPointData.getMovieName(), text)
|| MovieSearchUtils.isContainMovie(componentPointData.getMovieFirstName(), text)) {
movieSearchList.add(componentPointData);
Component component = findComponentById(componentPointData.getComponentId());
component.setPadding(SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING, SEARCH_PADDING);
}
}
if (movieSearchList.size() > 0) {
componentPointDataNow = movieSearchList.get(0);
Component component = findComponentById(componentPointDataNow.getComponentId());
component.setPadding(FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING, FOCUS_PADDING);
} else {
Component component = findComponentById(componentPointDataNow.getComponentId());
component.requestFocus();
}
}
private void clearHistroyBackGround() {
// 清空上次搜索的结果
for (ComponentPointData componentPointData : movieSearchList) {
Component component = findComponentById(componentPointData.getComponentId());
component.setPadding(0, 0, 0, 0);
}
movieSearchList.clear();
// 去掉当前焦点背景
clearLastBackg();
}
private void subscribe() {
MatchingSkills matchingSkills = new MatchingSkills();
matchingSkills.addEvent(EventConstants.SCREEN_REMOTE_CONTROLL_EVENT);
matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON);
CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills);
subscriber = new MainAbilitySlice.MyCommonEventSubscriber(subscribeInfo);
try {
CommonEventManager.subscribeCommonEvent(subscriber);
} catch (RemoteException e) {
LogUtils.error("", "subscribeCommonEvent occur exception.");
}
}
private void unSubscribe() {
try {
CommonEventManager.unsubscribeCommonEvent(subscriber);
} catch (RemoteException e) {
LogUtils.error(TAG, "unSubscribe Exception");
}
}
@Override
protected void onStop() {
super.onStop();
unSubscribe();
}
}
RemoteInputAbilitySlice.java
public class RemoteInputAbilitySlice extends AbilitySlice {
private static final int SHOW_KEYBOARD_DELAY = 800;
private static final int INIT_SIZE = 8;
private ConnectManager connectManager;
private TextField textField;
private String deviceIdConn;
private Component okButton;
private Component leftButton;
private Component rightButton;
private Component upButton;
private Component downButton;
private Component goBackButton;
private Component closeButton;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_remote_input);
// 获取大屏ID用于配对
deviceIdConn = intent.getStringParam("localDeviceId");
// 全屏设置
WindowManagerUtils.setWindows();
initView();
initListener();
showKeyBoard();
initConnManager();
}
@Override
public void onActive() {
super.onActive();
}
@Override
public void onForeground(Intent intent) {
super.onForeground(intent);
}
private void initView() {
if (findComponentById(ResourceTable.Id_remote_input) instanceof TextField) {
textField = (TextField) findComponentById(ResourceTable.Id_remote_input);
textField.requestFocus();
}
okButton = findComponentById(ResourceTable.Id_ok_button);
leftButton = findComponentById(ResourceTable.Id_left_button);
rightButton = findComponentById(ResourceTable.Id_right_button);
upButton = findComponentById(ResourceTable.Id_up_button);
downButton = findComponentById(ResourceTable.Id_down_button);
goBackButton = findComponentById(ResourceTable.Id_go_back);
closeButton = findComponentById(ResourceTable.Id_close_fa);
}
private void initListener() {
// 监听文本变化,远程同步
textField.addTextObserver((ss, ii, i1, i2) -> {
Map<String, String> map = new HashMap<>(INIT_SIZE);
map.put("inputString", ss);
connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_DATA, map);
});
okButton.setClickedListener(component -> {
// 点击OK按钮
buttonClickSound();
String searchString = textField.getText();
Map<String, String> map = new HashMap<>(INIT_SIZE);
map.put("inputString", searchString);
connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_SEARCH, map);
});
leftButton.setClickedListener(component -> {
// 点击左键按钮
sendMoveRequest(Constants.MOVE_LEFT);
});
rightButton.setClickedListener(component -> {
// 点击右键按钮
sendMoveRequest(Constants.MOVE_RIGHT);
});
upButton.setClickedListener(component -> {
// 点击向上按钮
sendMoveRequest(Constants.MOVE_UP);
});
downButton.setClickedListener(component -> {
// 点击向下按钮
sendMoveRequest(Constants.MOVE_DOWN);
});
goBackButton.setClickedListener(component -> {
// 返回大屏主页
sendMoveRequest(Constants.GO_BACK);
});
closeButton.setClickedListener(component -> {
// 返回主页
sendMoveRequest(Constants.GO_BACK);
terminateAbility();
});
}
private void sendMoveRequest(String direction) {
buttonClickSound();
Map<String, String> map = new HashMap<>(INIT_SIZE);
map.put("move", direction);
connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_MOVE, map);
}
private void showKeyBoard() {
getUITaskDispatcher().delayDispatch(() -> textField.simulateClick(), SHOW_KEYBOARD_DELAY);
}
private void initConnManager() {
connectManager = ConnectManagerIml.getInstance();
connectManager.connectPa(this, deviceIdConn);
}
private void buttonClickSound() {
// 步骤1:实例化对象
SoundPlayer soundPlayer = new SoundPlayer("packageName");
// 步骤2:播放键盘敲击音,音量为1.0
soundPlayer.playSound(SoundPlayer.SoundType.KEY_CLICK, 1.0f);
}
}
「 原理 」
MainAbilitySlice#onStart 生命周期中会做事件注册,类似 Android 中注册广播接收 subscribe() 。
在事件接收者 onReceiveEvent 中处理来自手机输入法输入的数据/请求查询/播放等命令。如果是数据则显示在电视端文本输入框中,并搜索该影片。
MainAbilitySlice#callback 当监听到设备组网连接成功后,打开 RemoteInputAbilitySlice 该页面就是手机端输入页面,在 initConnManager 中会打开 PA 类似 Android 中 service,在 IAbilityConnection 中回拿到远端电视端的引用。
IRemoteObject 接口,类似 Android 中拿到 Ibinder 接口。后续会用该接口给电视端发送数据。
手机端输入页面监听用户输入文本变化后会 connectManager.sendRequest(ConnectManagerIml.REQUEST_SEND_DATA, map),通过代理最终通过 IRemoteObject 接口给电视端发数据。后续到2电视 onReceiveEvent 接收到数据后处理对应命令。
按照华为官网描述该过程基于分布式任务调度和公共事件,将远端移动操作请求发送给TV端,类比 Android 通过 binder 机制完成跨进程 Service 通信,鸿蒙分布式调度底层通过极简D2D传输协议栈。
「 特点 」
跨设备分布式任务调度,硬件互助。
「 应用场景 」
冰箱通过手机输入法输入,家电使用手机相机作为输入等。