實用型ColorPicker的設計與實現

一、前言

ColorPicker,顏色選取器,簡稱拾色器。
說到拾色器,大家可能就會想到Photoshop, 使用得最多的應該是設計, 對於開發而言,平常要用到拾色器的機會不多。
如果有一天,項目中需要一個拾色器(多用於自定義顏色),該如何入手?
今天且來給大家分享一下拾色器的設計和實現。

二、顏色空間

要實現實用的拾色器,瞭解下顏色空間是必要的。
顏色空間也稱彩色模型(又稱彩色空間或彩色系統),它的用途是在某些標準下用通常可接受的方式對彩色加以說明。
關於顏色空間,文末附有一些鏈接,都是前人的精華總結。
尤其是《色彩空間中的 HSL、HSV、HSB 有什麼區別》中的討論(知乎大神的作答),讀完立即醍醐灌頂,茅塞頓開。

本節主要摘錄各大神的一些表述,並加以整理。
爲了閱讀體驗,就不逐一附上作者了,具體見文後鏈接。

2.1 RGB空間

RGB是從顏色發光的原理來設計定的,通俗點說它的顏色混合方式就好像有紅、綠、藍三盞燈,當它們的光相互疊合的時候,色彩相混,而亮度卻等於兩者亮度之總和,越混合亮度越高,即加法混合。
紅、綠、藍三個顏色通道每種色各分爲256階亮度,在0時“燈”最弱——是關掉的,而在255時“燈”最亮。
當三色灰度數值相同時,產生不同灰度值的灰色調,即三色灰度都爲0時,是最暗的黑色調;三色灰度都爲255時,是最亮的白色調。

2.2 HSB空間

RGB 是對機器很友好的色彩模式,但並不夠人性化,因爲我們對色彩的認識往往是”什麼顏色?鮮豔不鮮豔?亮還是暗?”。
例如,我們平時描述顏色,“深紫”,“淺綠”,“明黃”,"暗紅”等,一是交代基礎顏色,二是對顏色本身加以描述。
HSB(HSV) 基於 RGB ,是一個更人性化的表示方法。
H(Hue) 爲色相, 取值範圍:0-360°,基礎顏色。
S(Saturation) 爲飽和度, 取值範圍:0 - 1(0% - 100%), 表示色彩的純度。
B(Brightness)爲明度, 取值範圍:0 - 1(0% - 100%),表示對光量的感知。
明度在某些地方也叫Value,所以就有了HSV,HSV和HSB是一樣的,只是關於明度的叫法不一樣而已。

  • 色相環的每一種顏色,在RGB空間中,最多隻有兩個顏色通道(r, g, b)大於0,所以色相環的顏色是最純淨的。
  • 明度決定了RBG三個分量的大小,也就是決定了光亮的大小,在感知層面,就是明暗的區別。
  • 飽和度爲0的顏色,r,g,b相等, 當明度爲0時爲黑色,明度爲1時爲白色,大於0小於1時爲灰色;
    飽和度爲1時,顏色值僅取決於色相和明度,而明度只控制RGB分量的大小,所以顏色還是純淨的;
    飽和度在0到1之間時,爲飽和度爲0的顏色(黑灰白)和飽和度爲1的顏色的線性插值,越靠近1顏色越純淨。

2.3 明度

先看 Photoshop 的 HSB 顏色模型拾色器,如下圖所示,HSB 的 B(明度)控制純色中混入黑色的量,越往上,值越大,黑色越少,顏色明度越高。

2.4 飽和度

如下圖所示,HSB 的 S(飽和度)控制純色中混入白色的量,越往右,值越大,白色越少,顏色純度越高。


2.5 色相

色相指的是色彩的外相,是在不同波長的光照射下,人眼所感覺不同的顏色,如紅色、黃色、藍色等。
在HSL和HSV色彩空間中,H指的就是色相,是以紅色爲0°(360°);黃色爲60°;綠色爲120°;青色爲180°;藍色爲240°;品紅色爲300°。

從上圖可以看出,從0°到360°,是一個分段函數,其中,每一段都有一個顏色分量是0,一個分量是1,另一個分量或從0到1,或從1到0。

2.6 HSB轉RGB

對用戶而言,HSB空間更容易調節,但是對於計算機,用RGB空間渲染會更加方便。
以下是HSB到RGB的轉換公式:

圖中, h, s , v分別是色相,飽和度,明度。
有了這個組公式,我們就可以理解2.5節的色相條是怎麼來的了。
首先看到第一個公式,[]是取整符號,即 hi = (int) (h/60)% 6 。
令s=1, v=1, 則p=0, q=1-f, t=f。
當i=0(第一段),(r, g, b) = (v, t, p) = (1, f, 0), 而f=h/60-hi =h/60,
也就是,第一段中,r=1, g=h/60, b=0;
特例:當h=60, r=1,g=1,b=0, 混合出黃色。
以此類推。
另外我們還注意到,當h選定之後,顏色和s,v成線性關係,這一點對後面拾色器的實現很重要。

三、拾色器的設計

3.1 條形拾色器

需要用到自定義顏色的APP不多,網易APP是其中一個。
點擊“個性換膚->自選顏色”,會彈出這樣的界面:

頁面中間是預覽,底部是預定義的調色板;
調色板的最後,是一張多彩的圖片,點擊後切換成兩個顏色條,如下圖:

第一個顏色條很眼熟了,就是前面提到的色相;
底下這條,應該是以選取的色相爲基礎,飽和度爲1,明度從0到1漸變。
這兩個顏色條組合起來,顏色的取值範圍爲2.2節中的圓柱的側面。
取值範圍雖然只佔顏色空間的一部分,但是也是很有價值的一部分。
可能網易的設計師只想讓用戶選取鮮明的顏色,所以捨棄了飽和度的調節,同時換來了極大的簡潔性。

3.2 環形拾色器

開源的Android拾色器有不少,其中HoloColorPicker是star比較多的一個項目。

該項目把色相做成一個環,底下輔以飽和度和明度的調節,可以說是一個完整的拾色器了(可以選取整個顏色空間的顏色)。
把色相做成色相環,看起來比色相條要更加炫酷一些,但是佔用面積變大了。
像網易雲音樂的顏色選取,因爲要給預覽圖足夠的空間,所以只能用空間佔用少的拾色器;
像這種一下佔用大半個屏幕的設計,不適合網易雲音樂這種選色場景。

3.3 PS拾色器

Photoshop的常規拾色器是矩形構成的“飽和度-明度”選取面板,以及色相條(然後也可以通過設置換成色相輪)。

Photoshop作爲專業的圖像編輯軟件,拾色器無疑是很強大的。
同時電腦顯示器的面積畢竟比手機要大很多,鼠標的選取精確度也比手指觸摸屏幕要精確,
所以Photoshop的拾色器可以大開大合,提供各種面板,顯示全面的參數。

四、技術實現

通過前面三個小節的分析,我們可以看出,拾色器的實現要點爲:
以HSB顏色空間爲基礎,通過條形,環形,矩形等座標來調節HSB各分量的值,達成顏色的選取。

首先第一個要解決的問題就是,顏色空間的轉換計算。
幸運的是,SDK 的 Color 類提供HSB(HSV)和RGB之間的轉換方法:

public static void colorToHSV(@ColorInt int color, @Size(3) float hsv[]) 

public static int HSVToColor(@Size(3) float hsv[])

然後要解決的第二個問題是,座標的繪製。
同樣,SDK也提供了各種Shader, 使得我們可以輕鬆的繪製各種座標。
接下來我們結合實例看一下。

4.1 仿網易雲音樂

也不賣關子了,先上效果圖吧:

和網易雲音樂一樣,都是在下方顯示面板,有預置的調色板,調色板最後的方塊可以跳轉去自定義暗色;
不同之處在於,比之多了透明度和純度的調節,如此,可以選擇整個顏色空間,以及alpha通道。

以上實現的關鍵點在於,色相、飽和度(爲了字體長度相同,以“純度”作爲title),以及明度的繪製。
前面我們提到,色相是分段線性變化的,因此,我們可以利用 LinearGradient 來繪製。

public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int colors[],
            @Nullable float positions[], @NonNull TileMode tile) 

給LinearGradient的color數組福祉,只需列出色相在分段交界的顏色即可:

private static final int[] COLORS = new int[] {
        0xFFFF0000,
        0xFFFFFF00,
        0xFF00FF00,
        0xFF00FFFF,
        0xFF0000FF,
        0xFFFF00FF,
        0xFFFF0000,
};

前面2.6節提到,h確定之後, 顏色和s,v成線性關係,因此,也可以通過LinearGradient來繪製飽和度和明度。
例如,繪製飽和度時,只需計算hsv第二分量(hsv[1], 也就是s)等於0和等於1時的顏色值,作爲LinearGradient的colors的參數,即可繪製在當前h, v值對應的s的變化(也就是飽和度對應的顏色條)。

    hsv[1] = 0f;
    colors[0] = Color.HSVToColor(hsv);
    hsv[1] = 1f;
    colors[1] = Color.HSVToColor(hsv);

明度的繪製以此類推。

4.2 仿Photoshop

色相在0°和360°對應的都是紅色,首位相接,所以很多時候會被做成色相環(Photoshop中叫色相輪)。
飽和度和明度,如果合成一個二維座標,會更加直觀,這樣也是Photoshop的方案。

要繪製色相環,需要用到另一個Shader:

 public SweepGradient(float cx, float cy,
            @NonNull @ColorInt int colors[], @Nullable float positions[])

用法很簡單,把上一節給出的 COLORS 代入 SweepGradient 的 colors 即可。

而要合成飽和度和明度,可以用ComposeShader:

private Shader getSVShader() {
    if (mValShader == null) {
        mValShader = new LinearGradient(
                mSVRect.left, mSVRect.top,
                mSVRect.left, mSVRect.bottom,
                Color.WHITE, Color.BLACK, Shader.TileMode.CLAMP);
    }

    if (mShaderHSV[0] != mHSV[0] || mComposeShader == null) {
        mShaderHSV[0] = mHSV[0];
        Shader satShader = new LinearGradient(
                mSVRect.left, mSVRect.top,
                mSVRect.right, mSVRect.top,
                Color.WHITE, Color.HSVToColor(mShaderHSV), Shader.TileMode.CLAMP);
        mComposeShader = new ComposeShader(mValShader, satShader, PorterDuff.Mode.MULTIPLY);
    }

    return mComposeShader;
}

ComposeShader可以組合兩個Shader, 因爲顏色和s, v是線性關係,所以需要組合兩個LinearGradient。
第一個LinearGradient從左上角到左下角,從白到黑;
第二個LinearGradient從左上角到右上角,從白到色相的顏色。
效果如下:

既然拾色部分已經佔了這麼多空間了,所以乾脆把剩下的空間也用上,來做數據面板;
還加一個編輯框,可以手動輸入RGB顏色,同時限制編輯框只能輸入十六進制,限制輸入長度。

4.3 拾色器的選取

兩類拾色器中,通過條形座標來選取顏色比較節約空間,通過環形和矩形則相對直觀。
具體使用哪一種,要視情況而定:
如果要實時預覽效果,那拾色器就不能佔太多空間,這時候第一種方案會比較適合;
如果要製作調色板之類的,用第二種方案就比較高效。
不過,在移動設備上,第二種方案會打折扣,比如在sv面板中選取顏色時,手指會遮擋住指示器,
而在PC上的話,由於是用鼠標選取,所以可以精確選取,也沒有遮擋的問題。

五、總結

本文先是講述了顏色空間的相關知識,
然後介紹了業界的一些拾色器的設計,
最後演示了兩種拾色器,以及實現要點。

限於篇幅,在實現方面沒有講的很細,讀者可以具體看項目代碼。
代碼鏈接:
https://github.com/No89757/ColorPicker

參考資料:
色彩空間中的 HSL、HSV、HSB 有什麼區別
從 RGB 到 HSV 的轉換詳細介紹
RGB與HSB之間的轉換公式
維基百科 - 色相

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章