A helper class to extract prominent colors from an image.
顾名思义,调色盘,从类注释中,我们可以轻松得知这是一个可以帮助我们从一张图片中提取突出颜色的类。
色彩亮度,又称指色彩的明度。色彩明度是色彩的三要素(色相、明度和纯度)之一。不同颜色会有明暗的差异,相同颜色也有明暗深浅的变化。
// 通过资源获取图片的 bitmap 形式
val bitmapRes = BitmapFactory.decodeResource(resources,it.characterInfo.imgResId)
// 解析图片
Palette.from(bitmapRes).generate { paletteOut ->
paletteOut?.let { paletteIn ->
val headerPicture = findViewById<CircleImageView>(R.id.civ_header_picture)
headerPicture.setImageResource(it.characterInfo.imgResId)
val nameTV = findViewById<TextView>(R.id.tv_name)
nameTV.text = it.characterInfo.name
val describeTV = findViewById<TextView>(R.id.tv_describe)
describeTV.text = it.characterInfo.desc
// 获取目标集为 VIBRANT、LIGHT_MUTED 和 DARK_MUTED 的颜色样板
// 获取样板中的主色 rgb,标题色 titleTextColor 和文本色 bodyTextColor
paletteIn.vibrantSwatch?.rgb?.let { rgb ->
val bgView = findViewById<View>(R.id.view_bg_header_picture)
bgView.background = ColorDrawable(rgb)
}
paletteIn.lightMutedSwatch?.titleTextColor?.let { textColor ->
nameTV.setTextColor(textColor)
}
paletteIn.darkMutedSwatch?.bodyTextColor?.let { textColor ->
describeTV.setTextColor(textColor)
}
}
}
// 通过资源获取图片的 bitmap 形式
val bitmap = BitmapFactory.decodeResource(resources,it.characterInfo.imgResId)
Palette.from(bitmap).generate { paletteOut ->
paletteOut?.let { paletteIn ->
// 获取目标集为 VIBRANT 的颜色样板
// 获取样板中的主色 rgb 和标题色 titleTextColor
paletteIn.vibrantSwatch?.rgb?.let { rgb ->
it.nameTV.background = ColorDrawable(rgb)
}
paletteIn.vibrantSwatch?.titleTextColor?.let { textColor ->
it.nameTV.setTextColor(textColor)
}
}
}
// 详情页顶部和首页顶部一样,这里不再赘述
// 下方列表列举了同一张图片下分析不同目标色集合得到的各种样板色
// 背景颜色为样板色的主色
// 文本颜色为样板色的标题颜色
val bitmapRes = BitmapFactory.decodeResource(resources,characterInfo.imgResId)
Palette.from(bitmapRes).generate { paletteOut ->
paletteOut?.let { paletteIn ->
headerPictureIV.setImageResource(characterInfo.imgResId)
nameTV.text = characterInfo.name
describeTV.text = characterInfo.desc
paletteIn.vibrantSwatch?.rgb?.let { rgb ->
val bgView = findViewById<View>(R.id.view_bg_header_picture)
bgView.background = ColorDrawable(rgb)
}
paletteIn.lightMutedSwatch?.titleTextColor?.let { textColor ->
nameTV.setTextColor(textColor)
}
paletteIn.darkMutedSwatch?.bodyTextColor?.let { textColor ->
describeTV.setTextColor(textColor)
}
paletteIn.vibrantSwatch?.rgb?.let { bgColor ->
vibrantTV.setBackgroundColor(bgColor)
}
paletteIn.vibrantSwatch?.titleTextColor?.let { textColor ->
vibrantTV.setTextColor(textColor)
}
paletteIn.lightVibrantSwatch?.rgb?.let { bgColor ->
vibrantLightTV.setBackgroundColor(bgColor)
}
paletteIn.lightVibrantSwatch?.titleTextColor?.let { textColor ->
vibrantLightTV.setTextColor(textColor)
}
paletteIn.darkVibrantSwatch?.rgb?.let { bgColor ->
vibrantDarkTV.setBackgroundColor(bgColor)
}
paletteIn.darkVibrantSwatch?.titleTextColor?.let { textColor ->
vibrantDarkTV.setTextColor(textColor)
}
paletteIn.mutedSwatch?.rgb?.let { bgColor ->
mutedTV.setBackgroundColor(bgColor)
}
paletteIn.mutedSwatch?.titleTextColor?.let { textColor ->
mutedTV.setTextColor(textColor)
}
paletteIn.lightMutedSwatch?.rgb?.let { bgColor ->
mutedLightTV.setBackgroundColor(bgColor)
}
paletteIn.lightMutedSwatch?.titleTextColor?.let { textColor ->
mutedLightTV.setTextColor(textColor)
}
paletteIn.darkMutedSwatch?.rgb?.let { bgColor ->
mutedDarkTV.setBackgroundColor(bgColor)
}
paletteIn.darkMutedSwatch?.titleTextColor?.let { textColor ->
mutedDarkTV.setTextColor(textColor)
}
}
}
GitHub 仓库地址:https://github.com/NicholasHzf/PaletteLearningDemo
前置知识:
HSL 是一种将 RGB 色彩模型中的点在圆柱坐标系中的表示法。这两种表示法试图做到比基于笛卡尔坐标系的几何结构 RGB 更加直观。是运用最广的颜色系统之一。
入口函数:Palette.from(xxx).generate{// …}
A.) public static Builder from(@NonNull Bitmap bitmap)
这个 from 方法比较好理解,就是使用建造者模式,通过图片的 bitmap 来构建用于产生调色板实例的建造者。
@NonNull
public static Builder from(@NonNull Bitmap bitmap) {
return new Builder(bitmap);
}
B.) 这个方法就是简单的 new 了一个建造者出来。那么 Builder 的构造函数里面又发生了什么呢?我们继续往下看。
public Builder(@NonNull Bitmap bitmap) {
// ... 异常判断
// 添加默认的过滤器
mFilters.add(DEFAULT_FILTER);
// 设置图片的 bitmap
mBitmap = bitmap;
// 重置样板色集合
mSwatches = null;
// 添加系统默认的六种目标色
mTargets.add(Target.LIGHT_VIBRANT);
mTargets.add(Target.VIBRANT);
mTargets.add(Target.DARK_VIBRANT);
mTargets.add(Target.LIGHT_MUTED);
mTargets.add(Target.MUTED);
mTargets.add(Target.DARK_MUTED);
}
A.) 产生一个异步任务并使用特定参数执行它
@NonNull
public AsyncTask<Bitmap, Void, Palette> generate(@NonNull final PaletteAsyncListener listener) {
// ... 异常处理
// 产生一个异步任务并使用特定参数执行它
return new AsyncTask<Bitmap, Void, Palette>() {
@Override
@Nullable
protected Palette doInBackground(Bitmap... params) {
try {
// 真正的生成调色板的函数
return generate();
} catch (Exception e) {
Log.e(LOG_TAG, "Exception thrown during async generate", e);
return null;
}
}
@Override
protected void onPostExecute(@Nullable Palette colorExtractor) {
// 执行后的回调
listener.onGenerated(colorExtractor);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mBitmap);
}
B.) 真正的生成调色板的函数 generate()
@NonNull
public Palette generate() {
// ...
List<Swatch> swatches;
if (mBitmap != null) {
// 量化位图以减少颜色数量
// 首先缩小位图
final Bitmap bitmap = scaleBitmapDown(mBitmap);
// ...
final Rect region = mRegion;
// 如果我们有一个缩放过的位图和选中的区域,那么我们要使得这两个部分相匹配
if (bitmap != mBitmap && region != null) {
// 计算缩放比例
final double scale = bitmap.getWidth() / (double) mBitmap.getWidth();
// 匹配区域
region.left = (int) Math.floor(region.left * scale);
region.top = (int) Math.floor(region.top * scale);
region.right = Math.min((int) Math.ceil(region.right * scale),
bitmap.getWidth());
region.bottom = Math.min((int) Math.ceil(region.bottom * scale),
bitmap.getHeight());
}
// 从位图中生成一个量化器
final ColorCutQuantizer quantizer = new ColorCutQuantizer(getPixelsFromBitmap(bitmap),mMaxColors,mFilters.isEmpty() ? null : mFilters.toArray(new Filter[mFilters.size()]));
// 如果上述过程中产生了新的位图,那么就回收它
if (bitmap != mBitmap) {
bitmap.recycle();
}
// 从生成的量化器获取量化后的样板色集合
swatches = quantizer.getQuantizedColors();
// ...
} else if (mSwatches != null) {
// 没有特定的 bitmap 但是有给定的样板色集合,那么我们直接使用它们
swatches = mSwatches;
} else {
// ... 抛出异常
}
// 使用样板色集合和目标色集合创建调色板实例
final Palette p = new Palette(swatches, mTargets);
// 调色版创建
p.generate();
// ...
return p;
}
C.) swatches = quantizer.getQuantizedColors()
从生成的量化器获取量化后的样板色集合,这里是直接返回结果,那么我们就要看这个结果是哪里得到的
List<Palette.Swatch> getQuantizedColors() {
return mQuantizedColors;
}
D.) 查看 mQuantizedColors 的调用关系可以知道,是在量化器的构造函数中生成的,所以此刻需要看一下这个构造函数 ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters)
具体的量化过程和算法,我们不用太抠细节,感兴趣的小伙伴可以自行研究学习,这里不再详细解读,贴一下 ColorCutQuantizer 类的描述,可以看出是基于优化过的中位切割算法对颜色集合进行减色。
An color quantizer based on the Median-cut algorithm, but optimized for picking out distinct colors rather than representation colors. The color space is represented as a 3-dimensional cube with each dimension being an RGB component. The cube is then repeatedly divided until we have reduced the color space to the requested number of colors. An average color is then generated from each cube. What makes this different to median-cut is that median-cut divided cubes so that all of the cubes have roughly the same population, where this quantizer divides boxes based on their color volume. This means that the color space is divided into distinct colors, rather than representative colors.
重点关注一下我们的目标变量 mQuantizedColors
ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
// ...
if (distinctColorCount <= maxColors) {
// 本身颜色就少的情况下,直接使用就行,不需要减少
mQuantizedColors = new ArrayList<>();
for (int color : colors) {
mQuantizedColors.add(new Palette.Swatch(approximateToRgb888(color), hist[color]));
}
// ...
} else {
// 颜色过多的时候就需要使用量化来减少颜色的数量
mQuantizedColors = quantizePixels(maxColors);
// ...
}
}
private List<Palette.Swatch> quantizePixels(int maxColors) {
// ... 算法相关内容,分割 box
// 最后返回 boxes 的平均的颜色值
return generateAverageColors(pq);
}
private List<Palette.Swatch> generateAverageColors(Collection<Vbox> vboxes) {
ArrayList<Palette.Swatch> colors = new ArrayList<>(vboxes.size());
// 循环 boxes 获取平均颜色
for (Vbox vbox : vboxes) {
Palette.Swatch swatch = vbox.getAverageColor();
// ...
}
return colors;
}
final Palette.Swatch getAverageColor() {
// ...
return new Palette.Swatch(approximateToRgb888(redMean, greenMean, blueMean), totalPopulation);
}
E.) 那么我们的样板色的主色和标题色以及文本色是否是在构造样板色的时候就确定好的呢?看一下样板色的构造函数就清楚了!
public Swatch(@ColorInt int color, int population) {
mRed = Color.red(color);
mGreen = Color.green(color);
mBlue = Color.blue(color);
// 可以看的出来,主色在构造后就产生了
mRgb = color;
mPopulation = population;
}
主色确实在样板色对象构造后就确定了,那么标题色和文本色呢?我们再关注一下表示标题色和文本色的变量 mTitleTextColor 和 mBodyTextColor
@ColorInt
public int getTitleTextColor() {
ensureTextColorsGenerated();
return mTitleTextColor;
}
@ColorInt
public int getBodyTextColor() {
ensureTextColorsGenerated();
return mBodyTextColor;
}
可以看出,在获取这两种颜色的时候有一个确保目标颜色生成的函数,应该就是这个函数保证了标题色和文本色的生成。
private void ensureTextColorsGenerated() {
// 如果产生了就不再重复产生
if (!mGeneratedTextColors) {
// 首先检查白色系,因为大多数颜色都是深色系的,那么使用白色系的文字的情况会更多
// 计算可应用于前景的最小 alpha 值,以便与背景相比时至少具有 minContrastRatio 的对比度值
// Color.WHITE 前景色 mRgb 背景色 MIN_CONTRAST_BODY_TEXT 最小对比度
final int lightBodyAlpha = ColorUtils.calculateMinimumAlpha(
Color.WHITE, mRgb, MIN_CONTRAST_BODY_TEXT);
final int lightTitleAlpha = ColorUtils.calculateMinimumAlpha(
Color.WHITE, mRgb, MIN_CONTRAST_TITLE_TEXT);
// 如果白色系符合要求,则添加透明度,更新标题色和文本色并结束函数流程
if (lightBodyAlpha != -1 && lightTitleAlpha != -1) {
mBodyTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha);
mTitleTextColor = ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha);
mGeneratedTextColors = true;
return;
}
// 如果白色系不符合要求,则检查深色系
final int darkBodyAlpha = ColorUtils.calculateMinimumAlpha(
Color.BLACK, mRgb, MIN_CONTRAST_BODY_TEXT);
final int darkTitleAlpha = ColorUtils.calculateMinimumAlpha(
Color.BLACK, mRgb, MIN_CONTRAST_TITLE_TEXT);
// 如果深色系符合要求,则添加透明度,更新标题色和文本色并结束函数流程
if (darkBodyAlpha != -1 && darkTitleAlpha != -1) {
mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
mGeneratedTextColors = true;
return;
}
// 如果都不符合要求,则使用不匹配的值,有异常崩溃的可能
mBodyTextColor = lightBodyAlpha != -1
? ColorUtils.setAlphaComponent(Color.WHITE, lightBodyAlpha)
: ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
mTitleTextColor = lightTitleAlpha != -1
? ColorUtils.setAlphaComponent(Color.WHITE, lightTitleAlpha)
: ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
mGeneratedTextColors = true;
}
}
F.) final Palette p = new Palette(swatches, mTargets)
有了目标色集合和样板色集合,那是时候创建我们的调色板了!
Palette(List<Swatch> swatches, List<Target> targets) {
mSwatches = swatches;
mTargets = targets;
mUsedColors = new SparseBooleanArray();
mSelectedSwatches = new ArrayMap<>();
mDominantSwatch = findDominantSwatch();
}
构造函数看起来没什么稀奇,就是把样板色集合和目标色集合赋值了一下,那么我们给定目标色时,系统是怎么确定样板色的呢,我们继续看一下调色板的 generate 方法,毕竟从执行时机(创建调色版实例后立刻调用)和函数名称(产生/生成)来看,这是最有可能的地方
G.) p.generate()
确定目标色的样板色
void generate() {
// 生成对应的分数,再用键值对的形式把目标色和样板色一一对应存储起来
for (int i = 0, count = mTargets.size(); i < count; i++) {
final Target target = mTargets.get(i);
target.normalizeWeights();
mSelectedSwatches.put(target, generateScoredTarget(target));
}
// 清除“用过的”颜色集合,现在已经没有用了,它的作用是用于选择正确的样板色
mUsedColors.clear();
}
获取根据目标色获取的最高分数的样板色。“用过的”颜色列表,目前看来还不知道有什么用,继续看下去
@Nullable
private Swatch generateScoredTarget(final Target target) {
// 获取根据目标色获取的最高分数的样板色
final Swatch maxScoreSwatch = getMaxScoredSwatchForTarget(target);
// 如果样板色有效并且是目标色独有的,那么我们就把这个样板色的主色加入“用过的”颜色列表
if (maxScoreSwatch != null && target.isExclusive()) {
mUsedColors.append(maxScoreSwatch.getRgb(), true);
}
return maxScoreSwatch;
}
遍历样板色并计算相关得分,并从中获取得分最高的样板色,这里有两个重要函数,一个函数是用于判断是否需要计算得分,另一个是用于计算得分的
shouldBeScoredForTarget(swatch, target)
判断是否需要计算得分generateScore(swatch, target)
计算得分@Nullable
private Swatch getMaxScoredSwatchForTarget(final Target target) {
float maxScore = 0;
Swatch maxScoreSwatch = null;
// 遍历样板色并计算相关得分
for (int i = 0, count = mSwatches.size(); i < count; i++) {
final Swatch swatch = mSwatches.get(i);
if (shouldBeScoredForTarget(swatch, target)) {
final float score = generateScore(swatch, target);
if (maxScoreSwatch == null || score > maxScore) {
maxScoreSwatch = swatch;
maxScore = score;
}
}
}
return maxScoreSwatch;
}
判断是否需要计算得分
private boolean shouldBeScoredForTarget(final Swatch swatch, final Target target) {
// 检查 hsl 值是否在正确的范围内,以及这个颜色是否已经被使用了
final float hsl[] = swatch.getHsl();
return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation()
&& hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness()
&& !mUsedColors.get(swatch.getRgb());
}
终于到了最后一步,计算目标色对应的样板色的分数!
private float generateScore(Swatch swatch, Target target) {
final float[] hsl = swatch.getHsl();
// 这里有三个分数,分别是饱和度分数,亮度分数,受欢迎程度的分数
float saturationScore = 0;
float luminanceScore = 0;
float populationScore = 0;
// 获取样板色集合中最大的受欢迎程度的分数,下面会用它来计算百分比
final int maxPopulation = mDominantSwatch != null ? mDominantSwatch.getPopulation() : 1;
// 如果目标色的饱和度占比大于 0
// 则计算分数
if (target.getSaturationWeight() > 0) {
saturationScore = target.getSaturationWeight()
* (1f - Math.abs(hsl[1] - target.getTargetSaturation()));
}
// 如果目标色的亮度占比大于 0
// 则计算分数
if (target.getLightnessWeight() > 0) {
luminanceScore = target.getLightnessWeight()
* (1f - Math.abs(hsl[2] - target.getTargetLightness()));
}
// 如果目标色的受欢迎程度分数占比大于 0
// 则计算分数
if (target.getPopulationWeight() > 0) {
populationScore = target.getPopulationWeight()
* (swatch.getPopulation() / (float) maxPopulation);
}
// 三种分数相加即为最终得分
return saturationScore + luminanceScore + populationScore;
}
上述代码中的 “受欢迎程度分数” 最早可以追溯到 量化减色获取样板色集合的阶段,那个时候就确定了各个样板色的“受欢迎程度分数”,然后在调色板构造函数执行时,findDominantSwatch()
函数则确定了样板色集合中“受欢迎程度分数”最高的样板色。
至此,我们将输入位图到获得调色盘的整个流程走了一遍,下面梳理一下简要过程:
源码学习,路漫漫其修远兮,保持本心,好好学习!如有错误,烦请指正!感谢阅读!