├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── mateuszkaflowski │ │ └── mediastylepalette │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── mateuszkaflowski │ │ │ └── mediastylepalette │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-xhdpi │ │ ├── b.png │ │ ├── bookmark.png │ │ ├── c.png │ │ ├── cover.jpg │ │ ├── d.png │ │ ├── e.png │ │ ├── english.jpg │ │ ├── f.png │ │ ├── g.png │ │ ├── h.png │ │ ├── hammerzeit.jpg │ │ ├── i.png │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── minus.png │ │ ├── more.png │ │ ├── nagrales.jpg │ │ ├── next.png │ │ ├── pause.png │ │ ├── pause_notification.png │ │ ├── pause_pressed.png │ │ ├── play.png │ │ ├── play_notification.png │ │ ├── play_pressed.png │ │ ├── plus.png │ │ ├── previous.png │ │ ├── retrospektywa.jpg │ │ ├── tim.jpeg │ │ └── wandzel.jpg │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── example │ └── mateuszkaflowski │ └── mediastylepalette │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images └── demo.gif ├── library ├── .gitignore ├── build.gradle ├── library.iml ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── mkaflowski │ │ └── mediastylepalette │ │ ├── ImageUtils.java │ │ ├── MediaNotificationProcessor.java │ │ └── NotificationColorUtil.java │ └── res │ └── values │ └── strings.xml ├── local.properties └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # IntelliJ 36 | *.iml 37 | .idea/workspace.xml 38 | .idea/tasks.xml 39 | .idea/gradle.xml 40 | .idea/assetWizardSettings.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | .idea/caches 44 | 45 | # Keystore files 46 | # Uncomment the following line if you do not want to check your keystore files in. 47 | #*.jks 48 | 49 | # External native build folder generated in Android Studio 2.2 and later 50 | .externalNativeBuild 51 | 52 | # Google Services (e.g. APIs or Firebase) 53 | google-services.json 54 | 55 | # Freeline 56 | freeline.py 57 | freeline/ 58 | freeline_project_description.json 59 | 60 | # fastlane 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots 64 | fastlane/test_output 65 | fastlane/readme.md 66 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Media-Style-Palette 2 | 3 | The library uses system methods to get MediaStyle notification colors from Android Oreo. 4 | 5 | ![Alt Text](https://github.com/mkaflowski/Media-Style-Palette/blob/master/images/demo.gif?raw=true) 6 | 7 | ## Installation 8 | 9 | Add JitPack in your root build.gradle at the end of repositories: 10 | ``` 11 | allprojects { 12 | repositories { 13 | jcenter() 14 | maven { url "https://jitpack.io" } 15 | } 16 | } 17 | ``` 18 | 19 | Add it as a dependency in your app's build.gradle file: 20 | ``` 21 | dependencies { 22 | compile 'com.github.mkaflowski:Media-Style-Palette:1.x' //CHANGE X TO CURRENT VERSION!!! 23 | } 24 | ``` 25 | 26 | ## How to use 27 | 28 | ```java 29 | MediaNotificationProcessor processor = new MediaNotificationProcessor(this, bitmap); // can use drawable 30 | 31 | int backgroundColor = processor.getBackgroundColor(); 32 | int primaryTextColor = processor.getPrimaryTextColor(); 33 | int secondaryTextColor = processor.getSecondaryTextColor(); 34 | 35 | boolean isLight = processor.isLight(); 36 | 37 | // for async processing: 38 | final MediaNotificationProcessor processor = new MediaNotificationProcessor(this); 39 | processor.getPaletteAsync(onPaletteLoadedListener, bitmap); 40 | ``` 41 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "mkaflowski.mediastylepalette" 7 | minSdkVersion 16 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:27.1.1' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.0' 25 | testImplementation 'junit:junit:4.12' 26 | implementation 'com.android.support:support-media-compat:27.1.1' 27 | 28 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 29 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 30 | implementation project(path: ':library') 31 | } 32 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/mateuszkaflowski/mediastylepalette/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.mateuszkaflowski.mediastylepalette; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.example.mateuszkaflowski.mediastylepalette", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/mateuszkaflowski/mediastylepalette/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.mateuszkaflowski.mediastylepalette; 2 | 3 | import android.app.Activity; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.app.PendingIntent; 8 | import android.content.ComponentName; 9 | import android.content.Context; 10 | import android.content.Intent; 11 | import android.graphics.Bitmap; 12 | import android.graphics.BitmapFactory; 13 | import android.graphics.Color; 14 | import android.graphics.PorterDuff; 15 | import android.os.Build; 16 | import android.os.Bundle; 17 | import android.support.annotation.RequiresApi; 18 | import android.support.v4.app.NotificationCompat; 19 | import android.support.v4.app.NotificationManagerCompat; 20 | import android.support.v4.media.session.MediaButtonReceiver; 21 | import android.support.v4.media.session.MediaSessionCompat; 22 | import android.util.Log; 23 | import android.view.View; 24 | import android.view.Window; 25 | import android.widget.ImageView; 26 | import android.widget.SeekBar; 27 | import android.widget.TextView; 28 | 29 | import mkaflowski.mediastylepalette.MediaNotificationProcessor; 30 | 31 | 32 | public class MainActivity extends Activity { 33 | 34 | ImageView ivPlay; 35 | ImageView ivPrevious; 36 | ImageView ivRewinf; 37 | ImageView ivFF; 38 | ImageView ivNext; 39 | TextView tvTitle; 40 | TextView tvArtist; 41 | TextView tvDesc; 42 | TextView tvStart; 43 | TextView tvEnd; 44 | private String CHANNEL_ID = "testchannel"; 45 | private MediaSessionCompat mediaSessionCompat; 46 | private Bitmap icon; 47 | 48 | int[] ids = { R.drawable.i, R.drawable.b, R.drawable.c, R.drawable.d, R.drawable.e, 49 | R.drawable.f, R.drawable.g, R.drawable.h}; 50 | private int drawableId; 51 | int i = 0; 52 | 53 | @Override 54 | protected void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | setContentView(R.layout.activity_main); 57 | drawableId = ids[i]; 58 | init(); 59 | } 60 | 61 | private void init() { 62 | final View background = findViewById(R.id.main_container); 63 | 64 | icon = BitmapFactory.decodeResource(getResources(), 65 | drawableId); 66 | 67 | ImageView ivCover = findViewById(R.id.cover); 68 | ivCover.setImageResource(drawableId); 69 | final MediaNotificationProcessor processor = new MediaNotificationProcessor(this); 70 | processor.getPaletteAsync(new MediaNotificationProcessor.OnPaletteLoadedListener() { 71 | @Override 72 | public void onPaletteLoaded(MediaNotificationProcessor mediaNotificationProcessor) { 73 | int backgroundColor = processor.getBackgroundColor(); 74 | int foregroundColor = processor.getPrimaryTextColor(); 75 | int secondaryColor = processor.getSecondaryTextColor(); 76 | 77 | background.setBackgroundColor(backgroundColor); 78 | 79 | ivPlay = findViewById(R.id.playButton); 80 | ivPrevious = findViewById(R.id.previous); 81 | ivRewinf = findViewById(R.id.minusButton); 82 | ivFF = findViewById(R.id.plusButton); 83 | ivNext = findViewById(R.id.next); 84 | 85 | tvTitle = findViewById(R.id.title); 86 | tvArtist = findViewById(R.id.episodeTitle); 87 | tvDesc = findViewById(R.id.episodeDesc); 88 | tvStart = findViewById(R.id.time); 89 | tvEnd = findViewById(R.id.totalTime); 90 | 91 | tvTitle.setTextColor(foregroundColor); 92 | tvArtist.setTextColor(secondaryColor); 93 | tvDesc.setTextColor(foregroundColor); 94 | 95 | SeekBar sb = findViewById(R.id.progressBar); 96 | sb.getProgressDrawable().setColorFilter(foregroundColor, PorterDuff.Mode.SRC_ATOP); 97 | sb.getThumb().setColorFilter(foregroundColor, PorterDuff.Mode.SRC_ATOP); 98 | sb.getIndeterminateDrawable().setColorFilter(secondaryColor, PorterDuff.Mode.SRC_ATOP); 99 | 100 | showNotification(drawableId); 101 | 102 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 103 | Window window = getWindow(); 104 | window.setStatusBarColor(backgroundColor); 105 | window.setNavigationBarColor(backgroundColor); 106 | } 107 | 108 | ivPlay.setColorFilter(foregroundColor); 109 | 110 | if(processor.isLight()){ 111 | ivPrevious.setColorFilter(Color.DKGRAY); 112 | ivRewinf.setColorFilter(Color.DKGRAY); 113 | ivFF.setColorFilter(Color.DKGRAY); 114 | ivNext.setColorFilter(Color.DKGRAY); 115 | tvStart.setTextColor(Color.DKGRAY); 116 | tvEnd.setTextColor(Color.DKGRAY); 117 | }else{ 118 | ivPrevious.setColorFilter(Color.WHITE); 119 | ivRewinf.setColorFilter(Color.WHITE); 120 | ivFF.setColorFilter(Color.WHITE); 121 | ivNext.setColorFilter(Color.WHITE); 122 | tvStart.setTextColor(Color.WHITE); 123 | tvEnd.setTextColor(Color.WHITE); 124 | } 125 | } 126 | },icon); 127 | 128 | 129 | } 130 | 131 | private void showNotification(int drawableId) { 132 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 133 | createChannel(); 134 | } 135 | initMediaSession(); 136 | 137 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID); 138 | 139 | Intent playIntent = new Intent(this, MainActivity.class); 140 | playIntent.setAction("s"); 141 | PendingIntent pplayIntent = PendingIntent.getService(this, 0, 142 | playIntent, 0); 143 | 144 | builder.addAction(R.drawable.minus, "Play", pplayIntent); 145 | builder.addAction(R.drawable.play_notification, "Play", pplayIntent); 146 | builder.addAction(R.drawable.plus, "Play", pplayIntent); 147 | 148 | builder.setSmallIcon(R.mipmap.ic_launcher) 149 | .setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle() 150 | .setShowActionsInCompactView(0, 1, 2) 151 | .setShowCancelButton(true) 152 | .setMediaSession(mediaSessionCompat.getSessionToken())) 153 | .setCategory(NotificationCompat.CATEGORY_TRANSPORT) 154 | .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 155 | .setShowWhen(false) 156 | .setContentTitle("Title Name") 157 | .setContentText("Content text") 158 | .setSmallIcon(R.drawable.pause) 159 | .setWhen(0) 160 | .setAutoCancel(true) 161 | .setLargeIcon(icon); 162 | 163 | 164 | Notification notification = builder.build(); 165 | 166 | NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); 167 | 168 | notificationManager.notify(112, notification); 169 | 170 | } 171 | 172 | @RequiresApi(Build.VERSION_CODES.O) 173 | private void createChannel() { 174 | NotificationManager 175 | mNotificationManager = 176 | (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 177 | // The id of the channel. 178 | String id = CHANNEL_ID; 179 | // The user-visible name of the channel. 180 | CharSequence name = "Media playback"; 181 | // The user-visible description of the channel. 182 | String description = "Media playback controls"; 183 | int importance = NotificationManager.IMPORTANCE_LOW; 184 | NotificationChannel mChannel = new NotificationChannel(id, name, importance); 185 | // Configure the notification channel. 186 | mChannel.setDescription(description); 187 | mChannel.setShowBadge(false); 188 | mChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); 189 | mNotificationManager.createNotificationChannel(mChannel); 190 | 191 | } 192 | 193 | private void initMediaSession() { 194 | ComponentName mediaButtonReceiver = new ComponentName(this, MediaButtonReceiver.class); 195 | mediaSessionCompat = new MediaSessionCompat(getApplicationContext(), "MediaTAG", mediaButtonReceiver, null); 196 | 197 | 198 | mediaSessionCompat.setCallback(null); 199 | mediaSessionCompat.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); 200 | 201 | Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); 202 | mediaButtonIntent.setClass(this, MediaButtonReceiver.class); 203 | PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0); 204 | mediaSessionCompat.setMediaButtonReceiver(pendingIntent); 205 | 206 | //setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING); 207 | mediaSessionCompat.setActive(true); 208 | //setSessionToken(mediaSessionCompat.getSessionToken()); 209 | 210 | } 211 | 212 | public void next(View view) { 213 | i++; 214 | if (i >= ids.length) 215 | i = 0; 216 | drawableId = ids[i]; 217 | init(); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/b.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/bookmark.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/c.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/cover.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/d.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/e.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/english.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/english.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/f.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/g.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/h.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/hammerzeit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/hammerzeit.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/i.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/i.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/minus.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/more.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/more.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/nagrales.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/nagrales.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/next.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/pause.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/pause_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/pause_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/pause_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/pause_pressed.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/play.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/play_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/play_notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/play_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/play_pressed.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/plus.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/previous.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/retrospektywa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/retrospektywa.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/tim.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/tim.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/wandzel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/drawable-xhdpi/wandzel.jpg -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 16 | 17 | 21 | 22 | 31 | 32 | 41 | 42 | 43 | 44 | 50 | 51 | 58 | 59 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 80 | 86 | 87 | 92 | 93 | 104 | 105 | 115 | 116 | 128 | 129 | 130 | 137 | 138 | 152 | 153 | 169 | 170 | 186 | 187 | 188 | 197 | 198 | 206 | 207 | 215 | 216 | 222 | 223 | 231 | 232 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MediaStylePalette 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/test/java/com/example/mateuszkaflowski/mediastylepalette/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.example.mateuszkaflowski.mediastylepalette; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | maven { url "https://jitpack.io" } 9 | 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.1.2' 13 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 18 13:26:54 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkaflowski/Media-Style-Palette/214c76f38ae4eac8cfb61c112e49cabb0ef3064d/images/demo.gif -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | 4 | group='com.github.mkaflowski' 5 | 6 | android { 7 | compileSdkVersion 27 8 | 9 | 10 | 11 | defaultConfig { 12 | minSdkVersion 15 13 | targetSdkVersion 27 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 18 | 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | 28 | } 29 | 30 | dependencies { 31 | implementation fileTree(dir: 'libs', include: ['*.jar']) 32 | 33 | implementation 'com.android.support:appcompat-v7:27.1.1' 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 36 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 37 | implementation 'com.android.support:palette-v7:27.1.1' 38 | } 39 | // build a jar with source files 40 | task sourcesJar(type: Jar) { 41 | from android.sourceSets.main.java.srcDirs 42 | classifier = 'sources' 43 | } 44 | 45 | task javadoc(type: Javadoc) { 46 | failOnError false 47 | source = android.sourceSets.main.java.sourceFiles 48 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 49 | classpath += configurations.compile 50 | } 51 | 52 | // build a jar with javadoc 53 | task javadocJar(type: Jar, dependsOn: javadoc) { 54 | classifier = 'javadoc' 55 | from javadoc.destinationDir 56 | } 57 | 58 | artifacts { 59 | archives sourcesJar 60 | archives javadocJar 61 | } 62 | -------------------------------------------------------------------------------- /library/library.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /library/src/main/java/mkaflowski/mediastylepalette/ImageUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | package mkaflowski.mediastylepalette; 17 | 18 | import android.graphics.Bitmap; 19 | import android.graphics.Bitmap.Config; 20 | import android.graphics.Canvas; 21 | import android.graphics.Matrix; 22 | import android.graphics.Paint; 23 | import android.graphics.PorterDuff; 24 | import android.graphics.drawable.BitmapDrawable; 25 | import android.graphics.drawable.Drawable; 26 | 27 | /** 28 | * Utility class for image analysis and processing. 29 | * 30 | * @hide 31 | */ 32 | public class ImageUtils { 33 | // Amount (max is 255) that two channels can differ before the color is no longer "gray". 34 | private static final int TOLERANCE = 20; 35 | // Alpha amount for which values below are considered transparent. 36 | private static final int ALPHA_TOLERANCE = 50; 37 | // Size of the smaller bitmap we're actually going to scan. 38 | private static final int COMPACT_BITMAP_SIZE = 64; // pixels 39 | private int[] mTempBuffer; 40 | private Bitmap mTempCompactBitmap; 41 | private Canvas mTempCompactBitmapCanvas; 42 | private Paint mTempCompactBitmapPaint; 43 | private final Matrix mTempMatrix = new Matrix(); 44 | 45 | /** 46 | * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect 47 | * gray". 48 | *

49 | * Instead of scanning every pixel in the bitmap, we first resize the bitmap to no more than 50 | * COMPACT_BITMAP_SIZE^2 pixels using filtering. The hope is that any non-gray color elements 51 | * will survive the squeezing process, contaminating the result with color. 52 | */ 53 | public boolean isGrayscale(Bitmap bitmap) { 54 | int height = bitmap.getHeight(); 55 | int width = bitmap.getWidth(); 56 | 57 | // shrink to a more manageable (yet hopefully no more or less colorful) size 58 | if (height > COMPACT_BITMAP_SIZE || width > COMPACT_BITMAP_SIZE) { 59 | if (mTempCompactBitmap == null) { 60 | mTempCompactBitmap = Bitmap.createBitmap( 61 | COMPACT_BITMAP_SIZE, COMPACT_BITMAP_SIZE, Config.ARGB_8888 62 | ); 63 | mTempCompactBitmapCanvas = new Canvas(mTempCompactBitmap); 64 | mTempCompactBitmapPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 65 | mTempCompactBitmapPaint.setFilterBitmap(true); 66 | } 67 | mTempMatrix.reset(); 68 | mTempMatrix.setScale( 69 | (float) COMPACT_BITMAP_SIZE / width, 70 | (float) COMPACT_BITMAP_SIZE / height, 71 | 0, 0); 72 | mTempCompactBitmapCanvas.drawColor(0, PorterDuff.Mode.SRC); // select all, erase 73 | mTempCompactBitmapCanvas.drawBitmap(bitmap, mTempMatrix, mTempCompactBitmapPaint); 74 | bitmap = mTempCompactBitmap; 75 | width = height = COMPACT_BITMAP_SIZE; 76 | } 77 | final int size = height * width; 78 | ensureBufferSize(size); 79 | bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height); 80 | for (int i = 0; i < size; i++) { 81 | if (!isGrayscale(mTempBuffer[i])) { 82 | return false; 83 | } 84 | } 85 | return true; 86 | } 87 | 88 | /** 89 | * Makes sure that {@code mTempBuffer} has at least length {@code size}. 90 | */ 91 | private void ensureBufferSize(int size) { 92 | if (mTempBuffer == null || mTempBuffer.length < size) { 93 | mTempBuffer = new int[size]; 94 | } 95 | } 96 | 97 | /** 98 | * Classifies a color as grayscale or not. Grayscale here means "very close to a perfect 99 | * gray"; if all three channels are approximately equal, this will return true. 100 | *

101 | * Note that really transparent colors are always grayscale. 102 | */ 103 | public static boolean isGrayscale(int color) { 104 | int alpha = 0xFF & (color >> 24); 105 | if (alpha < ALPHA_TOLERANCE) { 106 | return true; 107 | } 108 | int r = 0xFF & (color >> 16); 109 | int g = 0xFF & (color >> 8); 110 | int b = 0xFF & color; 111 | return Math.abs(r - g) < TOLERANCE 112 | && Math.abs(r - b) < TOLERANCE 113 | && Math.abs(g - b) < TOLERANCE; 114 | } 115 | 116 | /** 117 | * Convert a drawable to a bitmap, scaled to fit within maxWidth and maxHeight. 118 | */ 119 | public static Bitmap buildScaledBitmap(Drawable drawable, int maxWidth, 120 | int maxHeight) { 121 | if (drawable == null) { 122 | return null; 123 | } 124 | int originalWidth = drawable.getIntrinsicWidth(); 125 | int originalHeight = drawable.getIntrinsicHeight(); 126 | if ((originalWidth <= maxWidth) && (originalHeight <= maxHeight) && 127 | (drawable instanceof BitmapDrawable)) { 128 | return ((BitmapDrawable) drawable).getBitmap(); 129 | } 130 | if (originalHeight <= 0 || originalWidth <= 0) { 131 | return null; 132 | } 133 | // create a new bitmap, scaling down to fit the max dimensions of 134 | // a large notification icon if necessary 135 | float ratio = Math.min((float) maxWidth / (float) originalWidth, 136 | (float) maxHeight / (float) originalHeight); 137 | ratio = Math.min(1.0f, ratio); 138 | int scaledWidth = (int) (ratio * originalWidth); 139 | int scaledHeight = (int) (ratio * originalHeight); 140 | Bitmap result = Bitmap.createBitmap(scaledWidth, scaledHeight, Config.ARGB_8888); 141 | // and paint our app bitmap on it 142 | Canvas canvas = new Canvas(result); 143 | drawable.setBounds(0, 0, scaledWidth, scaledHeight); 144 | drawable.draw(canvas); 145 | return result; 146 | } 147 | } -------------------------------------------------------------------------------- /library/src/main/java/mkaflowski/mediastylepalette/MediaNotificationProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | 17 | package mkaflowski.mediastylepalette; 18 | 19 | import android.content.Context; 20 | import android.graphics.Bitmap; 21 | import android.graphics.Canvas; 22 | import android.graphics.Color; 23 | import android.graphics.drawable.BitmapDrawable; 24 | import android.graphics.drawable.Drawable; 25 | import android.os.Handler; 26 | import android.support.annotation.ColorInt; 27 | import android.support.annotation.FloatRange; 28 | import android.support.annotation.NonNull; 29 | import android.support.v7.graphics.Palette; 30 | 31 | import java.util.List; 32 | 33 | import static android.support.v4.graphics.ColorUtils.RGBToXYZ; 34 | 35 | /** 36 | * A class the processes media notifications and extracts the right text and background colors. 37 | */ 38 | public class MediaNotificationProcessor { 39 | 40 | /** 41 | * The fraction below which we select the vibrant instead of the light/dark vibrant color 42 | */ 43 | private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 1.0f; 44 | 45 | /** 46 | * Minimum saturation that a muted color must have if there exists if deciding between two 47 | * colors 48 | */ 49 | private static final float MIN_SATURATION_WHEN_DECIDING = 0.19f; 50 | 51 | /** 52 | * Minimum fraction that any color must have to be picked up as a text color 53 | */ 54 | private static final double MINIMUM_IMAGE_FRACTION = 0.002; 55 | 56 | /** 57 | * The population fraction to select the dominant color as the text color over a the colored 58 | * ones. 59 | */ 60 | private static final float POPULATION_FRACTION_FOR_DOMINANT = 0.01f; 61 | 62 | /** 63 | * The population fraction to select a white or black color as the background over a color. 64 | */ 65 | private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f; 66 | private static final float BLACK_MAX_LIGHTNESS = 0.08f; 67 | private static final float WHITE_MIN_LIGHTNESS = 0.90f; 68 | private static final int RESIZE_BITMAP_AREA = 150 * 150; 69 | private float[] mFilteredBackgroundHsl = null; 70 | private Palette.Filter mBlackWhiteFilter = new Palette.Filter() { 71 | @Override 72 | public boolean isAllowed(int rgb, @NonNull float[] hsl) { 73 | return !isWhiteOrBlack(hsl); 74 | } 75 | }; 76 | 77 | private boolean mIsLowPriority; 78 | private int backgroundColor; 79 | private int secondaryTextColor; 80 | private int primaryTextColor; 81 | private int actionBarColor; 82 | 83 | private Drawable drawable; 84 | private Context context; 85 | 86 | public MediaNotificationProcessor(Context context, Drawable drawable) { 87 | this.context = context; 88 | this.drawable = drawable; 89 | getMediaPalette(); 90 | } 91 | 92 | public MediaNotificationProcessor(Context context, Bitmap bitmap) { 93 | this.context = context; 94 | this.drawable = new BitmapDrawable(context.getResources(), bitmap); 95 | getMediaPalette(); 96 | } 97 | 98 | public MediaNotificationProcessor(Context context) { 99 | this.context = context; 100 | } 101 | 102 | public void getPaletteAsync(final OnPaletteLoadedListener onPaletteLoadedListener, Drawable drawable) { 103 | this.drawable = drawable; 104 | final Handler handler = new Handler(); 105 | new Thread(new Runnable() { 106 | @Override 107 | public void run() { 108 | getMediaPalette(); 109 | handler.post(new Runnable() { 110 | @Override 111 | public void run() { 112 | onPaletteLoadedListener.onPaletteLoaded(MediaNotificationProcessor.this); 113 | } 114 | }); 115 | } 116 | }).start(); 117 | 118 | } 119 | 120 | public void getPaletteAsync(OnPaletteLoadedListener onPaletteLoadedListener, Bitmap bitmap) { 121 | this.drawable = new BitmapDrawable(context.getResources(), bitmap); 122 | getPaletteAsync(onPaletteLoadedListener, this.drawable); 123 | } 124 | 125 | 126 | /** 127 | * Processes a drawable and calculates the appropriate colors that should 128 | * be used. 129 | */ 130 | private void getMediaPalette() { 131 | Bitmap bitmap; 132 | if (drawable != null) { 133 | // We're transforming the builder, let's make sure all baked in RemoteViews are 134 | // rebuilt! 135 | 136 | int width = drawable.getIntrinsicWidth(); 137 | int height = drawable.getIntrinsicHeight(); 138 | int area = width * height; 139 | if (area > RESIZE_BITMAP_AREA) { 140 | double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area); 141 | width = (int) (factor * width); 142 | height = (int) (factor * height); 143 | 144 | bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 145 | Canvas canvas = new Canvas(bitmap); 146 | drawable.setBounds(0, 0, width, height); 147 | drawable.draw(canvas); 148 | 149 | // for the background we only take the left side of the image to ensure 150 | // a smooth transition 151 | Palette.Builder paletteBuilder = Palette.from(bitmap) 152 | .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) 153 | .clearFilters() // we want all colors, red / white / black ones too! 154 | .resizeBitmapArea(RESIZE_BITMAP_AREA); 155 | Palette palette = paletteBuilder.generate(); 156 | backgroundColor = findBackgroundColorAndFilter(drawable); 157 | // we want most of the full region again, slightly shifted to the right 158 | float textColorStartWidthFraction = 0.4f; 159 | paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0, 160 | bitmap.getWidth(), 161 | bitmap.getHeight()); 162 | if (mFilteredBackgroundHsl != null) { 163 | paletteBuilder.addFilter(new Palette.Filter() { 164 | @Override 165 | public boolean isAllowed(int rgb, @NonNull float[] hsl) { 166 | // at least 10 degrees hue difference 167 | float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]); 168 | return diff > 10 && diff < 350; 169 | } 170 | }); 171 | } 172 | paletteBuilder.addFilter(mBlackWhiteFilter); 173 | palette = paletteBuilder.generate(); 174 | int foregroundColor = selectForegroundColor(backgroundColor, palette); 175 | ensureColors(backgroundColor, foregroundColor); 176 | } 177 | } 178 | 179 | } 180 | 181 | private int selectForegroundColor(int backgroundColor, Palette palette) { 182 | if (isColorLight(backgroundColor)) { 183 | return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(), 184 | palette.getVibrantSwatch(), 185 | palette.getDarkMutedSwatch(), 186 | palette.getMutedSwatch(), 187 | palette.getDominantSwatch(), 188 | Color.BLACK); 189 | } else { 190 | return selectForegroundColorForSwatches(palette.getLightVibrantSwatch(), 191 | palette.getVibrantSwatch(), 192 | palette.getLightMutedSwatch(), 193 | palette.getMutedSwatch(), 194 | palette.getDominantSwatch(), 195 | Color.WHITE); 196 | } 197 | } 198 | 199 | private static boolean isColorLight(int backgroundColor) { 200 | return calculateLuminance(backgroundColor) > 0.5f; 201 | } 202 | 203 | public boolean isLight() { 204 | return isColorLight(backgroundColor); 205 | } 206 | 207 | /** 208 | * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. 209 | *

Defined as the Y component in the XYZ representation of {@code color}.

210 | */ 211 | @FloatRange(from = 0.0, to = 1.0) 212 | private static double calculateLuminance(@ColorInt int color) { 213 | final double[] result = getTempDouble3Array(); 214 | colorToXYZ(color, result); 215 | // Luminance is the Y component 216 | return result[1] / 100; 217 | } 218 | 219 | private static final ThreadLocal TEMP_ARRAY = new ThreadLocal<>(); 220 | 221 | private static double[] getTempDouble3Array() { 222 | double[] result = TEMP_ARRAY.get(); 223 | if (result == null) { 224 | result = new double[3]; 225 | TEMP_ARRAY.set(result); 226 | } 227 | return result; 228 | } 229 | 230 | private static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { 231 | RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); 232 | } 233 | 234 | private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant, 235 | Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch, 236 | Palette.Swatch dominantSwatch, int fallbackColor) { 237 | Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant); 238 | if (coloredCandidate == null) { 239 | coloredCandidate = selectMutedCandidate(mutedSwatch, moreMutedSwatch); 240 | } 241 | if (coloredCandidate != null) { 242 | if (dominantSwatch == coloredCandidate) { 243 | return coloredCandidate.getRgb(); 244 | } else if ((float) coloredCandidate.getPopulation() / dominantSwatch.getPopulation() 245 | < POPULATION_FRACTION_FOR_DOMINANT 246 | && dominantSwatch.getHsl()[1] > MIN_SATURATION_WHEN_DECIDING) { 247 | return dominantSwatch.getRgb(); 248 | } else { 249 | return coloredCandidate.getRgb(); 250 | } 251 | } else if (hasEnoughPopulation(dominantSwatch)) { 252 | return dominantSwatch.getRgb(); 253 | } else { 254 | return fallbackColor; 255 | } 256 | } 257 | 258 | private Palette.Swatch selectMutedCandidate(Palette.Swatch first, 259 | Palette.Swatch second) { 260 | boolean firstValid = hasEnoughPopulation(first); 261 | boolean secondValid = hasEnoughPopulation(second); 262 | if (firstValid && secondValid) { 263 | float firstSaturation = first.getHsl()[1]; 264 | float secondSaturation = second.getHsl()[1]; 265 | float populationFraction = first.getPopulation() / (float) second.getPopulation(); 266 | if (firstSaturation * populationFraction > secondSaturation) { 267 | return first; 268 | } else { 269 | return second; 270 | } 271 | } else if (firstValid) { 272 | return first; 273 | } else if (secondValid) { 274 | return second; 275 | } 276 | return null; 277 | } 278 | 279 | private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) { 280 | boolean firstValid = hasEnoughPopulation(first); 281 | boolean secondValid = hasEnoughPopulation(second); 282 | if (firstValid && secondValid) { 283 | int firstPopulation = first.getPopulation(); 284 | int secondPopulation = second.getPopulation(); 285 | if (firstPopulation / (float) secondPopulation 286 | < POPULATION_FRACTION_FOR_MORE_VIBRANT) { 287 | return second; 288 | } else { 289 | return first; 290 | } 291 | } else if (firstValid) { 292 | return first; 293 | } else if (secondValid) { 294 | return second; 295 | } 296 | return null; 297 | } 298 | 299 | private boolean hasEnoughPopulation(Palette.Swatch swatch) { 300 | // We want a fraction that is at least 1% of the image 301 | return swatch != null 302 | && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION); 303 | } 304 | 305 | public int findBackgroundColorAndFilter(Drawable drawable) { 306 | int width = drawable.getIntrinsicWidth(); 307 | int height = drawable.getIntrinsicHeight(); 308 | int area = width * height; 309 | 310 | double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area); 311 | width = (int) (factor * width); 312 | height = (int) (factor * height); 313 | 314 | Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 315 | Canvas canvas = new Canvas(bitmap); 316 | drawable.setBounds(0, 0, width, height); 317 | drawable.draw(canvas); 318 | 319 | // for the background we only take the left side of the image to ensure 320 | // a smooth transition 321 | Palette.Builder paletteBuilder = Palette.from(bitmap) 322 | .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight()) 323 | .clearFilters() // we want all colors, red / white / black ones too! 324 | .resizeBitmapArea(RESIZE_BITMAP_AREA); 325 | Palette palette = paletteBuilder.generate(); 326 | // by default we use the dominant palette 327 | Palette.Swatch dominantSwatch = palette.getDominantSwatch(); 328 | if (dominantSwatch == null) { 329 | // We're not filtering on white or black 330 | mFilteredBackgroundHsl = null; 331 | return Color.WHITE; 332 | } 333 | 334 | if (!isWhiteOrBlack(dominantSwatch.getHsl())) { 335 | mFilteredBackgroundHsl = dominantSwatch.getHsl(); 336 | return dominantSwatch.getRgb(); 337 | } 338 | // Oh well, we selected black or white. Lets look at the second color! 339 | List swatches = palette.getSwatches(); 340 | float highestNonWhitePopulation = -1; 341 | Palette.Swatch second = null; 342 | for (Palette.Swatch swatch : swatches) { 343 | if (swatch != dominantSwatch 344 | && swatch.getPopulation() > highestNonWhitePopulation 345 | && !isWhiteOrBlack(swatch.getHsl())) { 346 | second = swatch; 347 | highestNonWhitePopulation = swatch.getPopulation(); 348 | } 349 | } 350 | if (second == null) { 351 | // We're not filtering on white or black 352 | mFilteredBackgroundHsl = null; 353 | return dominantSwatch.getRgb(); 354 | } 355 | if (dominantSwatch.getPopulation() / highestNonWhitePopulation 356 | > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) { 357 | // The dominant swatch is very dominant, lets take it! 358 | // We're not filtering on white or black 359 | mFilteredBackgroundHsl = null; 360 | return dominantSwatch.getRgb(); 361 | } else { 362 | mFilteredBackgroundHsl = second.getHsl(); 363 | return second.getRgb(); 364 | } 365 | } 366 | 367 | private boolean isWhiteOrBlack(float[] hsl) { 368 | return isBlack(hsl) || isWhite(hsl); 369 | } 370 | 371 | /** 372 | * @return true if the color represents a color which is close to black. 373 | */ 374 | private boolean isBlack(float[] hslColor) { 375 | return hslColor[2] <= BLACK_MAX_LIGHTNESS; 376 | } 377 | 378 | /** 379 | * @return true if the color represents a color which is close to white. 380 | */ 381 | private boolean isWhite(float[] hslColor) { 382 | return hslColor[2] >= WHITE_MIN_LIGHTNESS; 383 | } 384 | 385 | public void setIsLowPriority(boolean isLowPriority) { 386 | mIsLowPriority = isLowPriority; 387 | } 388 | 389 | 390 | /** 391 | * The lightness difference that has to be added to the primary text color to obtain the 392 | * secondary text color when the background is light. 393 | */ 394 | private static final int LIGHTNESS_TEXT_DIFFERENCE_LIGHT = 20; 395 | 396 | /** 397 | * The lightness difference that has to be added to the primary text color to obtain the 398 | * secondary text color when the background is dark. 399 | * A bit less then the above value, since it looks better on dark backgrounds. 400 | */ 401 | private static final int LIGHTNESS_TEXT_DIFFERENCE_DARK = -10; 402 | 403 | private void ensureColors(int backgroundColor, int mForegroundColor) { 404 | { 405 | double backLum = NotificationColorUtil.calculateLuminance(backgroundColor); 406 | double textLum = NotificationColorUtil.calculateLuminance(mForegroundColor); 407 | double contrast = NotificationColorUtil.calculateContrast(mForegroundColor, 408 | backgroundColor); 409 | // We only respect the given colors if worst case Black or White still has 410 | // contrast 411 | boolean backgroundLight = backLum > textLum 412 | && NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.BLACK) 413 | || backLum <= textLum 414 | && !NotificationColorUtil.satisfiesTextContrast(backgroundColor, Color.WHITE); 415 | if (contrast < 4.5f) { 416 | if (backgroundLight) { 417 | secondaryTextColor = NotificationColorUtil.findContrastColor( 418 | mForegroundColor, 419 | backgroundColor, 420 | true /* findFG */, 421 | 4.5f); 422 | primaryTextColor = NotificationColorUtil.changeColorLightness( 423 | secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_LIGHT); 424 | } else { 425 | secondaryTextColor = 426 | NotificationColorUtil.findContrastColorAgainstDark( 427 | mForegroundColor, 428 | backgroundColor, 429 | true /* findFG */, 430 | 4.5f); 431 | primaryTextColor = NotificationColorUtil.changeColorLightness( 432 | secondaryTextColor, -LIGHTNESS_TEXT_DIFFERENCE_DARK); 433 | } 434 | } else { 435 | primaryTextColor = mForegroundColor; 436 | secondaryTextColor = NotificationColorUtil.changeColorLightness( 437 | primaryTextColor, backgroundLight ? LIGHTNESS_TEXT_DIFFERENCE_LIGHT 438 | : LIGHTNESS_TEXT_DIFFERENCE_DARK); 439 | if (NotificationColorUtil.calculateContrast(secondaryTextColor, 440 | backgroundColor) < 4.5f) { 441 | // oh well the secondary is not good enough 442 | if (backgroundLight) { 443 | secondaryTextColor = NotificationColorUtil.findContrastColor( 444 | secondaryTextColor, 445 | backgroundColor, 446 | true /* findFG */, 447 | 4.5f); 448 | } else { 449 | secondaryTextColor 450 | = NotificationColorUtil.findContrastColorAgainstDark( 451 | secondaryTextColor, 452 | backgroundColor, 453 | true /* findFG */, 454 | 4.5f); 455 | } 456 | primaryTextColor = NotificationColorUtil.changeColorLightness( 457 | secondaryTextColor, backgroundLight 458 | ? -LIGHTNESS_TEXT_DIFFERENCE_LIGHT 459 | : -LIGHTNESS_TEXT_DIFFERENCE_DARK); 460 | } 461 | } 462 | } 463 | actionBarColor = NotificationColorUtil.resolveActionBarColor(context, 464 | backgroundColor); 465 | } 466 | 467 | 468 | public int getPrimaryTextColor() { 469 | return primaryTextColor; 470 | } 471 | 472 | public int getSecondaryTextColor() { 473 | return secondaryTextColor; 474 | } 475 | 476 | public int getBackgroundColor() { 477 | return backgroundColor; 478 | } 479 | 480 | public interface OnPaletteLoadedListener { 481 | void onPaletteLoaded(MediaNotificationProcessor mediaNotificationProcessor); 482 | } 483 | } -------------------------------------------------------------------------------- /library/src/main/java/mkaflowski/mediastylepalette/NotificationColorUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | 17 | package mkaflowski.mediastylepalette; 18 | 19 | import android.app.Notification; 20 | import android.content.Context; 21 | import android.graphics.Bitmap; 22 | import android.graphics.Color; 23 | import android.support.annotation.ColorInt; 24 | import android.support.annotation.FloatRange; 25 | import android.support.annotation.IntRange; 26 | import android.support.annotation.NonNull; 27 | import android.text.SpannableStringBuilder; 28 | import android.text.Spanned; 29 | import android.text.style.BackgroundColorSpan; 30 | import android.text.style.CharacterStyle; 31 | import android.text.style.ForegroundColorSpan; 32 | import android.text.style.TextAppearanceSpan; 33 | import android.util.Log; 34 | import android.util.Pair; 35 | 36 | import com.example.library.R; 37 | 38 | import java.util.WeakHashMap; 39 | 40 | /** 41 | * Helper class to process legacy (Holo) notifications to make them look like material notifications. 42 | * 43 | * @hide 44 | */ 45 | public class NotificationColorUtil { 46 | 47 | private static final String TAG = "NotificationColorUtil"; 48 | private static final boolean DEBUG = false; 49 | 50 | private static final Object sLock = new Object(); 51 | private static NotificationColorUtil sInstance; 52 | 53 | private final ImageUtils mImageUtils = new ImageUtils(); 54 | private final WeakHashMap> mGrayscaleBitmapCache = 55 | new WeakHashMap>(); 56 | 57 | private final int mGrayscaleIconMaxSize; // @dimen/notification_large_icon_width (64dp) 58 | 59 | public static NotificationColorUtil getInstance(Context context) { 60 | synchronized (sLock) { 61 | if (sInstance == null) { 62 | sInstance = new NotificationColorUtil(context); 63 | } 64 | return sInstance; 65 | } 66 | } 67 | 68 | private NotificationColorUtil(Context context) { 69 | mGrayscaleIconMaxSize = context.getResources().getDimensionPixelSize( 70 | R.dimen.notification_large_icon_width); 71 | } 72 | 73 | /** 74 | * Checks whether a Bitmap is a small grayscale icon. 75 | * Grayscale here means "very close to a perfect gray"; icon means "no larger than 64dp". 76 | * 77 | * @param bitmap The bitmap to test. 78 | * @return True if the bitmap is grayscale; false if it is color or too large to examine. 79 | */ 80 | public boolean isGrayscaleIcon(Bitmap bitmap) { 81 | // quick test: reject large bitmaps 82 | if (bitmap.getWidth() > mGrayscaleIconMaxSize 83 | || bitmap.getHeight() > mGrayscaleIconMaxSize) { 84 | return false; 85 | } 86 | 87 | synchronized (sLock) { 88 | Pair cached = mGrayscaleBitmapCache.get(bitmap); 89 | if (cached != null) { 90 | if (cached.second == bitmap.getGenerationId()) { 91 | return cached.first; 92 | } 93 | } 94 | } 95 | boolean result; 96 | int generationId; 97 | synchronized (mImageUtils) { 98 | result = mImageUtils.isGrayscale(bitmap); 99 | 100 | // generationId and the check whether the Bitmap is grayscale can't be read atomically 101 | // here. However, since the thread is in the process of posting the notification, we can 102 | // assume that it doesn't modify the bitmap while we are checking the pixels. 103 | generationId = bitmap.getGenerationId(); 104 | } 105 | synchronized (sLock) { 106 | mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId)); 107 | } 108 | return result; 109 | } 110 | 111 | 112 | 113 | 114 | 115 | // /** 116 | // * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on 117 | // * the text. 118 | // * 119 | // * @param charSequence The text to process. 120 | // * @return The color inverted text. 121 | // */ 122 | // public CharSequence invertCharSequenceColors(CharSequence charSequence) { 123 | // if (charSequence instanceof Spanned) { 124 | // Spanned ss = (Spanned) charSequence; 125 | // Object[] spans = ss.getSpans(0, ss.length(), Object.class); 126 | // SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); 127 | // for (Object span : spans) { 128 | // Object resultSpan = span; 129 | // if (resultSpan instanceof CharacterStyle) { 130 | // resultSpan = ((CharacterStyle) span).getUnderlying(); 131 | // } 132 | // if (resultSpan instanceof TextAppearanceSpan) { 133 | // TextAppearanceSpan processedSpan = processTextAppearanceSpan( 134 | // (TextAppearanceSpan) span); 135 | // if (processedSpan != resultSpan) { 136 | // resultSpan = processedSpan; 137 | // } else { 138 | // // we need to still take the orgininal for wrapped spans 139 | // resultSpan = span; 140 | // } 141 | // } else if (resultSpan instanceof ForegroundColorSpan) { 142 | // ForegroundColorSpan originalSpan = (ForegroundColorSpan) resultSpan; 143 | // int foregroundColor = originalSpan.getForegroundColor(); 144 | // resultSpan = new ForegroundColorSpan(processColor(foregroundColor)); 145 | // } else { 146 | // resultSpan = span; 147 | // } 148 | // builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), 149 | // ss.getSpanFlags(span)); 150 | // } 151 | // return builder; 152 | // } 153 | // return charSequence; 154 | // } 155 | 156 | // private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) { 157 | // ColorStateList colorStateList = span.getTextColor(); 158 | // if (colorStateList != null) { 159 | // int[] colors = colorStateList.getColors(); 160 | // boolean changed = false; 161 | // for (int i = 0; i < colors.length; i++) { 162 | // if (ImageUtils.isGrayscale(colors[i])) { 163 | // 164 | // // Allocate a new array so we don't change the colors in the old color state 165 | // // list. 166 | // if (!changed) { 167 | // colors = Arrays.copyOf(colors, colors.length); 168 | // } 169 | // colors[i] = processColor(colors[i]); 170 | // changed = true; 171 | // } 172 | // } 173 | // if (changed) { 174 | // return new TextAppearanceSpan( 175 | // span.getFamily(), span.getTextStyle(), span.getTextSize(), 176 | // new ColorStateList(colorStateList.getStates(), colors), 177 | // span.getLinkTextColor()); 178 | // } 179 | // } 180 | // return span; 181 | // } 182 | 183 | /** 184 | * Clears all color spans of a text 185 | * @param charSequence the input text 186 | * @return the same text but without color spans 187 | */ 188 | public static CharSequence clearColorSpans(CharSequence charSequence) { 189 | if (charSequence instanceof Spanned) { 190 | Spanned ss = (Spanned) charSequence; 191 | Object[] spans = ss.getSpans(0, ss.length(), Object.class); 192 | SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); 193 | for (Object span : spans) { 194 | Object resultSpan = span; 195 | if (resultSpan instanceof CharacterStyle) { 196 | resultSpan = ((CharacterStyle) span).getUnderlying(); 197 | } 198 | if (resultSpan instanceof TextAppearanceSpan) { 199 | TextAppearanceSpan originalSpan = (TextAppearanceSpan) resultSpan; 200 | if (originalSpan.getTextColor() != null) { 201 | resultSpan = new TextAppearanceSpan( 202 | originalSpan.getFamily(), 203 | originalSpan.getTextStyle(), 204 | originalSpan.getTextSize(), 205 | null, 206 | originalSpan.getLinkTextColor()); 207 | } 208 | } else if (resultSpan instanceof ForegroundColorSpan 209 | || (resultSpan instanceof BackgroundColorSpan)) { 210 | continue; 211 | } else { 212 | resultSpan = span; 213 | } 214 | builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), 215 | ss.getSpanFlags(span)); 216 | } 217 | return builder; 218 | } 219 | return charSequence; 220 | } 221 | 222 | private int processColor(int color) { 223 | return Color.argb(Color.alpha(color), 224 | 255 - Color.red(color), 225 | 255 - Color.green(color), 226 | 255 - Color.blue(color)); 227 | } 228 | 229 | /** 230 | * Finds a suitable color such that there's enough contrast. 231 | * 232 | * @param color the color to start searching from. 233 | * @param other the color to ensure contrast against. Assumed to be lighter than {@param color} 234 | * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. 235 | * @param minRatio the minimum contrast ratio required. 236 | * @return a color with the same hue as {@param color}, potentially darkened to meet the 237 | * contrast ratio. 238 | */ 239 | public static int findContrastColor(int color, int other, boolean findFg, double minRatio) { 240 | int fg = findFg ? color : other; 241 | int bg = findFg ? other : color; 242 | if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { 243 | return color; 244 | } 245 | 246 | double[] lab = new double[3]; 247 | ColorUtilsFromCompat.colorToLAB(findFg ? fg : bg, lab); 248 | 249 | double low = 0, high = lab[0]; 250 | final double a = lab[1], b = lab[2]; 251 | for (int i = 0; i < 15 && high - low > 0.00001; i++) { 252 | final double l = (low + high) / 2; 253 | if (findFg) { 254 | fg = ColorUtilsFromCompat.LABToColor(l, a, b); 255 | } else { 256 | bg = ColorUtilsFromCompat.LABToColor(l, a, b); 257 | } 258 | if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { 259 | low = l; 260 | } else { 261 | high = l; 262 | } 263 | } 264 | return ColorUtilsFromCompat.LABToColor(low, a, b); 265 | } 266 | 267 | /** 268 | * Finds a suitable alpha such that there's enough contrast. 269 | * 270 | * @param color the color to start searching from. 271 | * @param backgroundColor the color to ensure contrast against. 272 | * @param minRatio the minimum contrast ratio required. 273 | * @return the same color as {@param color} with potentially modified alpha to meet contrast 274 | */ 275 | public static int findAlphaToMeetContrast(int color, int backgroundColor, double minRatio) { 276 | int fg = color; 277 | int bg = backgroundColor; 278 | if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { 279 | return color; 280 | } 281 | int startAlpha = Color.alpha(color); 282 | int r = Color.red(color); 283 | int g = Color.green(color); 284 | int b = Color.blue(color); 285 | 286 | int low = startAlpha, high = 255; 287 | for (int i = 0; i < 15 && high - low > 0; i++) { 288 | final int alpha = (low + high) / 2; 289 | fg = Color.argb(alpha, r, g, b); 290 | if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { 291 | high = alpha; 292 | } else { 293 | low = alpha; 294 | } 295 | } 296 | return Color.argb(high, r, g, b); 297 | } 298 | 299 | /** 300 | * Finds a suitable color such that there's enough contrast. 301 | * 302 | * @param color the color to start searching from. 303 | * @param other the color to ensure contrast against. Assumed to be darker than {@param color} 304 | * @param findFg if true, we assume {@param color} is a foreground, otherwise a background. 305 | * @param minRatio the minimum contrast ratio required. 306 | * @return a color with the same hue as {@param color}, potentially darkened to meet the 307 | * contrast ratio. 308 | */ 309 | public static int findContrastColorAgainstDark(int color, int other, boolean findFg, 310 | double minRatio) { 311 | int fg = findFg ? color : other; 312 | int bg = findFg ? other : color; 313 | if (ColorUtilsFromCompat.calculateContrast(fg, bg) >= minRatio) { 314 | return color; 315 | } 316 | 317 | float[] hsl = new float[3]; 318 | ColorUtilsFromCompat.colorToHSL(findFg ? fg : bg, hsl); 319 | 320 | float low = hsl[2], high = 1; 321 | for (int i = 0; i < 15 && high - low > 0.00001; i++) { 322 | final float l = (low + high) / 2; 323 | hsl[2] = l; 324 | if (findFg) { 325 | fg = ColorUtilsFromCompat.HSLToColor(hsl); 326 | } else { 327 | bg = ColorUtilsFromCompat.HSLToColor(hsl); 328 | } 329 | if (ColorUtilsFromCompat.calculateContrast(fg, bg) > minRatio) { 330 | high = l; 331 | } else { 332 | low = l; 333 | } 334 | } 335 | return findFg ? fg : bg; 336 | } 337 | 338 | public static int ensureTextContrastOnBlack(int color) { 339 | return findContrastColorAgainstDark(color, Color.BLACK, true /* fg */, 12); 340 | } 341 | 342 | /** 343 | * Finds a large text color with sufficient contrast over bg that has the same or darker hue as 344 | * the original color, depending on the value of {@code isBgDarker}. 345 | * 346 | * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}. 347 | */ 348 | public static int ensureLargeTextContrast(int color, int bg, boolean isBgDarker) { 349 | return isBgDarker 350 | ? findContrastColorAgainstDark(color, bg, true, 3) 351 | : findContrastColor(color, bg, true, 3); 352 | } 353 | 354 | /** 355 | * Finds a text color with sufficient contrast over bg that has the same or darker hue as the 356 | * original color, depending on the value of {@code isBgDarker}. 357 | * 358 | * @param isBgDarker {@code true} if {@code bg} is darker than {@code color}. 359 | */ 360 | private static int ensureTextContrast(int color, int bg, boolean isBgDarker) { 361 | return isBgDarker 362 | ? findContrastColorAgainstDark(color, bg, true, 4.5) 363 | : findContrastColor(color, bg, true, 4.5); 364 | } 365 | 366 | /** Finds a background color for a text view with given text color and hint text color, that 367 | * has the same hue as the original color. 368 | */ 369 | public static int ensureTextBackgroundColor(int color, int textColor, int hintColor) { 370 | color = findContrastColor(color, hintColor, false, 3.0); 371 | return findContrastColor(color, textColor, false, 4.5); 372 | } 373 | 374 | private static String contrastChange(int colorOld, int colorNew, int bg) { 375 | return String.format("from %.2f:1 to %.2f:1", 376 | ColorUtilsFromCompat.calculateContrast(colorOld, bg), 377 | ColorUtilsFromCompat.calculateContrast(colorNew, bg)); 378 | } 379 | 380 | // /** 381 | // * Resolves {@param color} to an actual color if it is {@link Notification#COLOR_DEFAULT} 382 | // */ 383 | // public static int resolveColor(Context context, int color) { 384 | // if (color == Notification.COLOR_DEFAULT) { 385 | // return context.getColor(com.android.internal.R.color.notification_icon_default_color); 386 | // } 387 | // return color; 388 | // } 389 | // 390 | // 391 | // public static int resolveContrastColor(Context context, int notificationColor, 392 | // int backgroundColor) { 393 | // return NotificationColorUtil.resolveContrastColor(context, notificationColor, 394 | // backgroundColor, false /* isDark */); 395 | // } 396 | 397 | // /** 398 | // * Resolves a Notification's color such that it has enough contrast to be used as the 399 | // * color for the Notification's action and header text. 400 | // * 401 | // * @param notificationColor the color of the notification or {@link Notification#COLOR_DEFAULT} 402 | // * @param backgroundColor the background color to ensure the contrast against. 403 | // * @param isDark whether or not the {@code notificationColor} will be placed on a background 404 | // * that is darker than the color itself 405 | // * @return a color of the same hue with enough contrast against the backgrounds. 406 | // */ 407 | // public static int resolveContrastColor(Context context, int notificationColor, 408 | // int backgroundColor, boolean isDark) { 409 | // final int resolvedColor = resolveColor(context, notificationColor); 410 | // 411 | // final int actionBg = context.getColor( 412 | // com.android.internal.R.color.notification_action_list); 413 | // 414 | // int color = resolvedColor; 415 | // color = NotificationColorUtil.ensureLargeTextContrast(color, actionBg, isDark); 416 | // color = NotificationColorUtil.ensureTextContrast(color, backgroundColor, isDark); 417 | // 418 | // if (color != resolvedColor) { 419 | // if (DEBUG){ 420 | // Log.w(TAG, String.format( 421 | // "Enhanced contrast of notification for %s %s (over action)" 422 | // + " and %s (over background) by changing #%s to %s", 423 | // context.getPackageName(), 424 | // NotificationColorUtil.contrastChange(resolvedColor, color, actionBg), 425 | // NotificationColorUtil.contrastChange(resolvedColor, color, backgroundColor), 426 | // Integer.toHexString(resolvedColor), Integer.toHexString(color))); 427 | // } 428 | // } 429 | // return color; 430 | // } 431 | 432 | /** 433 | * Change a color by a specified value 434 | * @param baseColor the base color to lighten 435 | * @param amount the amount to lighten the color from 0 to 100. This corresponds to the L 436 | * increase in the LAB color space. A negative value will darken the color and 437 | * a positive will lighten it. 438 | * @return the changed color 439 | */ 440 | public static int changeColorLightness(int baseColor, int amount) { 441 | final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); 442 | ColorUtilsFromCompat.colorToLAB(baseColor, result); 443 | result[0] = Math.max(Math.min(100, result[0] + amount), 0); 444 | return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); 445 | } 446 | 447 | // public static int resolveAmbientColor(Context context, int notificationColor) { 448 | // final int resolvedColor = resolveColor(context, notificationColor); 449 | // 450 | // int color = resolvedColor; 451 | // color = NotificationColorUtil.ensureTextContrastOnBlack(color); 452 | // 453 | // if (color != resolvedColor) { 454 | // if (DEBUG){ 455 | // Log.w(TAG, String.format( 456 | // "Ambient contrast of notification for %s is %s (over black)" 457 | // + " by changing #%s to #%s", 458 | // context.getPackageName(), 459 | // NotificationColorUtil.contrastChange(resolvedColor, color, Color.BLACK), 460 | // Integer.toHexString(resolvedColor), Integer.toHexString(color))); 461 | // } 462 | // } 463 | // return color; 464 | // } 465 | 466 | // public static int resolvePrimaryColor(Context context, int backgroundColor) { 467 | // boolean useDark = shouldUseDark(backgroundColor); 468 | // if (useDark) { 469 | // return context.getColor( 470 | // com.android.internal.R.color.notification_primary_text_color_light); 471 | // } else { 472 | // return context.getColor( 473 | // com.android.internal.R.color.notification_primary_text_color_dark); 474 | // } 475 | // } 476 | // 477 | // public static int resolveSecondaryColor(Context context, int backgroundColor) { 478 | // boolean useDark = shouldUseDark(backgroundColor); 479 | // if (useDark) { 480 | // return context.getColor( 481 | // com.android.internal.R.color.notification_secondary_text_color_light); 482 | // } else { 483 | // return context.getColor( 484 | // com.android.internal.R.color.notification_secondary_text_color_dark); 485 | // } 486 | // } 487 | // 488 | public static int resolveActionBarColor(Context context, int backgroundColor) { 489 | if (backgroundColor == Notification.COLOR_DEFAULT) { 490 | return Color.BLACK; 491 | } 492 | return getShiftedColor(backgroundColor, 7); 493 | } 494 | 495 | /** 496 | * Get a color that stays in the same tint, but darkens or lightens it by a certain 497 | * amount. 498 | * This also looks at the lightness of the provided color and shifts it appropriately. 499 | * 500 | * @param color the base color to use 501 | * @param amount the amount from 1 to 100 how much to modify the color 502 | * @return the now color that was modified 503 | */ 504 | public static int getShiftedColor(int color, int amount) { 505 | final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); 506 | ColorUtilsFromCompat.colorToLAB(color, result); 507 | if (result[0] >= 4) { 508 | result[0] = Math.max(0, result[0] - amount); 509 | } else { 510 | result[0] = Math.min(100, result[0] + amount); 511 | } 512 | return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); 513 | } 514 | 515 | private static boolean shouldUseDark(int backgroundColor) { 516 | boolean useDark = backgroundColor == Notification.COLOR_DEFAULT; 517 | if (!useDark) { 518 | useDark = ColorUtilsFromCompat.calculateLuminance(backgroundColor) > 0.5; 519 | } 520 | return useDark; 521 | } 522 | 523 | public static double calculateLuminance(int backgroundColor) { 524 | return ColorUtilsFromCompat.calculateLuminance(backgroundColor); 525 | } 526 | 527 | 528 | public static double calculateContrast(int foregroundColor, int backgroundColor) { 529 | return ColorUtilsFromCompat.calculateContrast(foregroundColor, backgroundColor); 530 | } 531 | 532 | public static boolean satisfiesTextContrast(int backgroundColor, int foregroundColor) { 533 | return NotificationColorUtil.calculateContrast(foregroundColor, backgroundColor) >= 4.5; 534 | } 535 | 536 | /** 537 | * Composite two potentially translucent colors over each other and returns the result. 538 | */ 539 | public static int compositeColors(int foreground, int background) { 540 | return ColorUtilsFromCompat.compositeColors(foreground, background); 541 | } 542 | 543 | public static boolean isColorLight(int backgroundColor) { 544 | return calculateLuminance(backgroundColor) > 0.5f; 545 | } 546 | 547 | /** 548 | * Framework copy of functions needed from android.support.v4.graphics.ColorUtils. 549 | */ 550 | private static class ColorUtilsFromCompat { 551 | private static final double XYZ_WHITE_REFERENCE_X = 95.047; 552 | private static final double XYZ_WHITE_REFERENCE_Y = 100; 553 | private static final double XYZ_WHITE_REFERENCE_Z = 108.883; 554 | private static final double XYZ_EPSILON = 0.008856; 555 | private static final double XYZ_KAPPA = 903.3; 556 | 557 | private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; 558 | private static final int MIN_ALPHA_SEARCH_PRECISION = 1; 559 | 560 | private static final ThreadLocal TEMP_ARRAY = new ThreadLocal<>(); 561 | 562 | private ColorUtilsFromCompat() {} 563 | 564 | /** 565 | * Composite two potentially translucent colors over each other and returns the result. 566 | */ 567 | public static int compositeColors(@ColorInt int foreground, @ColorInt int background) { 568 | int bgAlpha = Color.alpha(background); 569 | int fgAlpha = Color.alpha(foreground); 570 | int a = compositeAlpha(fgAlpha, bgAlpha); 571 | 572 | int r = compositeComponent(Color.red(foreground), fgAlpha, 573 | Color.red(background), bgAlpha, a); 574 | int g = compositeComponent(Color.green(foreground), fgAlpha, 575 | Color.green(background), bgAlpha, a); 576 | int b = compositeComponent(Color.blue(foreground), fgAlpha, 577 | Color.blue(background), bgAlpha, a); 578 | 579 | return Color.argb(a, r, g, b); 580 | } 581 | 582 | private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { 583 | return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); 584 | } 585 | 586 | private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { 587 | if (a == 0) return 0; 588 | return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); 589 | } 590 | 591 | /** 592 | * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. 593 | *

Defined as the Y component in the XYZ representation of {@code color}.

594 | */ 595 | @FloatRange(from = 0.0, to = 1.0) 596 | public static double calculateLuminance(@ColorInt int color) { 597 | final double[] result = getTempDouble3Array(); 598 | colorToXYZ(color, result); 599 | // Luminance is the Y component 600 | return result[1] / 100; 601 | } 602 | 603 | /** 604 | * Returns the contrast ratio between {@code foreground} and {@code background}. 605 | * {@code background} must be opaque. 606 | *

607 | * Formula defined 608 | * here. 609 | */ 610 | public static double calculateContrast(@ColorInt int foreground, @ColorInt int background) { 611 | if (Color.alpha(background) != 255) { 612 | Log.wtf(TAG, "background can not be translucent: #" 613 | + Integer.toHexString(background)); 614 | } 615 | if (Color.alpha(foreground) < 255) { 616 | // If the foreground is translucent, composite the foreground over the background 617 | foreground = compositeColors(foreground, background); 618 | } 619 | 620 | final double luminance1 = calculateLuminance(foreground) + 0.05; 621 | final double luminance2 = calculateLuminance(background) + 0.05; 622 | 623 | // Now return the lighter luminance divided by the darker luminance 624 | return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); 625 | } 626 | 627 | /** 628 | * Convert the ARGB color to its CIE Lab representative components. 629 | * 630 | * @param color the ARGB color to convert. The alpha component is ignored 631 | * @param outLab 3-element array which holds the resulting LAB components 632 | */ 633 | public static void colorToLAB(@ColorInt int color, @NonNull double[] outLab) { 634 | RGBToLAB(Color.red(color), Color.green(color), Color.blue(color), outLab); 635 | } 636 | 637 | /** 638 | * Convert RGB components to its CIE Lab representative components. 639 | * 640 | *

    641 | *
  • outLab[0] is L [0 ...100)
  • 642 | *
  • outLab[1] is a [-128...127)
  • 643 | *
  • outLab[2] is b [-128...127)
  • 644 | *
645 | * 646 | * @param r red component value [0..255] 647 | * @param g green component value [0..255] 648 | * @param b blue component value [0..255] 649 | * @param outLab 3-element array which holds the resulting LAB components 650 | */ 651 | public static void RGBToLAB(@IntRange(from = 0x0, to = 0xFF) int r, 652 | @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 653 | @NonNull double[] outLab) { 654 | // First we convert RGB to XYZ 655 | RGBToXYZ(r, g, b, outLab); 656 | // outLab now contains XYZ 657 | XYZToLAB(outLab[0], outLab[1], outLab[2], outLab); 658 | // outLab now contains LAB representation 659 | } 660 | 661 | /** 662 | * Convert the ARGB color to it's CIE XYZ representative components. 663 | * 664 | *

The resulting XYZ representation will use the D65 illuminant and the CIE 665 | * 2° Standard Observer (1931).

666 | * 667 | *
    668 | *
  • outXyz[0] is X [0 ...95.047)
  • 669 | *
  • outXyz[1] is Y [0...100)
  • 670 | *
  • outXyz[2] is Z [0...108.883)
  • 671 | *
672 | * 673 | * @param color the ARGB color to convert. The alpha component is ignored 674 | * @param outXyz 3-element array which holds the resulting LAB components 675 | */ 676 | public static void colorToXYZ(@ColorInt int color, @NonNull double[] outXyz) { 677 | RGBToXYZ(Color.red(color), Color.green(color), Color.blue(color), outXyz); 678 | } 679 | 680 | /** 681 | * Convert RGB components to it's CIE XYZ representative components. 682 | * 683 | *

The resulting XYZ representation will use the D65 illuminant and the CIE 684 | * 2° Standard Observer (1931).

685 | * 686 | *
    687 | *
  • outXyz[0] is X [0 ...95.047)
  • 688 | *
  • outXyz[1] is Y [0...100)
  • 689 | *
  • outXyz[2] is Z [0...108.883)
  • 690 | *
691 | * 692 | * @param r red component value [0..255] 693 | * @param g green component value [0..255] 694 | * @param b blue component value [0..255] 695 | * @param outXyz 3-element array which holds the resulting XYZ components 696 | */ 697 | public static void RGBToXYZ(@IntRange(from = 0x0, to = 0xFF) int r, 698 | @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 699 | @NonNull double[] outXyz) { 700 | if (outXyz.length != 3) { 701 | throw new IllegalArgumentException("outXyz must have a length of 3."); 702 | } 703 | 704 | double sr = r / 255.0; 705 | sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4); 706 | double sg = g / 255.0; 707 | sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4); 708 | double sb = b / 255.0; 709 | sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4); 710 | 711 | outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805); 712 | outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722); 713 | outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505); 714 | } 715 | 716 | /** 717 | * Converts a color from CIE XYZ to CIE Lab representation. 718 | * 719 | *

This method expects the XYZ representation to use the D65 illuminant and the CIE 720 | * 2° Standard Observer (1931).

721 | * 722 | *
    723 | *
  • outLab[0] is L [0 ...100)
  • 724 | *
  • outLab[1] is a [-128...127)
  • 725 | *
  • outLab[2] is b [-128...127)
  • 726 | *
727 | * 728 | * @param x X component value [0...95.047) 729 | * @param y Y component value [0...100) 730 | * @param z Z component value [0...108.883) 731 | * @param outLab 3-element array which holds the resulting Lab components 732 | */ 733 | public static void XYZToLAB(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, 734 | @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, 735 | @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z, 736 | @NonNull double[] outLab) { 737 | if (outLab.length != 3) { 738 | throw new IllegalArgumentException("outLab must have a length of 3."); 739 | } 740 | x = pivotXyzComponent(x / XYZ_WHITE_REFERENCE_X); 741 | y = pivotXyzComponent(y / XYZ_WHITE_REFERENCE_Y); 742 | z = pivotXyzComponent(z / XYZ_WHITE_REFERENCE_Z); 743 | outLab[0] = Math.max(0, 116 * y - 16); 744 | outLab[1] = 500 * (x - y); 745 | outLab[2] = 200 * (y - z); 746 | } 747 | 748 | /** 749 | * Converts a color from CIE Lab to CIE XYZ representation. 750 | * 751 | *

The resulting XYZ representation will use the D65 illuminant and the CIE 752 | * 2° Standard Observer (1931).

753 | * 754 | *
    755 | *
  • outXyz[0] is X [0 ...95.047)
  • 756 | *
  • outXyz[1] is Y [0...100)
  • 757 | *
  • outXyz[2] is Z [0...108.883)
  • 758 | *
759 | * 760 | * @param l L component value [0...100) 761 | * @param a A component value [-128...127) 762 | * @param b B component value [-128...127) 763 | * @param outXyz 3-element array which holds the resulting XYZ components 764 | */ 765 | public static void LABToXYZ(@FloatRange(from = 0f, to = 100) final double l, 766 | @FloatRange(from = -128, to = 127) final double a, 767 | @FloatRange(from = -128, to = 127) final double b, 768 | @NonNull double[] outXyz) { 769 | final double fy = (l + 16) / 116; 770 | final double fx = a / 500 + fy; 771 | final double fz = fy - b / 200; 772 | 773 | double tmp = Math.pow(fx, 3); 774 | final double xr = tmp > XYZ_EPSILON ? tmp : (116 * fx - 16) / XYZ_KAPPA; 775 | final double yr = l > XYZ_KAPPA * XYZ_EPSILON ? Math.pow(fy, 3) : l / XYZ_KAPPA; 776 | 777 | tmp = Math.pow(fz, 3); 778 | final double zr = tmp > XYZ_EPSILON ? tmp : (116 * fz - 16) / XYZ_KAPPA; 779 | 780 | outXyz[0] = xr * XYZ_WHITE_REFERENCE_X; 781 | outXyz[1] = yr * XYZ_WHITE_REFERENCE_Y; 782 | outXyz[2] = zr * XYZ_WHITE_REFERENCE_Z; 783 | } 784 | 785 | /** 786 | * Converts a color from CIE XYZ to its RGB representation. 787 | * 788 | *

This method expects the XYZ representation to use the D65 illuminant and the CIE 789 | * 2° Standard Observer (1931).

790 | * 791 | * @param x X component value [0...95.047) 792 | * @param y Y component value [0...100) 793 | * @param z Z component value [0...108.883) 794 | * @return int containing the RGB representation 795 | */ 796 | @ColorInt 797 | public static int XYZToColor(@FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_X) double x, 798 | @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Y) double y, 799 | @FloatRange(from = 0f, to = XYZ_WHITE_REFERENCE_Z) double z) { 800 | double r = (x * 3.2406 + y * -1.5372 + z * -0.4986) / 100; 801 | double g = (x * -0.9689 + y * 1.8758 + z * 0.0415) / 100; 802 | double b = (x * 0.0557 + y * -0.2040 + z * 1.0570) / 100; 803 | 804 | r = r > 0.0031308 ? 1.055 * Math.pow(r, 1 / 2.4) - 0.055 : 12.92 * r; 805 | g = g > 0.0031308 ? 1.055 * Math.pow(g, 1 / 2.4) - 0.055 : 12.92 * g; 806 | b = b > 0.0031308 ? 1.055 * Math.pow(b, 1 / 2.4) - 0.055 : 12.92 * b; 807 | 808 | return Color.rgb( 809 | constrain((int) Math.round(r * 255), 0, 255), 810 | constrain((int) Math.round(g * 255), 0, 255), 811 | constrain((int) Math.round(b * 255), 0, 255)); 812 | } 813 | 814 | /** 815 | * Converts a color from CIE Lab to its RGB representation. 816 | * 817 | * @param l L component value [0...100] 818 | * @param a A component value [-128...127] 819 | * @param b B component value [-128...127] 820 | * @return int containing the RGB representation 821 | */ 822 | @ColorInt 823 | public static int LABToColor(@FloatRange(from = 0f, to = 100) final double l, 824 | @FloatRange(from = -128, to = 127) final double a, 825 | @FloatRange(from = -128, to = 127) final double b) { 826 | final double[] result = getTempDouble3Array(); 827 | LABToXYZ(l, a, b, result); 828 | return XYZToColor(result[0], result[1], result[2]); 829 | } 830 | 831 | private static int constrain(int amount, int low, int high) { 832 | return amount < low ? low : (amount > high ? high : amount); 833 | } 834 | 835 | private static float constrain(float amount, float low, float high) { 836 | return amount < low ? low : (amount > high ? high : amount); 837 | } 838 | 839 | private static double pivotXyzComponent(double component) { 840 | return component > XYZ_EPSILON 841 | ? Math.pow(component, 1 / 3.0) 842 | : (XYZ_KAPPA * component + 16) / 116; 843 | } 844 | 845 | public static double[] getTempDouble3Array() { 846 | double[] result = TEMP_ARRAY.get(); 847 | if (result == null) { 848 | result = new double[3]; 849 | TEMP_ARRAY.set(result); 850 | } 851 | return result; 852 | } 853 | 854 | /** 855 | * Convert HSL (hue-saturation-lightness) components to a RGB color. 856 | *
    857 | *
  • hsl[0] is Hue [0 .. 360)
  • 858 | *
  • hsl[1] is Saturation [0...1]
  • 859 | *
  • hsl[2] is Lightness [0...1]
  • 860 | *
861 | * If hsv values are out of range, they are pinned. 862 | * 863 | * @param hsl 3-element array which holds the input HSL components 864 | * @return the resulting RGB color 865 | */ 866 | @ColorInt 867 | public static int HSLToColor(@NonNull float[] hsl) { 868 | final float h = hsl[0]; 869 | final float s = hsl[1]; 870 | final float l = hsl[2]; 871 | 872 | final float c = (1f - Math.abs(2 * l - 1f)) * s; 873 | final float m = l - 0.5f * c; 874 | final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); 875 | 876 | final int hueSegment = (int) h / 60; 877 | 878 | int r = 0, g = 0, b = 0; 879 | 880 | switch (hueSegment) { 881 | case 0: 882 | r = Math.round(255 * (c + m)); 883 | g = Math.round(255 * (x + m)); 884 | b = Math.round(255 * m); 885 | break; 886 | case 1: 887 | r = Math.round(255 * (x + m)); 888 | g = Math.round(255 * (c + m)); 889 | b = Math.round(255 * m); 890 | break; 891 | case 2: 892 | r = Math.round(255 * m); 893 | g = Math.round(255 * (c + m)); 894 | b = Math.round(255 * (x + m)); 895 | break; 896 | case 3: 897 | r = Math.round(255 * m); 898 | g = Math.round(255 * (x + m)); 899 | b = Math.round(255 * (c + m)); 900 | break; 901 | case 4: 902 | r = Math.round(255 * (x + m)); 903 | g = Math.round(255 * m); 904 | b = Math.round(255 * (c + m)); 905 | break; 906 | case 5: 907 | case 6: 908 | r = Math.round(255 * (c + m)); 909 | g = Math.round(255 * m); 910 | b = Math.round(255 * (x + m)); 911 | break; 912 | } 913 | 914 | r = constrain(r, 0, 255); 915 | g = constrain(g, 0, 255); 916 | b = constrain(b, 0, 255); 917 | 918 | return Color.rgb(r, g, b); 919 | } 920 | 921 | /** 922 | * Convert the ARGB color to its HSL (hue-saturation-lightness) components. 923 | *
    924 | *
  • outHsl[0] is Hue [0 .. 360)
  • 925 | *
  • outHsl[1] is Saturation [0...1]
  • 926 | *
  • outHsl[2] is Lightness [0...1]
  • 927 | *
928 | * 929 | * @param color the ARGB color to convert. The alpha component is ignored 930 | * @param outHsl 3-element array which holds the resulting HSL components 931 | */ 932 | public static void colorToHSL(@ColorInt int color, @NonNull float[] outHsl) { 933 | RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), outHsl); 934 | } 935 | 936 | /** 937 | * Convert RGB components to HSL (hue-saturation-lightness). 938 | *
    939 | *
  • outHsl[0] is Hue [0 .. 360)
  • 940 | *
  • outHsl[1] is Saturation [0...1]
  • 941 | *
  • outHsl[2] is Lightness [0...1]
  • 942 | *
943 | * 944 | * @param r red component value [0..255] 945 | * @param g green component value [0..255] 946 | * @param b blue component value [0..255] 947 | * @param outHsl 3-element array which holds the resulting HSL components 948 | */ 949 | public static void RGBToHSL(@IntRange(from = 0x0, to = 0xFF) int r, 950 | @IntRange(from = 0x0, to = 0xFF) int g, @IntRange(from = 0x0, to = 0xFF) int b, 951 | @NonNull float[] outHsl) { 952 | final float rf = r / 255f; 953 | final float gf = g / 255f; 954 | final float bf = b / 255f; 955 | 956 | final float max = Math.max(rf, Math.max(gf, bf)); 957 | final float min = Math.min(rf, Math.min(gf, bf)); 958 | final float deltaMaxMin = max - min; 959 | 960 | float h, s; 961 | float l = (max + min) / 2f; 962 | 963 | if (max == min) { 964 | // Monochromatic 965 | h = s = 0f; 966 | } else { 967 | if (max == rf) { 968 | h = ((gf - bf) / deltaMaxMin) % 6f; 969 | } else if (max == gf) { 970 | h = ((bf - rf) / deltaMaxMin) + 2f; 971 | } else { 972 | h = ((rf - gf) / deltaMaxMin) + 4f; 973 | } 974 | 975 | s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); 976 | } 977 | 978 | h = (h * 60f) % 360f; 979 | if (h < 0) { 980 | h += 360f; 981 | } 982 | 983 | outHsl[0] = constrain(h, 0f, 360f); 984 | outHsl[1] = constrain(s, 0f, 1f); 985 | outHsl[2] = constrain(l, 0f, 1f); 986 | } 987 | 988 | } 989 | } -------------------------------------------------------------------------------- /library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MediaStyle Palette 3 | 4 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file is automatically generated by Android Studio. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file should *NOT* be checked into Version Control Systems, 5 | # as it contains information specific to your local configuration. 6 | # 7 | # Location of the SDK. This is only used by Gradle. 8 | # For customization when using a Version Control System, please read the 9 | # header note. 10 | sdk.dir=/Users/mateuszkaflowski/Library/Android/sdk -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':library' 2 | --------------------------------------------------------------------------------