侧边栏壁纸
博主头像
分享你我博主等级

行动起来,活在当下

  • 累计撰写 117 篇文章
  • 累计创建 13 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

expo在安卓端设置徽标问题

管理员
2025-03-24 / 0 评论 / 0 点赞 / 12 阅读 / 12466 字

expo在安卓端设置徽标是可以的但是清除、获取当前徽标数均有问题。下面通过注册插件方式加载原生代码解决

import { ConfigPlugin, withAndroidManifest, AndroidConfig } from '@expo/config-plugins';
import { ExpoConfig } from '@expo/config-types';

const { addMetaDataItemToMainApplication, getMainApplicationOrThrow } = AndroidConfig.Manifest;

const withAndroidBadge: ConfigPlugin = (config: ExpoConfig) => {
  return withAndroidManifest(config, async (config) => {
    const mainApplication = getMainApplicationOrThrow(config.modResults);

    // 添加 me.leolin.shortcutbadger
    addMetaDataItemToMainApplication(
      mainApplication,
      'me.leolin.shortcutbadger.impl.xiaomi.XiaomiHomeBadger',
      'me.leolin.shortcutbadger.impl.xiaomi.XiaomiHomeBadger'
    );

    // 添加其他厂商的实现...
    addMetaDataItemToMainApplication(
      mainApplication,
      'me.leolin.shortcutbadger.impl.huawei.HuaweiHomeBadger',
      'me.leolin.shortcutbadger.impl.huawei.HuaweiHomeBadger'
    );

    addMetaDataItemToMainApplication(
      mainApplication,
      'me.leolin.shortcutbadger.impl.oppo.OPPOHomeBader',
      'me.leolin.shortcutbadger.impl.oppo.OPPOHomeBader'
    );

    return config;
  });
};

export default withAndroidBadge; 
import { ConfigPlugin, withDangerousMod, AndroidConfig } from '@expo/config-plugins';
import { ExpoConfig } from '@expo/config-types';
import * as fs from 'fs';
import * as path from 'path';

const createBadgeModuleContents = (packageName: string) => `
package ${packageName};

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import me.leolin.shortcutbadger.ShortcutBadger;
import android.content.SharedPreferences;
import android.content.Context;

public class BadgeModule extends ReactContextBaseJavaModule {
    private static final String BADGE_COUNT_KEY = "badge_count";
    private final SharedPreferences preferences;

    public BadgeModule(ReactApplicationContext reactContext) {
        super(reactContext);
        preferences = reactContext.getSharedPreferences("badge_preferences", Context.MODE_PRIVATE);
    }

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

    @ReactMethod
    public void getBadgeCount(Promise promise) {
        try {
            int count = preferences.getInt(BADGE_COUNT_KEY, 0);
            promise.resolve(count);
        } catch (Exception e) {
            promise.reject("ERROR", e.getMessage());
        }
    }

    @ReactMethod
    public void setBadgeCount(int count, Promise promise) {
        try {
            preferences.edit().putInt(BADGE_COUNT_KEY, count).apply();
            boolean success = ShortcutBadger.applyCount(getReactApplicationContext(), count);
            promise.resolve(success);
        } catch (Exception e) {
            promise.reject("ERROR", e.getMessage());
        }
    }

    @ReactMethod
    public void clearBadge(Promise promise) {
        try {
            preferences.edit().putInt(BADGE_COUNT_KEY, 0).apply();
            boolean success = ShortcutBadger.removeCount(getReactApplicationContext());
            promise.resolve(success);
        } catch (Exception e) {
            promise.reject("ERROR", e.getMessage());
        }
    }
}
`;

const createBadgePackageContents = (packageName: string) => `
package ${packageName};

import com.facebook.react.ReactPackage;
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 BadgePackage 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 BadgeModule(reactContext));
        return modules;
    }
}
`;

const withAndroidBadgeModule: ConfigPlugin = (config: ExpoConfig) => {
  return withDangerousMod(config, [
    'android',
    async (config) => {
      const packageName = config.android?.package || 'club.rubicone.dev';
      const packagePath = packageName.replace(/\./g, '/');
      
      const javaPath = path.join(
        config.modRequest.platformProjectRoot,
        'app',
        'src',
        'main',
        'java',
        packagePath
      );

      await fs.promises.mkdir(javaPath, { recursive: true });

      await fs.promises.writeFile(
        path.join(javaPath, 'BadgeModule.java'),
        createBadgeModuleContents(packageName)
      );

      await fs.promises.writeFile(
        path.join(javaPath, 'BadgePackage.java'),
        createBadgePackageContents(packageName)
      );

      // 修改 MainApplication.kt 添加 BadgePackage
      const mainApplicationPath = path.join(javaPath, 'MainApplication.kt');
      let mainApplicationContent = await fs.promises.readFile(mainApplicationPath, 'utf8');

      if (!mainApplicationContent.includes('BadgePackage')) {
        // 匹配 getPackages 函数
        const packagePattern = /override fun getPackages\(\): List<ReactPackage> \{[\s\S]*?val packages = PackageList\(this\)\.packages[\s\S]*?return packages/;
        
        mainApplicationContent = mainApplicationContent.replace(
          packagePattern,
          (match) => {
            const insertPoint = match.indexOf('return packages');
            return match.slice(0, insertPoint) +
              '            packages.add(BadgePackage())\n            ' +
              match.slice(insertPoint);
          }
        );

        // 添加导入语句
        if (!mainApplicationContent.includes('import ' + packageName + '.BadgePackage')) {
          const lastImportIndex = mainApplicationContent.lastIndexOf('import ');
          const insertIndex = mainApplicationContent.indexOf('\n', lastImportIndex) + 1;
          mainApplicationContent = 
            mainApplicationContent.slice(0, insertIndex) +
            `import ${packageName}.BadgePackage\n` +
            mainApplicationContent.slice(insertIndex);
        }

        await fs.promises.writeFile(mainApplicationPath, mainApplicationContent);
      }

      // 修改 build.gradle 添加依赖
      const buildGradlePath = path.join(config.modRequest.platformProjectRoot, 'app', 'build.gradle');
      let buildGradleContent = await fs.promises.readFile(buildGradlePath, 'utf8');

      if (!buildGradleContent.includes('me.leolin:ShortcutBadger')) {
        buildGradleContent = buildGradleContent.replace(
          /dependencies\s*{/,
          `dependencies {\n    implementation "me.leolin:ShortcutBadger:1.1.22@aar"`
        );

        await fs.promises.writeFile(buildGradlePath, buildGradleContent);
      }

      return config;
    },
  ]);
};

export default withAndroidBadgeModule; 
import { ConfigPlugin, withGradleProperties } from '@expo/config-plugins';

const withAndroidGradleProperties: ConfigPlugin = (config) => {
  return withGradleProperties(config, (config) => {
    config.modResults = config.modResults.filter(
      (item) => !(item.type === 'property') || item.key !== 'shortcutBadgerDependency'
    );

    config.modResults.push({
      type: 'property',
      key: 'shortcutBadgerDependency',
      value: 'me.leolin:ShortcutBadger:1.1.22@aar',
    });

    return config;
  });
};

export default withAndroidGradleProperties; 

然后统一封装一下在IOS使用expo-notifications原有的,在安卓端使用我们自己注册的原生插件
```

// 角标管理
export const BadgeManager = {
    // 设置角标数
    setBadgeCount: async (count: number): Promise<boolean> => {
        try {
            if (Platform.OS === 'android') {
                if (NativeModules.BadgeModule) {
                    await NativeModules.BadgeModule.setBadgeCount(count);
                }
            } else {
                await Notifications.setBadgeCountAsync(count);
            }
            return true;
        } catch (error) {
            Logger.getInstance().error("Failed to set badge count", "角标设置", error);
            return false;
        }
    },

    // 清除角标
    clearBadge: async (): Promise<boolean> => {
        try {
            console.log("Clearing badge count")
            if (Platform.OS === 'android') {
                if (NativeModules.BadgeModule) {
                    await NativeModules.BadgeModule.clearBadge();
                }
            } else {
                await Notifications.setBadgeCountAsync(0);
            }
            return true;
        } catch (error) {
            Logger.getInstance().error("Failed to clear badge count", "角标清除", error);
            return false;
        }
    },

    // 增加角标数
    incrementBadge: async (): Promise<boolean> => {
        try {
            let currentBadge=0;
            if (Platform.OS === 'android') {
                if (NativeModules.BadgeModule) {
                    currentBadge = await NativeModules.BadgeModule.getBadgeCount();
                }
            } else {
                currentBadge = await Notifications.getBadgeCountAsync();
            }
            return await BadgeManager.setBadgeCount(currentBadge + 1);
        } catch (error) {
            Logger.getInstance().error("Failed to increment badge count", "角标增加", error);
            return false;
        }
    }
};

自动加载注册插件目录下插件

import { ExpoConfig, ConfigContext } from 'expo/config';
import { ConfigPlugin } from '@expo/config-plugins';
import ts from 'typescript';
import fs from 'fs';
import path from 'path';

// 添加缓存机制
const pluginCache = new Map<string, ConfigPlugin<any>>();
const compiledFiles = new Set<string>();

// 修改 compilePlugin 函数,添加错误处理
const compilePlugin = (filePath: string): ConfigPlugin<any> => {
  try {
    // 检查缓存
    const cacheKey = `${filePath}:${fs.statSync(filePath).mtimeMs}`;
    if (pluginCache.has(cacheKey)) {
      return pluginCache.get(cacheKey)!;
    }
    const source = fs.readFileSync(filePath, 'utf8');
    const result = ts.transpileModule(source, {
      compilerOptions: {
        module: ts.ModuleKind.CommonJS,
        target: ts.ScriptTarget.ES2016,
        esModuleInterop: true,
      },
    });

    const compiledDir = path.join(process.cwd(), 'compiled-plugins');
    if (!fs.existsSync(compiledDir)) {
      fs.mkdirSync(compiledDir, { recursive: true });
    }

    const fileName = path.basename(filePath, '.ts');
    const tempFile = path.join(compiledDir, `${fileName}.js`);
    
    console.log('Compiling plugin:', fileName);
    fs.writeFileSync(tempFile, result.outputText);
    
    // 记录已编译的文件
    compiledFiles.add(tempFile);

    const plugin = require(tempFile);
    // 存入缓存
    pluginCache.set(cacheKey, plugin.default);
    return plugin.default;
  } catch (error) {
    console.error(`Error loading plugin ${path.basename(filePath)}:`, error);
    throw error;
  }
};

// 添加清理函数
const cleanupCompiledPlugins = () => {
  if (process.env.EAS_BUILD) {
    // 在 EAS 构建过程中不清理文件
    return;
  }
  
  compiledFiles.forEach(file => {
    try {
      if (fs.existsSync(file)) {
        fs.unlinkSync(file);
      }
    } catch (error) {
      console.warn(`Failed to cleanup file ${file}:`, error);
    }
  });
  compiledFiles.clear();
};

// 修改 loadPlugins 函数
const loadPlugins = (): ConfigPlugin<any>[] => {
  const pluginsDir = path.join(process.cwd(), 'plugins');
  const files = fs.readdirSync(pluginsDir);
  
  // 在加载新插件前清理旧的编译文件
  cleanupCompiledPlugins();
  
  const plugins = files
    .filter(file => file.startsWith('with') && file.endsWith('.ts'))
    .map(file => compilePlugin(path.join(pluginsDir, file)));

  // 注册进程退出时的清理函数
  if (!process.env.EAS_BUILD) {
    process.on('exit', cleanupCompiledPlugins);
    process.on('SIGINT', () => {
      cleanupCompiledPlugins();
      process.exit();
    });
  }

  return plugins;
};

// 加载所有插件
const customPlugins = loadPlugins();

// 定义插件类型
type ExpoPlugin = string | [string, any] | [string] | [] | ConfigPlugin<any>;

// 配置类型
type AppConfig = Omit<ExpoConfig, 'plugins'> & {
  plugins: ExpoPlugin[];
};

0

评论区