Android Jetpack: 利用 Palette 進行圖片取色 | 開發者說·DTalk
本文原作者: BennuC, 原文 釋出於: BennuCTech
Palette 即調色盤這個功能其實很早就釋出了, Jetpack 同樣將這個功能也納入其中,想要使用這個功能,需要先依賴庫。
implementation 'androidx.palette:palette:1.0.0'
本篇文章就來講解一下如何使用 Palette 在圖片中提取顏色。
建立 Palette
建立 Palette 其實很簡單,如下:
var builder = Palette.from(bitmap)
var palette = builder.generate()
這樣,我們就通過一個 Bitmap 建立一個 Pallete 物件。
注意: 直接使用 Palette.generate(bitmap) 也可以,但是這個方法已經不推薦使用了,網上很多老文章中依然使用這種方式。建議還是使用 Palette.Builder 這種方式。
generate() 這個函式是同步的,當然考慮圖片處理可能比較耗時, Android 同時提供了非同步函式 。
public AsyncTask<Bitmap, Void, Palette> generate(
@NonNull final PaletteAsyncListener listener) {
通過一個 PaletteAsyncListener 來獲取 Palette 例項,這個介面如下:
public interface PaletteAsyncListener {
/**
* Called when the {@link Palette} has been generated. {@code null} will be passed when an
* error occurred during generation.
*/
void onGenerated(@Nullable Palette palette);
}
提取顏色
有了 Palette 例項,通過 Palette 物件的相應函式就可以獲取圖片中的顏色,而且不只一種顏色,下面一一列舉:
-
getDominantColor: 獲取圖片中的主色調;
-
getMutedColor : 獲取圖片中柔和的顏色;
-
getDarkMutedColor : 獲取圖片中柔和的暗色;
-
getLightMutedColor : 獲取圖片中柔和的亮色;
-
getVibrantColor : 獲取圖片中有活力的顏色;
-
getDarkVibrantColor : 獲取圖片中有活力的暗色;
-
getLightVibrantColor : 獲取圖片中有活力的亮色。
這些函式都需要提供一個預設顏色,如果這個顏色 Swatch 無效則使用這個預設顏色。光這麼說不直觀,我們來測試一下,程式碼如下:
var bitmap = BitmapFactory.decodeResource(resources, R.mipmap.a)
var builder = Palette.from(bitmap)
var palette = builder.generate()
color0.setBackgroundColor(palette.getDominantColor(Color.WHITE))
color1.setBackgroundColor(palette.getMutedColor(Color.WHITE))
color2.setBackgroundColor(palette.getDarkMutedColor(Color.WHITE))
color3.setBackgroundColor(palette.getLightMutedColor(Color.WHITE))
color4.setBackgroundColor(palette.getVibrantColor(Color.WHITE))
color5.setBackgroundColor(palette.getDarkVibrantColor(Color.WHITE))
color6.setBackgroundColor(palette.getLightVibrantColor(Color.WHITE))
執行後結果如下:
這樣各個顏色的差別就一目瞭然。除了上面的函式,還可以使用 getColorForTarget 這個函式,如下:
@ColorInt
public int getColorForTarget(@NonNull final Target target, @ColorInt final int defaultColor) {
這個函式需要一個 Target,提供了 6 個靜態欄位,如下:
/**
* A target which has the characteristics of a vibrant color which is light in luminance.
*/
public static final Target LIGHT_VIBRANT;
/**
* A target which has the characteristics of a vibrant color which is neither light or dark.
*/
public static final Target VIBRANT;
/**
* A target which has the characteristics of a vibrant color which is dark in luminance.
*/
public static final Target DARK_VIBRANT;
/**
* A target which has the characteristics of a muted color which is light in luminance.
*/
public static final Target LIGHT_MUTED;
/**
* A target which has the characteristics of a muted color which is neither light or dark.
*/
public static final Target MUTED;
/**
* A target which has the characteristics of a muted color which is dark in luminance.
*/
public static final Target DARK_MUTED;
其實就是對應著上面除了主色調之外的六種顏色。
文字顏色自動適配
在上面的執行結果中可以看到,每個顏色上面的文字都很清楚的顯示,而且它們並不是同一種顏色。其實這也是 Palette 提供的功能。
通過下面的函式,我們可以得到各種色調所對應的 Swatch 物件 :
-
getDominantSwatch
-
getMutedSwatch
-
getDarkMutedSwatch
-
getLightMutedSwatch
-
getVibrantSwatch
-
getDarkVibrantSwatch
-
getLightVibrantSwatch
注意: 同上面一樣,也可以通過 getSwatchForTarget(@NonNull final Target target) 來獲取 。
Swatch 類提供了以下函式:
-
getPopulation(): 樣本中的畫素數量;
-
getRgb(): 顏色的 RBG 值;
-
getHsl(): 顏色的 HSL 值;
-
getBodyTextColor(): 能都適配這個 Swatch 的主體文字的顏色值;
-
getTitleTextColor(): 能都適配這個 Swatch 的標題文字的顏色值。
所以我們通過 getBodyTextColor() 和 getTitleTextColor() 可以很容易得到在這個顏色上可以很好實 現 的標題和主體文字顏色。所以上面的測試程式碼完整如下:
var bitmap = BitmapFactory.decodeResource(resources, R.mipmap.a)
var builder = Palette.from(bitmap)
var palette = builder.generate()
color0.setBackgroundColor(palette.getDominantColor(Color.WHITE))
color0.setTextColor(palette.dominantSwatch?.bodyTextColor ?: Color.WHITE)
color1.setBackgroundColor(palette.getMutedColor(Color.WHITE))
color1.setTextColor(palette.mutedSwatch?.bodyTextColor ?: Color.WHITE)
color2.setBackgroundColor(palette.getDarkMutedColor(Color.WHITE))
color2.setTextColor(palette.darkMutedSwatch?.bodyTextColor ?: Color.WHITE)
color3.setBackgroundColor(palette.getLightMutedColor(Color.WHITE))
color3.setTextColor(palette.lightMutedSwatch?.bodyTextColor ?: Color.WHITE)
color4.setBackgroundColor(palette.getVibrantColor(Color.WHITE))
color4.setTextColor(palette.vibrantSwatch?.bodyTextColor ?: Color.WHITE)
color5.setBackgroundColor(palette.getDarkVibrantColor(Color.WHITE))
color5.setTextColor(palette.darkVibrantSwatch?.bodyTextColor ?: Color.WHITE)
color6.setBackgroundColor(palette.getLightVibrantColor(Color.WHITE))
color6.setTextColor(palette.lightVibrantSwatch?.bodyTextColor ?: Color.WHITE)
這樣每個顏色上的文字都可以清晰的顯示。
那麼這個標題和主體文字顏色有什麼差別,他們又是如何得到的?我們來看看原始碼:
/**
* Returns an appropriate color to use for any 'title' text which is displayed over this
* {@link Swatch}'s color. This color is guaranteed to have sufficient contrast.
*/
@ColorInt
public int getTitleTextColor() {
ensureTextColorsGenerated();
return mTitleTextColor;
}
/**
* Returns an appropriate color to use for any 'body' text which is displayed over this
* {@link Swatch}'s color. This color is guaranteed to have sufficient contrast.
*/
@ColorInt
public int getBodyTextColor() {
ensureTextColorsGenerated();
return mBodyTextColor;
}
可以看到都會先執行 ensureTextColorsGenerated() ,它的原始碼如下:
private void ensureTextColorsGenerated() {
if (!mGeneratedTextColors) {
// First check white, as most colors will be dark
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) {
// If we found valid light values, use them and return
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) {
// If we found valid dark values, use them and return
mBodyTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkBodyAlpha);
mTitleTextColor = ColorUtils.setAlphaComponent(Color.BLACK, darkTitleAlpha);
mGeneratedTextColors = true;
return;
}
// If we reach here then we can not find title and body values which use the same
// lightness, we need to use mismatched values
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;
}
}
通過程式碼可以看到,這兩種文字顏色實際上要麼是白色要麼是黑色,只是透明度 Alpha 不同。
這裡面有一個關鍵函式,即 ColorUtils.calculateMinimumAlpha() :
public static int calculateMinimumAlpha(@ColorInt int foreground, @ColorInt int background,
float minContrastRatio) {
if (Color.alpha(background) != 255) {
throw new IllegalArgumentException("background can not be translucent: #"
+ Integer.toHexString(background));
}
// First lets check that a fully opaque foreground has sufficient contrast
int testForeground = setAlphaComponent(foreground, 255);
double testRatio = calculateContrast(testForeground, background);
if (testRatio < minContrastRatio) {
// Fully opaque foreground does not have sufficient contrast, return error
return -1;
}
// Binary search to find a value with the minimum value which provides sufficient contrast
int numIterations = 0;
int minAlpha = 0;
int maxAlpha = 255;
while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS &&
(maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) {
final int testAlpha = (minAlpha + maxAlpha) / 2;
testForeground = setAlphaComponent(foreground, testAlpha);
testRatio = calculateContrast(testForeground, background);
if (testRatio < minContrastRatio) {
minAlpha = testAlpha;
} else {
maxAlpha = testAlpha;
}
numIterations++;
}
// Conservatively return the max of the range of possible alphas, which is known to pass.
return maxAlpha;
}
它根據背景色和前景色計算前景色最合適的 Alpha。這期間如果小於 minContrastRatio 則返回 -1,說明這個前景色不合適。而標題和主體文字的差別就是這個 minContrastRatio 不同而已。
回到 e nsureTextColorsGenerated 程式碼可以看到,先根據當前色調,計算出白色前景色的 Alpha,如果兩個 Alpha 都不是 -1,就返回對應顏色;否則計算黑色前景色的 Alpha,如果都不是 -1,返回對應顏色;否則標題和主體文字一個用白色一個用黑色,返回對應顏色即可。
更多功能
上面我們建立 Palette 時先通過 Palette.from(bitmap) 得到了一個 Palette.Builder 物件,通過這個 builder 可以實現更多功能,比如:
-
addFilter: 增加一個過濾器;
-
setRegion: 設定圖片上的提取區域;
-
maximumColorCount: 調色盤的最大顏色數。
等等。
總結
通過上面我們看到,Palette 的功能很強大,但是它使用起來非常簡單,可以讓我們很方便的提取圖片中的顏色,並且適配合適的文字顏色。同時注意因為 ColorUtils 是 public 的,所以當我們需要文字自動適配顏色的情況時,也可以通過 ColorUtils 的幾個函式自己實現計算動態顏色的方案。
長按右側二維碼
檢視更多開發者精彩分享

"開發者說·DTalk" 面向
中國開發者們徵集 Google 移動應用 (apps & games) 相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出眾的中國開發者們提供更好展現自己、充分發揮自己特長的平臺。我們將通過大家的技術內容著重選出優秀案例進行 谷歌開發技術專家 (GDE)
的推薦。
點選屏末 | 閱讀原文 | 即刻報名參與 " 開發者說 · DTalk"
- 京東金融Android瘦身探索與實踐
- 京東金融Android瘦身探索與實踐
- Android效能優化-ListView自適應效能問題
- Blazor在IoT領域的前端實踐 @.NET開發者日
- 升級 Android 目標版本到 31(S) 居然這麼多坑
- 如何精準分析特定使用者的應用效能問題?這兩個功能您一定要了解
- ShareSDK Android端許可權說明
- 適用於Android開發者的多執行緒總結
- ShareSDK Android端許可權說明
- MobPush Android常見問題
- MobPush Android For Unity
- 共碼未來 | 持續賦能開發者和初創生態
- 要近萬元買iPhone 14 Pro才能玩靈動島?Android 開發者:別急,我給你自制了一個 App
- 揭祕 Jetpack Compose 快照系統 | 開發者說·DTalk
- 輕鬆學習,考取證書 | 商品詳情繫列內容第十講
- 是時候讓所有人能一起聊個痛快了!
- 在 Jetpack Compose 中安全地使用資料流
- 共碼未來 | 助力打造現代、高效、流暢的開發體驗
- 共碼未來丨2022 Google 谷歌開發者大會主旨演講亮點回顧
- 京東金融客戶端使用者觸達方式的探索與實踐