BilibiliCards
Проект предварительного просмотра видео
<iframe width="960" height="540" src="//player.bilibili.com/player.html?aid=249580798&bvid=BV1cv411K7Pu&cid=380159498&page=1" frameborder="0" allowfullscreen>Это приложение сервисной карточки для чистой версии HarmonyOS от Bilibili.
HarmonyOS была выпущена 2 июня этого года, и июнь уже был захвачен HarmonyOS. Самое очевидное изменение, вероятно, это сервисные карточки. Я также изучаю HarmonyOS и практикуюсь в разработке сервисных карточек. Давайте посмотрим на конечный результат.
 |  |  |
---|---|---|
Далее я расскажу о процессе разработки. Моя среда разработки: IDE: DevEco Studio 2.1 Release SDK: API Version 5
Я пропущу часть установки программного обеспечения и создания проекта, так как думаю, что все знакомы с этим процессом. Перейдём к созданию сервисной карточки.
Существует четыре размера карточек: микрокарточка, маленькая карточка, средняя карточка и большая карточка. Официально предоставляются четыре базовых шаблона и двенадцать продвинутых шаблонов. Можно выбрать. Базовые шаблоны следующие: 
Основная цель разработки сервисной карточки — отображение информации и прямой доступ к услугам. Исходя из этой концепции, я выбрал несколько функций Bilibili, которые я часто использую, например, список отслеживания сериалов.

Как видно из таблицы ниже, рекомендуется использовать JS. Источник таблицы:
Сценарии | Java-карточки | JS-карточки | Поддерживаемые версии |
---|---|---|---|
Реальное обновление (например, часы) | Для реального обновления Java использует ComponentProvider, стоимость которого высока | JS может выполнять обновление на стороне клиента, но требует настраиваемых компонентов | HarmonyOS 2.0 и выше |
Метод разработки | В Java UI на стороне карточки необходимо одновременно обрабатывать данные и компоненты, создавая ComponentProvider для удалённого рендеринга | На стороне использования JS загружается и отображается, а на стороне провайдера требуется только обработка данных, компонентов и логики | HarmonyOS 2.0 и выше |
Поддержка компонентов | Text, Image, DirectionalLayout, PositionLayout, DependentLayout | div, list, list-item, swiper, stack, image, text, span, progress, button (настраиваемые: chart, clock, calendar) | HarmonyOS 2.0 и выше |
Динамические эффекты внутри карточки | Не поддерживается | Пока не открыто | HarmonyOS 2.0 и выше |
Тень и размытие | Не поддерживается | Поддерживается | HarmonyOS 2.0 и выше |
Адаптивный макет | Не поддерживается | Поддерживается | HarmonyOS 2.0 и выше |
Пользовательские переходы на страницу внутри карточки | Не поддерживается | Поддерживается | HarmonyOS 2.0 и выше |
В основном это добавление разрешений на доступ к сети, отправка сетевого запроса и получение возвращаемого значения JSON для последующего анализа и извлечения нужных данных.
Добавление сетевых разрешений: В файле конфигурации config.json в модуле module необходимо добавить:
{
... ...
"module": {
... ...
"reqPermissions": [{"name":"ohos.permission.INTERNET"}]
}
}
Добавление зависимостей: Необходимо найти файл entry/build.gradle и в разделе dependencies добавить следующее:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.har'])
testImplementation 'junit:junit:4.13'
ohosTestImplementation 'com.huawei.ohos.testkit:runner:1.0.0.100'
// ZZRHttp позволяет выполнять HTTP-запросы в отдельном процессе
implementation 'com.zzrv5.zzrhttp:ZZRHttp:1.0.1'
// fastjson позволяет анализировать формат JSON
implementation group: 'com.alibaba', name: 'fastjson', version: '1.2.75'
}
HTTP-запрос: Для примера возьмём получение количества подписчиков. Если в браузере ввести https://api.bilibili.com/x/relation/stat?vmid=383565952 (где vmid — это ID пользователя, которого вы хотите запросить), то значение follower будет равно количеству подписчиков.
Можно использовать HttpURLConnection или okhttp, но это требует создания дочерних потоков, обработки исключений и других операций. Поэтому здесь используется ZZRHttp, который был разработан ZZR-учителем.
Код реализации:
// Получение количества подписчиков Bilibili, здесь мы используем ZZRHttp
String url = "https://api.bilibili.com/x/relation/stat?vmid=383565952";
ZZRHttp.get(url, new ZZRCallBack.CallBackString() {
@Override
public void onFailure(int i, String s) {
HiLog.info(TAG, "API вернул ошибку");
}
@Override
public void onResponse(String s) {
HiLog.info(TAG, "Ответ API успешный");
// Если ответ успешный, результат будет сохранён в строке s.
// s = {"code":0,"message":"0","ttl":1,"data":{"mid":383565952,"following":70,"whisper":0,"black":0,"follower":5384}}
}
});
Анализ JSON: Полученный ответ представляет собой данные в формате JSON. Чтобы получить значение follower, необходимо проанализировать JSON.
Сначала создайте класс Java на основе содержимого JSON. Это можно сделать вручную или использовать онлайн-инструменты для генерации классов из JSON.
public class BilibiliFollower {
public static class Data {
private int follower;
public int getFollower() {
return follower;
}
public void setFollower(int follower) {
this.follower = follower;
}
}
private BilibiliFollower.Data data;
public BilibiliFollower.Data getData() {
return data;
}
public void setData(BilibiliFollower.Data data) {
this.data = data;
}
}
Затем используйте fastjson для анализа JSON и сохранения результата в классе BilibiliFollower:
// Анализ JSON с использованием fastjson
try {
// 1. Вызов fastjson для разбора, результат сохраняется в классе JSON
BilibiliFollower bilibiliFollower = JSON.parseObject(s, BilibiliFollower.class);
// 2. Метод get для получения проанализированных данных
BilibiliFollower.Data data = bilibiliFollower.getData();
System.out.println("Анализ успешен: " + data.getFollower());
} catch (Exception e) {
HiLog.info(TAG, "Анализ не удался");
}
Заключение: Обязательно добавьте сетевые разрешения, иначе вы не сможете получить данные. Были добавлены два пакета зависимостей, что упрощает извлечение данных. Процесс получения других данных карточек аналогичен, но код более сложный, поэтому он не демонстрируется здесь. Если вам интересно, вы можете загрузить полный код и посмотреть его. ``` https://api.bilibili.com/x/relation/stat?vmid=383565952; ZZRHttp.get(url, new ZZRCallBack.CallBackString() { @Override public void onFailure(int i, String s) {HiLog.info(TAG, "API返回失败");} @Override public void onResponse(String s) { HiLog.info(TAG, "API返回成功"); try { //1.调用fastjson解析,结果保存在JSON对应的类 BilibiliFollower bilibiliFollower = JSON.parseObject(s,BilibiliFollower.class); //2.get方法获取解析内容 BilibiliFollower.Data data= bilibiliFollower.getData(); System.out.println("解析成功" + data.getFollower());
//这部分用来更新卡片信息
ZSONObject zsonObject = new ZSONObject(); //1.将要刷新的数据存放在一个ZSONObject实例中
zsonObject.put("follower",data.getFollower()); //2.更新数据,data.getFollower()就是在API数据请求中获取的粉丝数。
FormBindingData formBindingData = new FormBindingData(zsonObject); //3.将其封装在一个FormBindingData的实例中
try {
((MainAbility)context).updateForm(formId,formBindingData); //4.调用MainAbility的方法updateForm(),并将formBindingData作为第二个实参
} catch (FormException e) {
e.printStackTrace();
HiLog.info(TAG, "更新卡片失败");
}
} catch (Exception e) {
HiLog.info(TAG, "解析失败");
}
}
}); }```
正常来说这样就可以正常更新数据了,但是会有个问题。就是在服务卡片首次创建添加到桌面的时候,在添加完的至少30分钟里,数据是不会更新的。此时如果在index.json中设置初始信息,那么在添加完成的前30分钟数据都是写死在data中的。如果不设置初始信息那么卡片就是空白的。
Поэтому, согласно анализу механизма работы сервисной карточки, нам также необходимо выполнить однократное обновление при инициализации карточки в onCreateForm(). Это очень просто, вызовите onUpdateForm(formId) в onCreateForm().
@Override
protected ProviderFormInfo onCreateForm(Intent intent) {
... ...
//初始化时先在线更新一下卡片
onUpdateForm(formId);
return formController.bindFormData();
}
Резюме: В методе onUpdateForm(formId), сетевой запрос API должен быть запущен в новом дочернем потоке, иначе это повлияет на загрузку страницы. Это также причина использования ZZRhttp. Однако теперь возникает другая проблема: когда количество карточек становится большим, одновременное обновление такого количества карточек может стать очень медленным, и эта проблема еще требует решения.
В настоящее время сервисные карточки поддерживают только общие события щелчка, типы событий: событие перехода (router) и событие сообщения (message). Для получения более подробной информации см. официальную документацию.
Далее реализуем взаимодействие с сервисными карточками. Когда пользователь нажимает на сервисную карточку, он переходит на соответствующую страницу, поэтому здесь используется событие перехода. Например, карточка для отслеживания сериалов.
Сначала мы должны добавить страницу, на которую нужно перейти. В качестве примера возьмем карточку для отслеживания сериалов:
@Override public void onStart(Intent intent) { super.onStart(intent); super.setUIContent(ResourceTable.Layout_ability_video);
Text text = (Text) findComponentById(ResourceTable.Id_text); text.setText("Страница перехода в процессе");
// Случайный массив изображений int[] resource = {ResourceTable.Media_36e,ResourceTable.Media_36g,ResourceTable.Media_36h,ResourceTable.Media_38p}; Component component = findComponentById(ResourceTable.Id_image); if (component instanceof Image) { Image image = (Image) component; image.setPixelMap(resource[(int)(Math.random()*3)]);//случайное отображение одной картинки }
String url = "https://m.bilibili.com";
String param = intent.getStringParam("params");//получить значение поля params из intent, которое определяет событие перехода if(param !=null){ ZSONObject data = ZSONObject.stringToZSON(param); url = data.getString("url"); }
webview(url); } //запустить webview public void webview(String url){ WebView webView = (WebView) findComponentById(ResourceTable.Id_webview); webView.getWebConfig().setJavaScriptPermit(true); // Если веб-странице требуется использовать JavaScript, добавьте эту строку; как использовать JavaScript подробно описано ниже webView.load(url); }
3. Добавьте webview, чтобы заменить текстовый элемент управления по умолчанию на webview.
```xml
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:alignment="center"
ohos:orientation="vertical">
<ohos.agp.components.webengine.WebView
ohos:id="$+id:webview"
ohos:height="match_parent"
ohos:width="match_parent">
``` **2. Тестирование программного обеспечения**
Здесь используется видеодинамическая сервисная карточка, и проводится тестирование события сообщения. Эффект показан на рисунке: при нажатии на левую или правую сторону реализуется скольжение сервисной карточки. В маленькой карточке такой опыт работы не очень хорош. Поэтому пример события сообщения предназначен только для тестирования и не добавлен в проект.
1. В index.hml добавьте onclick к элементам, которые должны запускать событие, например: onclick="sendMessageEvent".
```html
<!-- Для удобства тестирования напрямую добавляем onclick в левый и правый div-компоненты -->
<div class="div" onclick="sendMessageEvent0">
<image class="item_image" src="{{ src0 }}"></image>
<text class="item_title">{{ itemTitle0 }}</text>
<text class="item_content">{{ itemContent0 }}</text>
</div>
<div class="div" onclick="sendMessageEvent1">
<image class="item_image" src="{{ src1 }}"></image>
<text class="item_title">{{ itemTitle1 }}</text>
<text class="item_content>{{ itemContent1 }}</text>
</div>
В index.json добавьте соответствующие действия.
{ "data": { }, "actions": { "sendMessageEvent0": { "action": "message", "params": { "p1": "left", "index": "{{index}}" } }, "sendMessageEvent1": { "action": "message", "params": { "p1": "right", "index": "{{index}}" } } } }
3. Если это событие сообщения (message), то при нажатии на элемент с onclick будет запущена функция в MainAbility.
```java
@Override
protected void onTriggerFormEvent(long formId, String message) {
HiLog.info(TAG, "onTriggerFormEvent: " + message); //params передаётся через message
super.onTriggerFormEvent(formId, message);
FormControllerManager formControllerManager = FormControllerManager.getInstance(this);
FormController formController = formControllerManager.getController(formId);//получаем контроллер карточки через formId
formController.onTriggerFormEvent(formId, message);//затем вызываем соответствующий контроллер WidgetImpl
}
Наконец, вызовите функцию onTriggerFormEvent() в контроллере WidgetImpl.
public void onTriggerFormEvent(long formId, String message) { HiLog.info(TAG, "onTriggerFormEvent."+message);
// Сначала получаем параметры из message
ZSONObject data = ZSONObject.stringToZSON(message);
String p1 = data.getString("p1");
Integer index = data.getIntValue("index");
ZSONObject zsonObject = new ZSONObject(); // Сохраняем данные для обновления в экземпляре ZSONObject
Integer indexMax = 2; // Если есть N блоков слайдера, устанавливаем N-1
if(p1.equals("right")){ // Если это правая сторона
if(index == indexMax){index = -1;} // Реализуем циклическое перелистывание
index = index+1;
zsonObject.put("index",index);
}else { // Если это левая сторона
if(index == 0){index = indexMax+1;} // Реализуем циклическое перелистывание
index = index-1;
zsonObject.put("index",index);
}
FormBindingData formBindingData = new FormBindingData(zsonObject);
try {
((MainAbility)context).updateForm(formId,formBindingData);
} catch (FormException e) {
e.printStackTrace();
HiLog.info(TAG, "Обновление карточки не удалось");
}
}
5. **Компонент списка может иметь только один onclick, и при его нажатии необходимо также определить, какой элемент списка был выбран.**
```html
<list class="list" else>
<list-item for="{{list}}" class="list-item">
<div class="div">
``` **Текст запроса:**
onclick="sendRouteEvent"> ... ...
Этот фрагмент кода привёл к тому, что я долго мучился с проблемой. В итоге я обнаружил, что в файле index.json можно использовать $item и $idx для получения переменных и индексов элементов HTML-страницы списка. Однако в официальной документации я не нашёл соответствующей информации. После долгих попыток я решил эту проблему.
**Перевод:**
onClick="sendRouteEvent"
Из-за этого фрагмента кода у меня возникли серьёзные проблемы. Наконец, я понял, что могу использовать `$item` и `$idx` в `index.json`, чтобы получить переменные и индексы элементов HTML-списка. Но в официальной документации не было такой информации. Я долго пытался решить эту проблему и наконец справился с ней.
```json
"actions": {
"sendRouteEvent": {
"action": "router",
"bundleName": "com.liangzili.demos",
"abilityName": "com.liangzili.demos.Video",
"params": {
"url": "{{$item.short_url}}",
"index": "{{$idx}}"
}
}
}
Перевод:
{
"actions": {
"sendRouteEvent": {
"action": "router",
"bundleName": "com.liangzili.demos",
"abilityName": "com.liangzili.demos.Video",
"params": {
"url": "$item.short_url",
"index": "$idx"
}
}
}
}
В целом, после того как я разобрался со списком и его событиями клика, я понял, насколько он полезен. Список действительно удобен в использовании.
Примечание: в этом переводе могут быть неточности, так как часть исходного текста осталась непереведённой. preferences,Map<String,String> map)
// Перебираем map
for (Map.Entry<String, String> entry : map.entrySet()) {
HiLog.info(TAG, entry.getKey() + "=" + entry.getValue());
preferences.putString(entry.getKey(), entry.getValue()); // 3. Записываем данные в экземпляр Preferences
}
preferences.flushSync(); // 4. Сохраняем экземпляр Preferences с помощью flush() или flushSync().
/
/**
* Получение значения SESSDATA в Cookie
* @param context Используется для указания пути к файлу данных
* @return Значение SESSDATA в Cookie
*/
public static String getSessData(Context context) {
// 1. Создаём базу данных с использованием вспомогательного класса для операций с базой данных
DatabaseHelper databaseHelper = new DatabaseHelper(context);
// 2. Получаем экземпляр Preferences с соответствующим именем файла
Preferences preferences = databaseHelper.getPreferences("bilibili");
// 3. Читаем данные
String SESSDATA = preferences.getString("SESSDATA", "");
return SESSDATA;
}
/**
* Получение значения Vmid в Cookie
* @param context
* @return Значение Vmid в Cookie
*/
public static String getVmid(Context context){
// 1. Создаём базу данных с использованием вспомогательного класса для операций с базой данных
DatabaseHelper databaseHelper = new DatabaseHelper(context);
// 2. Получаем экземпляр Preferences с соответствующим именем файла
Preferences preferences = databaseHelper.getPreferences("bilibili");
// 3. Читаем данные
String DedeUserID = preferences.getString("DedeUserID", "");
return DedeUserID;
}
Добавление скрытой страницы активности для празднования Дня святого Валентина с использованием распределённой способности HarmonyOS для воспроизведения видеоэффектов.
Например, PlayerSlice — эта страница используется для воспроизведения видео.
При нажатии на изображение аватара на карточке происходит переход на страницу. Код выглядит следующим образом:
src/main/js/fans/pages/index/index.hml
<div class="card_root_layout" else>
<div class="div_left_container">
<stack class="stack-parent" onclick="sendRouterEvent">
<image src="{{src}}" class="image_src"></image>
<image src="{{vip}}" class="image_vip"></image>
</stack>
</div>
<text class="item_title">{{follower}}</text>
</div>
В actions задаётся переход к только что созданной странице воспроизведения.
src/main/js/fans/pages/index/index.json
"actions": {
"sendRouterEvent": {
"action": "router",
"abilityName": "com.liangzili.demos.Player",
"params": true
}
}
Извлекаем параметр params из intent. Если страница воспроизведения была запущена с карточки, получаем значение true. Если она была запущена распределённым способом, получаем false.
params = intent.getStringParam("params");//извлекаем значение поля params из намерения
if(params.equals("true")){
Intent intent0 = new Intent();
Operation op = new Intent.OperationBuilder()
.withDeviceId(DistributedUtils.getDeviceId())//параметр 1. является ли это межприборным, пустым, не межприборным
.withBundleName("com.liangzili.demos")//параметр 2. в config.json bundleName
.withAbilityName("com.liangzili.demos.Player")//параметр 3. способность, которую нужно переключить
.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
.build();
intent0.setOperation(op);
intent0.setParam("params","false");
startAbility(intent0);
videoSource = "resources/base/media/right.mp4";
}else{
videoSource = "resources/base/media/left.mp4";
}
Если params вызывает запуск распределённым способом, необходимо заранее запросить разрешение у приложения.
Разрешение | Описание |
---|---|
ohos.permission.DISTRIBUTED_DATASYNC | Обязательное (разрешение на управление распределёнными данными, позволяющее обмениваться данными между устройствами) |
ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE | Обязательное (разрешает получать информацию об изменении состояния устройств в распределённой сети) |
ohos.permission.GET_DISTRIBUTED_DEVICE_INFO | Обязательное (позволяет получить список и информацию о устройствах в распределённой сети) |
ohos.permission.GET_BUNDLE_INFO | Обязательное (запрашивает информацию о других приложениях) |
Приложение запрашивает у пользователя разрешение на использование распределённых функций при первом запуске.
src/main/java/com/liangzili/demos/MainAbility.java
requestPermissionsFromUser(new ## 5. Получение идентификатора удалённого устройства
Чтобы открыть страницу на удалённом устройстве, сначала необходимо получить идентификатор этого устройства.
```java
public class DistributedUtils {
public static String getDeviceId() {
// Получаем список онлайн-устройств, DeviceInfo.FLAG_GET_ONLINE_DEVICE исключает локальное устройство.
List<DeviceInfo> deviceList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICЕ);
if (deviceList.isEmpty()) {
return null;
}
int deviceNum = deviceList.size();
List<String> deviceIds = new ArrayList<>(deviceNum); // Извлекаем идентификаторы устройств
List<String> deviceNames = new ArrayList<>(deviceNum); // Извлекаем имена устройств
deviceList.forEach((device) -> {
deviceIds.add(device.getDeviceId());
deviceNames.add(device.getDeviceName());
});
String devcieIdStr = deviceIds.get(0);
return devcieIdStr;
}
}
Видео воспроизводится с использованием кода из статьи «Видеоплеер для HarmonyOS» на сайте HarmonyOS.51cto.com.
// Устанавливаем полупрозрачное состояние строки состояния
getWindow().addFlags(WindowManager.LayoutConfig.MARK_TRANSLUCENT_STATUS);
initPlayer();
// Необходимо переписать два обратных вызова: VideoSurfaceCallback и VideoPlayerCallback
private void initPlayer() {
sfProvider=(SurfaceProvider) findComponentById(ResourceTable.Id_surfaceProvider);
// image=(Image) findComponentById(ResourceTable.Id_img);
sfProvider.getSurfaceOps().get().addCallback(new VideoSurfaceCallback());
// sfProvider.pinToZTop(boolean)--если установлено значение true, видеоэлемент управления будет отображаться в самом верхнем слое, но если установлено значение false, хотя он не отображается в самом верхнем слое, появится чёрный экран,
// необходимо добавить строку кода: WindowManager.getInstance().getTopWindow().get().setTransparent(true);
sfProvider.pinToZTop(true);
//WindowManager.getInstance().getTopWindow().get().setTransparent(true);
player=new Player(getContext());
//sfProvider добавляет прослушиватель событий
sfProvider.setClickedListener(new Component.ClickedListener() {
@Override
public void onClick(Component component) {
if(player.isNowPlaying()){
// Если воспроизведение приостановлено, приостановить
player.pause();
// Кнопка воспроизведения видна
image.setVisibility(Component.VISIBLE);
}else {
// Если приостановленное воспроизведение, продолжить воспроизведение
player.play();
// Кнопка воспроизведения скрыта
image.setVisibility(Component.HIDE);
}
}
});
}
private class VideoSurfaceCallback implements SurfaceOps.Callback {
@Override
public void surfaceCreated(SurfaceOps surfaceOps) {
HiLog.info(logLabel,"surfaceCreated() called.");
if (sfProvider.getSurfaceOps().isPresent()) {
Surface surface = sfProvider.getSurfaceOps().get().getSurface();
playLocalFile(surface);
}
}
@Override
public void surfaceChanged(SurfaceOps surfaceOps, int i, int i1, int i2) {
HiLog.info(logLabel,"surfaceChanged() called.");
}
@Override
public void surfaceDestroyed(SurfaceOps surfaceOps) {
HiLog.info(logLabel,"surfaceDestroyed() called.");
}
}
private void playLocalFile(Surface surface) {
try {
RawFileDescriptor filDescriptor = getResourceManager().getRawFileEntry(videoSource).openRawFileDescriptor();
Source source = new Source(filDescriptor.getFileDescriptor(),filDescriptor.getStartPosition(),filDescriptor.getFileSize());
player.setSource(source);
player.setVideoSurface(surface);
player.setPlayerCallback(new VideoPlayerCallback());
player.prepare();
sfProvider.setTop(0);
player.play();
} catch (Exception e) {
HiLog.info(logLabel,"playUrl Exception:" + e.getMessage());
}
}
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )