前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >轻听变色之谜

轻听变色之谜

原创
作者头像
QQ音乐技术团队
修改于 2017-10-30 01:46:15
修改于 2017-10-30 01:46:15
1.8K00
代码可运行
举报
运行总次数:0
代码可运行

轻听是一款小而美的Android本地音乐播放器,而它的特点之一就是拥有多彩的外衣,如下:

其中,左边6张是白天模式下的几种不同主题色的样式,右边是夜间模式。

那么轻听是如何实现变色的呢?

主要是结合以下两种方式:

  • 自定义Style和Theme
  • 动态配置主题色

自定义Style和Theme

Style和Theme主要用来实现白天模式和夜间模式。

一个Style是一系列属性的集合,用来指定View或者Window的外观和格式。它可以指定的属性包括高度, Padding, 文字颜色,文字尺寸,背景颜色等等。

Style是在Xml资源文件中定义的,比如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<style name="ListItemTitleStyle" parent="TextAppearance.AppCompat.Body1">
    <item name="android:singleLine">true</item>
    <item name="android:ellipsize">end</item>
    <item name="android:textColor">?android:attr/textColorPrimary</item>
</style>

在布局文件中是这样使用的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<TextView
    android:id="@+id/text_item_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    style="@style/ListItemTitleStyle" />

这样一个Style就可以运用在多个地方,既可以统一样式,又可以减少代码量。

而Theme,其实就是一个Style,不同于我们上面提到对单一View的应用,Theme是应用于整个Activity或Application的。各位Android开发同学一定不陌生,在Manifest的Activity声明中就会经常看到。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<activity android:theme="@style/AppLightTheme.NoActionBar"/>

这样,AppLightTheme.NoActionBar中的所有属性都会应用在整个Activity中。

轻听这里,实现夜间模式分三步:

  • 自定义Style
  • 应用Style中的属性
  • 设置Theme

自定义Style

我们这里,就是写两个Style ,然后各自有一套对应的颜色值。

简单介绍一下几个主要的颜色值:

  • colorPrimary: 主题色
  • colorAccent: 辅助色(或强调色)
  • textColorPrimary: 主要的文字颜色,一般TextView的文字都是这个颜色
  • textColorSecondary: 辅助的文字颜色,一般比textColorPrimary的颜色弱一点,用于一些弱化的表示
  • windowBackground: Window的背景色

我们在资源文件中写对应的两套Style:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<style name="AppLightTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorAccent">@color/colorAccent</item>
    <item name="android:textColorPrimary">@color/colorPrimaryTextBlack</item>
    <item name="android:textColorSecondary">@color/colorSubTextBlack</item>
    <item name="android:windowBackground">@color/white</item>
</style>

<style name="AppDarkTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
    <item name="colorPrimary">@color/darkColorPrimary</item>
    <item name="colorAccent">@color/darkColorAccent</item>
    <item name="android:textColorPrimary">@color/white</item>
    <item name="android:textColorSecondary">@color/colorSubTextWhite</item>
    <item name="android:windowBackground">@color/dark_bg</item>
</style>

细心的同学会发现,Style里的属性,有的前面会以“android:”开头,如android:textColorPrimary,有的则没有,如colorPrimary。 以“android:”开头的属性,是系统的属性。而另一种属于自定义的属性,在资源文件中声明如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<attr name="minibar_background" format="color" />

format包括boolean, color, dimension, enum, flag, float, fraction, integer, reference, string。 在此就不赘述了,这是它们的一个相当灵活的使用方式。

应用Style中的属性

比如,colorAccent是在design包中定义的,属于自定义属性,在使用的时候,直接“?attr/”+属性名就可以了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<ImageView
    android:tint="?attr/colorAccent" />

系统属性要多加一个”android:”, 是”?android:attr/“+属性名。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<TextView
    android:textColor="?android:attr/textColorPrimary"/>

这样,当指定了Theme之后,就会去相应的Style下面取对应的颜色值,从而呈现出不同的色彩。

设置Theme

在Manifest中设置是常见的方式。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<activity android:theme="@style/AppLightTheme.NoActionBar"/>

不过为了实现模式的切换,我们是在Activity的onCreate中进行的设置

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected void initTheme() {
    if (MusicPreferences.getInstance().isNightMode()) {
        setTheme(R.style.AppDarkTheme_NoActionBar);
    } else {
        setTheme(R.style.AppLightTheme_NoActionBar);
    }
}

这样,通过给Activity设置不同的Theme,页面就能呈现出不同的颜色搭配。

问题

Style和Theme的方式实现简单,非常适用于模式较为固定的场景,如白天模式和夜间模式。

但是,轻听在白天的模式的时候的基础之上还有几种不同的主题色。

简单聊一下主题色。

主题色,即colorPrimary,是根据品牌形象,为App定义的一个主色调,一般应用于AppBar。

同时,有一个强调色,即colorAccent,是用在Checkbox或下划线等需要给人以提示作用的地方,起辅助的作用。

还有一个颜色是colorPrimaryDark,就是比colorPrimary稍微深一些,主要用在状态栏。

比如上图中,“蓝色”就是主题色,“红”色就是强调色。

主题色和强调色的色值可以不一样,也可以一样。在一般的设计中都是不同的。在轻听的设计中,为了突出品牌色,将强调色跟主题色统一设计成了一个颜色,所以你会看到,到处都是“绿”色。

在强调色跟主题色统一的情况下,6个主题色,6套Style,似乎还可以接受。

但是,万一以后设计同学良心发现了呢?6在6套主题色的基础之上再出6套强调色,那可就是36个Style。如果以后的调色方式再更为灵活,如:

我数学不好,谁帮我算算,别忘了加上强调色还得再平方一下……

如果给每一个主题色都写一套Style,工作量会很大,而且不灵活。这个时候Style就玩不转了。

我们需要一种更为灵活的方案。

动态配置主题色

动态配置主题色是借鉴了github开源控件app-theme-engine。在gradle中引入方式是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
compile('com.github.naman14:app-theme-engine:0.5.1@aar') {
    transitive = true
}

由于找不到这个项目维护的地址,所以我们自己进行了扩展和优化。

  • 颜色配置
  • 颜色处理器
  • 遍历逻辑控制器

颜色配置

颜色配置主要负责存储颜色值。

因为这里存储数据较小,而且简单,所以用SharedPreference来存储,稍加封装就可以。

颜色处理器

颜色处理器Processor主要负责对每一个View的各种颜色进行设置。

首先,定义一个接口

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public interface Processor<T extends View, E> {
    void process(@NonNull Context context, @Nullable String key, @Nullable T target, @Nullable E extra);
}

process方法就是来处理视图颜色的。

其中target就是要传入的视图,这里使用泛型,在各个派生的Processor中具体实现。

以下是几个主要的Processor。

其中DefaultProcessor是默认Processor,可以处理绝大部分的变色情况。其他几种,如他们的名字一样,会额外再处理他们特定的情况。

Processor的处理方式分三步:

  • 给View设置tag
  • 解析View的tag
  • 根据具体的tag进行颜色的设置

以DefaultProcessor为例。有一个TextView,我们要使他的文字颜色为强调色。

首先,给View设置一个tag,”text_accent_color”。如果有多个tag,以逗号分隔。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<TextView
    android:tag="text_accent_color"
    />

然后在process方法中将tag解析出来

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void process(@NonNull Context context, @Nullable String key, @Nullable View view, @Nullable Void extra) {
    if(view != null && view.getTag() != null && view.getTag() instanceof String) {
        String tag = (String)view.getTag();
        if(tag.contains(",")) {
            String[] splitTags = tag.split(",");
            int len = splitTags.length;

            for(int i = 0; i < len; ++i) {
                String part = splitTags[i];
                processTagPart(context, view, part, key);
            }
        } else {
            processTagPart(context, view, tag, key);
        }

    }
}

这里会根据分隔符(逗号)来对tag的数量进行解析,然后依次根据每个tag依次处理。

在处理方法processTagPart中,会找到”text_accent_color”相对应的处理逻辑

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if (view instanceof  TextView) {
    ((TextView) view).setTextColor(Config.accentColor(context, key));
}

Config.accentColor(context, key)的作用就是从颜色配置模块中读取当前的强调色。

其他一些稍微复杂一点的情况,则可以使用相对应的Processor去进行特殊的处理。

例如,ViewPagerProcessor。ViewPager在滑动边界的时候会有一个边界反馈的效果,如下图:

这里需要特殊处理一下。ViewPager中,负责两个边缘效果的是EdgeEffectCompat。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private EdgeEffectCompat mLeftEdge;
private EdgeEffectCompat mRightEdge;

EdgeEffectCompat是一个对系统版本做兼容性处理的类,里面有真正的边缘效果模块EdgeEffect

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public final class EdgeEffectCompat {
    private Object mEdgeEffect;
}

注意到,这两处都是私有的,所以我们必须通过两次反射来获取EdgeEffect,然后更改颜色。

首先,通过反射获取ViewPager的左右EdgeEffectCompat。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static void setEdgeGlowColor(@NonNull ViewPager viewPager, @ColorInt int color) {
    if(Build.VERSION.SDK_INT >= 21) {
        try {
            Field edgeLeft = ViewPager.class.getDeclaredField("mLeftEdge");
            edgeLeft.setAccessible(true);
            Field edgeRight = ViewPager.class.getDeclaredField("mRightEdge");
            edgeRight.setAccessible(true);
            EdgeEffectCompat ee = (EdgeEffectCompat)edgeLeft.get(viewPager);
            if (ee != null) {
                setEdgeGlowColor(ee, color);
            }
            ee = (EdgeEffectCompat)edgeRight.get(viewPager);
            if (ee != null) {
                setEdgeGlowColor(ee, color);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后再获取真正的EdgeEffect,并更改颜色。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private static void setEdgeGlowColor(@NonNull EdgeEffectCompat edgeEffect, @ColorInt int color) throws Exception {
    if(Build.VERSION.SDK_INT >= 21) {
        Field field = EdgeEffectCompat.class.getDeclaredField("mEdgeEffect");
        field.setAccessible(true);
        EdgeEffect effect = (EdgeEffect) field.get(edgeEffect);
        if (effect != null) {
            effect.setColor(color);
        }
    }
}

这样边缘效果的颜色就修改好啦。

遍历逻辑

遍历逻辑控制器主要负责对整个页面的所有View进行遍历,并进行颜色处理。

以下是遍历逻辑:

1.初始化Processor

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private static void initProcessors() {
    mProcessors = new HashMap();
    mProcessors.put("[default]", new DefaultProcessor());
    mProcessors.put(ScrollView.class.getName(), new MusicScrollViewProcessor());
    mProcessors.put(ListView.class.getName(), new MusicListViewProcessor());
    mProcessors.put(RecyclerView.class.getName(), new MusicRecyclerViewProcessor());
    mProcessors.put(Toolbar.class.getName(), new MusicToolbarProcessor());
    mProcessors.put(NavigationView.class.getName(), new MusicNavigationViewProcessor());
    mProcessors.put(TabLayout.class.getName(), new MusicTabLayoutProcessor());
    mProcessors.put(ViewPager.class.getName(), new MusicViewPagerProcessor());
}

将各Processor实例化后存入HashMap,key为类名。

2.开始刷新的时机是onStart,因为这个时候布局已经基本初始化完毕。我们会判断Activity之前是否start过,避免重复的进行处理。至于在此之后生成的布局,会单独对其进行一次刷新。

3.从流程图中可以看出,在处理ContentView之前,我们会单独处理几个特殊的布局。

StatusBar是顶部状态栏,NavigationBar是底部导航栏,有时我们会希望让这两处也兼容主题色。

如果用到ActionBar,也需要处理一下。不过MD的实现中,一般都是NoActionBar的,而用我们自己布局的ToolBar来代替。

在有侧边栏的页面中,根布局一般都是DrawerLayout,在侧边栏滑出的时候,可以设置DrawerLayout的状态栏颜色。

4.找我们自己的根布局:ContentView

ContentView就是我们用setContentView设置的布局,它上面还有ContentParent,DecorView,Window。

直接根据资源id找?不现实,因为每个Activity的ContentView资源id基本都不一样的。

这里采取一种迂回的方式,先找到ContentView的父布局ContentParent。

我们看setContentView的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
public void setContentView(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mOriginalWindowCallback.onContentChanged();
}

可以看到,实际上ContentParent的资源id是固定的:android.R.id.content。

系统会先把ContentParent的子视图清除,然后通过LayoutInflater的inflate方法将我们指定布局的视图解析出来并添加到ContentParent中。

那么我们就可以根据android.R.id.content先找到ContentParent,进而找到ContentView

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ViewGroup contentView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);

5.获取Processor

根据View的类名获取Processor

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Processor processor = mProcessors.get(viewClass.getName());
if(processor != null) {
    return processor;
} else {
    Class current = viewClass;

    do {
        current = current.getSuperclass();
        if(current == null) {
            break;
        }

        processor = mProcessors.get(current.getName());
    } while(processor == null);

    if (processor == null) {
        mProcessors.get("[default]")
    }

    return processor;
}

从HashMap中获取对应的Processor,如果找不到则根据父类的名字查找。找到之后就可以调用process方法进行处理。

6.遍历

这里会从ContentView开始进行深度优先遍历,处理所有的视图。

有一些特殊的ViewGroup不需要遍历其子布局,例如TabLayout,因为其自己的方法已经满足绝大部分的情况。

结语

以上,就是两种变色方案的具体实现。

  • 自定义Style和Theme实现简单,整洁,适用于模式较为固定的场景。
  • 动态配置主题色实现起来略微复杂,但是比较灵活,适用于主题色较多的场景。

将这两种方案结合,就实现了轻听的变色。

大家轻拍,如果有好的方法或者建议,可以多多讨论交流优化~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
c++DLL编程详解
DLL(Dynamic Link Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。 静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。静态链接库和动态链接库的另外一个区别在于静态链接库
拾点阳光
2018/05/10
2.3K0
lib文件和dll文件的区别_dll2lib
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/167993.html原文链接:https://javaforall.cn
全栈程序员站长
2022/09/20
2.9K0
使用 `#pragma comment(lib, "xxx.lib")` 简化 DLL 依赖管理
在 Windows 平台上的 C/C++ 开发中,动态链接库(DLL)是实现代码复用和模块化的核心工具。然而,使用 DLL 时通常需要手动配置链接器以引入对应的导入库(.lib 文件),这不仅繁琐,还容易出错。本文将详细介绍一种利用 #pragma comment(lib, "xxx.lib") 预处理指令来简化 DLL 依赖管理的方法,阐明它解决的问题、具体用法以及使用时的限制。这篇博客将以专业且易懂的方式编写,适合开发者和技术爱好者阅读。
码事漫谈
2025/03/04
1010
使用 `#pragma comment(lib, "xxx.lib")` 简化 DLL 依赖管理
vs报错“错误 LNK2019 无法解析的外部符号”的几种原因及解决方案[通俗易懂]
大家好,又见面了,我是你们的朋友全栈君。   运行vs程序的时候,报错严重性 代码 说明 项目 文件 行 禁止显示状态 错误 LNK2019 无法解析的外部符号 "__declspec(dlli
全栈程序员站长
2022/09/07
22.6K0
vs报错“错误 LNK2019 无法解析的外部符号”的几种原因及解决方案[通俗易懂]
一分钟详解VS中快速生成dll和lib方法
问题:如果我们在Visual Studio工程中,想要快速学习如何生成dll和lib,有什么小技巧呢?
3D视觉工坊
2020/12/11
2.7K0
一分钟详解VS中快速生成dll和lib方法
VS2010编写动态链接库DLL和单元测试,转让DLL测试的正确性
本文将创建一个简单的动态库-link,谱写控制台应用程序使用该动态链接库,该动态链接库为“JAVA调用动态链接库DLL之JNative学习”中使用的DLL,仅仅是项目及文件名不同。
全栈程序员站长
2022/07/05
1.3K0
VS2010编写动态链接库DLL和单元测试,转让DLL测试的正确性
C++ DLL 工程创建与使用
DLL,是 Dynamic Link Library的缩写,中文名 动态链接库。DLL是一个包含可由多个程序,同时使用的代码和数据的库。 本文简介DLL 概念,记录 DLL 工程创建与使用方法。 简介 动态链接库( Dynamic-link library,缩写为 DLL) 是微软公司在windows 系统中实现共享函数库概念的一种实现方式。所谓动态链接,就是把常用的公共函数封装到 DLL 文件中,当程序需要用到这些函数时,系统才会动态地将 DLL 加载到内存中使用。 调用方式主要分为两种:
为为为什么
2023/01/30
2K0
C++ DLL 工程创建与使用
exe调用DLL的方式
编写dll时,有个重要的问题需要解决,那就是函数重命名——Name-Mangling。解决方式有两种,一种是直接在代码里解决采用extent”c”、_declspec(dllexport)、#pragma comment(linker, "/export:[Exports Name]=[Mangling Name]"),另一种是采用def文件。
CN_Simo
2020/08/20
2.7K0
无法解析外部符号
本人在写qt工程的时候遇到无法解析外部符号 原因:只写了类声明,但还没有写实现类,造成调用时无法解析。 解决方法,把还没有实现类的声明给注释掉。
全栈程序员站长
2022/09/07
2.7K0
无法解析外部符号
C++之Error无法解析的外部符号[通俗易懂]
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/153486.html原文链接:https://javaforall.cn
全栈程序员站长
2022/09/13
3K0
关于各种无法解析的外部符号问题的相应解决方案
在使用vs2008调试程序的过程中,经常会出现无法解析的外部符号问题,可能的原因有很多种,下面这些是我一年来积累的经验. 仅供参考.
全栈程序员站长
2022/09/13
9160
visual studio静态,动态链接库开发工具简单使用
这里我不会使用visual studio的图形界面工具,作为专业人士,还是搞懂自己的工具是怎么运转的,这样比较好。
byronhe
2021/06/25
1.1K0
c#封装动态库_nginx调用so动态库
一直对动态库的封装理解不是很透彻,虽然之前写过一个Demo,不过并没有真正的理解。所以写下来,帮助自己理解下。
全栈程序员站长
2022/11/14
2.8K0
c#封装动态库_nginx调用so动态库
Win32编程之静态库编写与使用.动态链接库的编写与使用
  静态库其实就是解决模块开发的一种解决方案.在以前.我们写代码的时候.每个人都可以独立写一个项目.但是现在不行了.一个项目往往要很多人一起去编写.而其中用到的技术就类似于静态库.
IBinary
2022/05/10
7630
Win32编程之静态库编写与使用.动态链接库的编写与使用
Windows Api学习笔记-动态连接库(DLL)的使用
#include <windows.h> #include <iostream> #include "12dll.h" using namespace std; #pragma comment(lib,"12Dll")//要链接到什么库文件 void main() { //CMy12Dll a; cout<<fnMy12Dll()<<endl; char b; cin>>b; } VS2008 新建WIN32项目 选择动态连接库 应用程序类型为:WINDOWS 应用程序 附加选项为:导出符号 d
liulun
2022/05/09
6230
VC++的DLL应用(含Demo演示)
      在大学大一的时候学的是C,然后后来大二的时候专业又开了C++这个课程,然后再后来自己又自学了一点VC++,大三的时候也试着编写过一个MFC的最简单的窗口程序。到大四的时候,自己又做了一个GIS的项目,是用C#.NET来编写的,然后发现C#上手好容易,而且还大部分语法规则都沿用了C,C++的习惯,于是觉得C++实在是没有一点优势可言啊。但这个暑假的实习经历又改变了我的观点:C++在写窗口程序虽然麻烦,但是却什么能做,而且对比C#来说,对运行环境的要求不高,不用像C#程序在安装之前还要安装100M多的运行.NET环境。C++和C#各有优缺,目前我对它们俩的定位是:C++用来写一些底层的程序,比如驱动,或者是一些算法类型的函数接口,然后用C#来调用这些接口并进行界面设计。如何函数的实现跨语言呢?显然DLL是个很重要的内容,故在此对VC++的DLL模块进行介绍。
用户1170933
2022/05/10
9730
VC++的DLL应用(含Demo演示)
C++基础 静态库与动态库
如果在程序中使用静态链接库,那么链接器在链接的过程中会将.obj文件和.lib文件组织成可执行exe文件,也就是将.lib中的代码链接到可执行文件中,因此生成的exe文件比较大。 程序运行时,将全部数据加载到内存。如果程序体积较大,功能较为复杂,那么加载到内存中的时间就会比较长,最直接的一个例子就是双击打开一个软件,要很久才能看到界面。这是静态链接库的一个弊端。 但程序在发行时不需要提供库文件。
xxpcb
2020/08/04
1.4K0
c++动态库和静态库的区别_静态库里面包含动态库
C++静态库与动态库
全栈程序员站长
2022/11/11
1.9K0
VC++DLL动态链接库程序
最近查找了一下VC++中关于编写DLL动态库的资料,主要是导出函数和导出类的编写。因为在实际项目开发中有时需要使用C++编写好DLL接口,控制设备,提供给其他语言如Nodejs、C#等使用。
ccf19881030
2019/07/10
1.4K0
关于opentelemetry-cpp社区对于C++ Head Only组件单例和符号可见性的讨论小记
前段时间有人在 opentelemetry-cpp 提出了api组件在动态库中单例无法工作的 issue ,( https://github.com/open-telemetry/opentelemetry-cpp/issues/1520 ) 。
owent
2023/03/06
1.1K0
推荐阅读
相关推荐
c++DLL编程详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文