首页
Preview

React Native:创建iOS和Android主屏幕小部件的终极指南

小部件是一种很棒的工具,它可以让你的主屏幕看起来更有吸引力,并提供快速有用的信息。在本文中,我们将向你展示如何为Android和iOS创建小部件,并将其纳入你的React Native应用程序中。

小部件是如何工作的?

小部件作为应用程序的扩展工作。它本身无法作为独立的应用程序工作。小部件有三种大小(小,中,大)并且可以是静态或可配置的。小部件在交互方面受到限制。它不能滚动,只能轻击。小型小部件只能有一种类型的交互区域,而中型和大型小部件可以有多个轻击交互区域。

为什么要开发小部件?

小部件通常不仅用于向用户提供重要信息并从主屏幕访问其应用程序,而且还用于区分这些应用程序与竞争对手,并保持用户参与度。

使用React Native创建小部件

⚠️ 很遗憾,使用React Native无法创建主屏幕小部件。但是别担心,我们为你提供了解决方案!在本指南中,我们将探讨如何使用本地小部件与你的React Native应用程序进行通信。让我们开始吧!

TL;DR

如果你更喜欢使用样例🍖而不是阅读文档📖,你可以在此存储库中尝试样例。欢迎贡献拉取请求和其他类型的贡献!

🛠️ 设置

  • 创建一个新应用程序
react-native init RNWidget
  1. 添加将创建小部件和应用程序之间桥接的依赖项
yarn add react-native-shared-group-preferences
  1. 为了与本地模块进行通信,请将此代码添加到App.js中
import React, {useState} from 'react';
import {
  View,
  TextInput,
  StyleSheet,
  NativeModules,
  SafeAreaView,
  Text,
  Image,
  ScrollView,
  KeyboardAvoidingView,
  Platform,
  ToastAndroid,
} from 'react-native';
import SharedGroupPreferences from 'react-native-shared-group-preferences';
import AwesomeButton from 'react-native-really-awesome-button';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';

const group = 'group.streak';

const SharedStorage = NativeModules.SharedStorage;

const App = () => {
  const [text, setText] = useState('');
  const widgetData = {
    text,
  };

  const handleSubmit = async () => {
    try {
      // iOS
      await SharedGroupPreferences.setItem('widgetKey', widgetData, group);
    } catch (error) {
      console.log({error});
    }
    const value = `${text} days`;
    // Android
    SharedStorage.set(JSON.stringify({text: value}));
    ToastAndroid.show('Change value successfully!', ToastAndroid.SHORT);
  };

  return (
    <SafeAreaView style={styles.safeAreaContainer}>
      <KeyboardAwareScrollView
        enableOnAndroid
        extraScrollHeight={100}
        keyboardShouldPersistTaps="handled">
        <View style={styles.container}>
          <Text style={styles.heading}>Change Widget Value</Text>
          <View style={styles.bodyContainer}>
            <View style={styles.instructionContainer}>
              <View style={styles.thoughtContainer}>
                <Text style={styles.thoughtTitle}>
                  Enter the value that you want to display on your home widget
                </Text>
              </View>
              <View style={styles.thoughtPointer}></View>
              <Image
                source={require('./assets/bea.png')}
                style={styles.avatarImg}
              />
            </View>

            <TextInput
              style={styles.input}
              onChangeText={newText => setText(newText)}
              value={text}
              keyboardType="decimal-pad"
              placeholder="Enter the text to display..."
            />

            <AwesomeButton
              backgroundColor={'#33b8f6'}
              height={50}
              width={'100%'}
              backgroundDarker={'#eeefef'}
              backgroundShadow={'#f1f1f0'}
              style={styles.actionButton}
              onPress={handleSubmit}>
              Submit
            </AwesomeButton>
          </View>
        </View>
      </KeyboardAwareScrollView>
    </SafeAreaView>
  );
};

export default App;

const styles = StyleSheet.create({
  safeAreaContainer: {
    flex: 1,
    width: '100%',
    backgroundColor: '#fafaf3',
  },
  container: {
    flex: 1,
    width: '100%',
    padding: 12,
  },
  heading: {
    fontSize: 24,
    color: '#979995',
    textAlign: 'center',
  },
  input: {
    width: '100%',
    // fontSize: 20,
    minHeight: 50,
    borderWidth: 1,
    borderColor: '#c6c6c6',
    borderRadius: 8,
    padding: 12,
  },
  bodyContainer: {
    flex: 1,
    margin: 18,
  },
  instructionContainer: {
    margin: 25,
    paddingHorizontal: 20,
    paddingTop: 30,
    borderWidth: 1,
    borderRadius: 12,
    backgroundColor: '#ecedeb',
    borderColor: '#bebfbd',
    marginBottom: 35,
  },
  avatarImg: {
    height: 180,
    width: 180,
    resizeMode: 'contain',
    alignSelf: 'flex-end',
  },
  thoughtContainer: {
    minHeight: 50,
    borderRadius: 12,
    borderWidth: 1,
    padding: 12,
    backgroundColor: '#ffffff',
    borderColor: '#c6c6c6',
  },
  thoughtPointer: {
    width: 0,
    height: 0,
    borderStyle: 'solid',
    overflow: 'hidden',
    borderTopWidth: 12,
    borderRightWidth: 10,
    borderBottomWidth: 0,
    borderLeftWidth: 10,
    borderTopColor: 'blue',
    borderRightColor: 'transparent',
    borderBottomColor: 'transparent',
    borderLeftColor: 'transparent',
    marginTop: -1,
    marginLeft: '50%',
  },
  thoughtTitle: {
    fontSize: 14,
  },
  actionButton: {
    marginTop: 40,
  },
});

让我解释一下如何在应用程序中使用SharedGroupPreferencesSharedStorageSharedGroupPreferences从库中导入,你可以通过使用keyvaluegroup存储项目的setItem方法来使用它。对于此示例,密钥将是widgetKey,值将是widgetData,它是一个包含用户输入的JavaScript对象,并且该组将是应用程序和小部件之间共享信息的组的名称。当我们到达Swift代码时,我们将更详细地讨论这一点。

现在,对于Android,我们将使用SharedStorage。你不需要安装任何其他库,因为它已包含在React Native包中。该值将是一个序列化的JavaScript对象,该对象将被转换为字符串并使用设置的SharedStorage方法保存。很容易,对吧?

对于本机代码。让我们从iOS开始。

🍏 iOS实现

  • 在Xcode中打开你的应用程序项目,然后选择File > New > Target。

  1. 从应用程序扩展组中选择小部件扩展名,然后单击下一步。

  1. 输入你的扩展名。

  1. 如果小部件提供用户可配置属性,请选中包含配置意图复选框。

  1. 单击完成。

  2. 如果要求激活方案,请单击激活。

  1. 你的小部件已准备就绪!现在你有一个包含所有必要文件的新文件夹,用于你的小部件。

  1. 在我们进入下一步之前,让我们测试一下你的基本小部件。要执行此操作,只需打开终端并使用以下命令运行应用程序
react-native run-ios
  1. 要添加小部件,你必须轻击主屏幕并保持按下,直到左上角出现一个➕。单击该图标后,将显示应用程序列表。我们新创建的应用程序应该已包含在内。

  1. 要与React Native小部件进行通信,我们必须添加“App Group”

  1. 现在,对于我们的Streak小部件,使用以下代码编辑StreakWidget.swift
import WidgetKit
import SwiftUI
import Intents

struct WidgetData: Decodable {
   var text: String
}

struct Provider: IntentTimelineProvider {
   func placeholder(in context: Context) -> SimpleEntry {
      SimpleEntry(date: Date(), configuration: ConfigurationIntent(), text: "Placeholder")
  }
  
  func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
      let entry = SimpleEntry(date: Date(), configuration: configuration, text: "Data goes here")
      completion(entry)
  }
  
   func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
      let userDefaults = UserDefaults.init(suiteName: "group.streak")
      if userDefaults != nil {
        let entryDate = Date()
        if let savedData = userDefaults!.value(forKey: "widgetKey") as? String {
            let decoder = JSONDecoder()
            let data = savedData.data(using: .utf8)
            if let parsedData = try? decoder.decode(WidgetData.self, from: data!) {
                let nextRefresh = Calendar.current.date(byAdding: .minute, value: 5, to: entryDate)!
                let entry = SimpleEntry(date: nextRefresh, configuration: configuration, text: parsedData.text)
                let timeline = Timeline(entries: [entry], policy: .atEnd)
                completion(timeline)
            } else {
                print("Could not parse data")
            }
        } else {
            let nextRefresh = Calendar.current.date(byAdding: .minute, value: 5, to: entryDate)!
            let entry = SimpleEntry(date: nextRefresh, configuration: configuration, text: "No data set")
            let timeline = Timeline(entries: [entry], policy: .atEnd)
            completion(timeline)
        }
      }
  }
}

struct SimpleEntry: TimelineEntry {
   let date: Date
      let configuration: ConfigurationIntent
      let text: String
}

struct StreakWidgetEntryView : View {
  var entry: Provider.Entry
  
  var body: some View {
    HStack {
      VStack(alignment: .leading, spacing: 0) {
        HStack(alignment: .center) {
          Image("streak")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 37, height: 37)
          Text(entry.text)
            .foregroundColor(Color(red: 1.00, green: 0.59, blue: 0.00))
            .font(Font.system(size: 21, weight: .bold, design: .rounded))
            .padding(.leading, -8.0)
        }
        .padding(.top, 10.0)
        .frame(maxWidth: .infinity)
        Text("Way to go!")
          .foregroundColor(Color(red: 0.69, green: 0.69, blue: 0.69))
          .font(Font.system(size: 14))
          .frame(maxWidth: .infinity)
        Image("duo")
          .renderingMode(.original)
          .resizable()
          .aspectRatio(contentMode: .fit)
          .frame(maxWidth: .infinity)
        
      }
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

@main
struct StreakWidget: Widget {
  let kind: String = "StreakWidget"
  
  var body: some WidgetConfiguration {
    IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
      StreakWidgetEntryView(entry: entry)
    }
    .configurationDisplayName("My Widget")
    .description("This is an example widget.")
  }
}

struct StreakWidget_Previews: PreviewProvider {
  static var previews: some View {
    StreakWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), text: "Widget preview"))
      .previewContext(WidgetPreviewContext(family: .systemSmall))
  }
}

基本上:

  • 从我们之前创建的共享组中读取UserDefaults对象
let userDefaults = UserDefaults.init(suiteName: "group.streak")
  • 获取已编码为字符串的数据
let savedData = userDefaults!.value(forKey: "widgetKey")
  • 解码为对象
let parsedData = try? decoder.decode(WidgetData.self, from: data!)
  • 创建一个时间线
let nextRefresh = Calendar.current.date(byAdding: .minute, value: 5, to: entryDate)!

请注意,添加到时间线结构的对象必须符合TimelineEntry协议,这意味着它们需要有一个日期字段,除此之外不能有其他字段。这是需要记住的重要信息。

iOS就是这样。只需运行npm start,然后在虚拟设备或实际设备上测试应用程序。

安装应用程序后,你只需要从小部件列表中选择小部件,然后将其放置在主屏幕上。

接下来,打开应用程序,在输入字段中输入内容,按回车键,然后返回主屏幕。

iOS就是这样。现在,让我们看看如何在Android上实现相同的功能。

🤖 Android实现

  • 在Android Studio中打开Android文件夹。然后,在Android Studio中,右键单击res > New > Widget > App Widget:

  1. 命名和配置你的小部件,然后单击完成:

  1. 现在运行应用程序,你可以看到小部件可用。

  2. 为了在小部件和React Native应用程序之间通信,我们将使用SharedPreferences Android本机模块,它就像iOS的UserDefaults一样。

这涉及将新的SharedStorage.javaSharedStoragePackager.java文件添加到与MainApplication.java相同的目录中。

SharedStorage.java

package com.rnwidget;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;

public class SharedStorage extends ReactContextBaseJavaModule {
 ReactApplicationContext context;

 public SharedStorage(ReactApplicationContext reactContext) {
  super(reactContext);
  context = reactContext;
 }

 @Override
 public String getName() {
  return "SharedStorage";
 }

 @ReactMethod
 public void set(String message) {
  SharedPreferences.Editor editor = context.getSharedPreferences("DATA", Context.MODE_PRIVATE).edit();
  editor.putString("appData", message);
  editor.commit();

  Intent intent = new Intent(getCurrentActivity().getApplicationContext(), StreakWidget.class);
  intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
  int[] ids = AppWidgetManager.getInstance(getCurrentActivity().getApplicationContext()).getAppWidgetIds(new ComponentName(getCurrentActivity().getApplicationContext(), StreakWidget.class));
  intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
  getCurrentActivity().getApplicationContext().sendBroadcast(intent);

 }
}

SharedStoragePackager.java

package com.rnwidget;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SharedStoragePackager implements ReactPackage {

 @Override
 public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
  return Collections.emptyList();
 }

 @Override
 public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
  List<NativeModule> modules = new ArrayList<>();

  modules.add(new SharedStorage(reactContext));

  return modules;
 }

}
  1. 更改包名称,如AndroidManifest.xml文件中所示,位于android > app > src > main。

  1. 在getPackages方法中,添加以下代码到MainApplication.java文件中。
packages.add(new SharedStoragePackager());
  1. 现在,让我们讨论如何在StreakWidget.java中接收数据。为了更新小部件的内容,我们需要使用SharedPreferences并使用updateAppWidget方法进行管理。这里是用来替换现有方法的代码:
package com.rnwidget;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.widget.RemoteViews;
import android.content.SharedPreferences;

import org.json.JSONException;
import org.json.JSONObject;

/**
 * Implementation of App Widget functionality.
 */
public class StreakWidget extends AppWidgetProvider {

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        try {
            SharedPreferences sharedPref = context.getSharedPreferences("DATA", Context.MODE_PRIVATE);
            String appString = sharedPref.getString("appData", "{\"text\":'no data'}");
            JSONObject appData = new JSONObject(appString);
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.streak_widget);
            views.setTextViewText(R.id.appwidget_text, appData.getString("text"));
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }catch (JSONException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onEnabled(Context context) {
        // Enter relevant functionality for when the first widget is created
    }

    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }
}
  1. 现在,让我们谈谈小部件的外观。这一步是可选的,但我们将使用与iOS示例相同的设计。在Android Studio中,导航到你的应用程序 > res > layout > streak_widget.xml文件。你可以像这样查看设计预览:

  1. 就是这样!在Android设备上进行测试。

结论

干得好!通过学习如何使用React Native创建AppWidget,你已经为你的工具箱增加了一项宝贵的技能。即使这个主题对你来说是新的,也不要担心,它在应用程序中非常简单易用。继续努力!

请为此鼓掌👏🏻,并与你的朋友分享!

请留下你的反馈意见。

如果你想支持我作为一名作家,请考虑在Medium上关注我,并在LinkedIn上联系我。

译自:https://levelup.gitconnected.com/react-native-ultimate-guide-to-create-a-home-screen-widget-for-ios-and-android-83708dc1844c

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
一个人玩
先找到想要的,然后出发

评论(0)

添加评论