contain

Palette Cycle

Creating a Live Wallpaper Android App with Kotlin in Intellij

Welcome

1
2
3
4

INSPIRATION

CYCLING

contain

CYCLING

background

CYCLING

background
bark
zoom

CYCLING

background
bark
zoom

1
pixel-1
lines

CYCLING

background
bark
zoom

1
pixel-1
2
pixel-2
lines

CYCLING

background
bark
zoom

1
pixel-1
2
pixel-2
3
pixel-3
lines

FINAL APP

Code

INTELLIJ

  • Focus on productivity through ergonomic design
  • Developer convenience
  • Contextual code suggestions

ANDROID STUDIO

  • Replaced Eclipse as Official Android IDE
  • Basically Intellij

ANDROID FILE STRUCTURE

  • Android Manifest
  • XML for Layouts
  • Java / Kotlin for functionality

MANIFEST

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="rak.pixellwp">

    <uses-feature
        android:name="android.software.live_wallpaper"
        android:required="true"/>

    <uses-permission android:name="android.permission.INTERNET"/>

    <application android:name=".PixelLwpApp">

    <service
     android:name=".cycling.CyclingWallpaperService">
     <meta-data
        android:name="android.service.wallpaper"
        android:resource="@xml/cycling_wallpaper" />
    </service>

        <activity
            android:name=".cycling.preferences.CyclingPreferenceActivity"
            android:theme="@android:style/Theme.Material.Wallpaper.NoTitleBar" >
        </activity>

        <activity android:name=".SetWallpaperActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
  • Wiring of activities and services

KOTLIN

  • Built by Jetbrains (Intellij, Android Studio)
  • Java bytecode
  • OO or Functional
  • Concise
  • Developer Focused
  • First class citizen (Spring and Android)
  • Non-nullable and immutable focus

DATA CLASSES - JAVA

package rak.pixellwp.cycling.models;

public class Pixel {
    private float x;
    private float y;
    private int index;

    public Pixel(float x, float y, int index){
        this.x = x;
        this.y = y;
        this.index = index;
    }

    public float getX(){
        return this.x;
    }

    public float getY(){
        return this.y;
    }

    public int getIndex(){
        return this.index;
    }
}

DATA CLASSES - Elixir

defmodule Pixel do
  @type x :: Float.t()
  @type y :: Float.t()
  @type index :: Integer.t()

  @enforce_keys [:x, :y, :index]
  defstruct(x, y, index)
end

DATA CLASSES - KOTLIN

package rak.pixellwp.cycling.models

class Pixel(val x: Float, val y: Float, val index: Int)

  • Val = immutable
  • Var = Mutable

DATA KEYWORD

package rak.pixellwp.cycling.models

data class Pixel(val x: Float, val y: Float, val index: Int)

SYNTAX TWEAKS

fun getCurrentPalette(current: Palette, currentTime: Int) : Palette{
    return someValue
}

STREAMING AND FUNCTIONAL PROGRAMMING

Java

public class ForEachExample {

    private Map<Integer, Palette> parseEntries(Map<String, String> entries, List<Palette> palettes){
        Map<Integer, Palette> newMap = new HashMap<>();
        for (Map.Entry<String, String> entry : entries.entrySet()) {
            Palette value = findPaletteWithId(entry.getValue(), palettes);
            newMap.put(Integer.parseInt(entry.getKey()), value);
        }
        return newMap;
    }

    private Palette findPaletteWithId(String id, List<Palette> palettes){
        for (Palette it : palettes){
            if (it.getId().equals(id)){
                return it;
            }
        }
        throw new NoSuchElementException();
    }
}

STREAMING AND FUNCTIONAL PROGRAMMING

private fun parseEntries(entries: Map<String, String>, palettes: List<Palette>): Map<Int, Palette> {
    val newMap = HashMap<Int, Palette>()
    entries.forEach{ entry -> newMap[Integer.parseInt(entry.key)] = palettes.first { it.id == entry.value } }
    return newMap
}

LOADING IMAGES - LOGGING

class ImageLoader(private val context: Context) : JsonDownloadListener {

    fun downloadImage(image: ImageInfo) {
        Log.d(logTag, "Unable to find ${image.name} locally, downloading using id ${image.id}")
        JsonDownloader(image, this).execute()
        Toast.makeText(context, "Unable to find ${image.name} locally. I'll change the image as soon as it's downloaded", Toast.LENGTH_LONG).show()
    }
}

DOWNLOADING IMAGES


class JsonDownloader(private val image: ImageInfo, private val listener: JsonDownloadListener) : AsyncTask<String, Void, String>() {
    override fun doInBackground(vararg params: String?): String {
        return downloadImage()
    }

    private fun downloadImage(): String {
        …
    }

    override fun onPostExecute(result: String?) {
        …
    }
}

DOWNLOADING IMAGES


class JsonDownloader(private val image: ImageInfo, private val listener: JsonDownloadListener) : AsyncTask<String, Void, String>() {

    private fun downloadImage(): String {
        var json = ""
        try {
            val inputStream = URL(getFullUrl(image)).openStream()

            val s = java.util.Scanner(inputStream).useDelimiter("\\A")
            json = if (s.hasNext()) s.next() else ""

            inputStream.close()
            return json
        } catch (e: Exception) {
            Log.e(logTag, "Unable to download image from ${getFullUrl(image)}")
            e.printStackTrace()
        }
        return json
    }

}

SAVING IMAGES

class ImageLoader(private val context: Context) : JsonDownloadListener {

    private fun saveImage(image: ImageInfo, json: String) {
        try {
            val stream = OutputStreamWriter(context.openFileOutput(image.getFileName(), Context.MODE_PRIVATE))
            stream.write(json)
            stream.close()
        } catch (e: Exception) {
            Log.e(logTag, "Unable to save image")
            e.printStackTrace()
        }
        Log.d(logTag, "saved ${image.name} as ${image.getFileName()}")
        downloading.remove(image)
    }

}

LOADING IMAGES

private fun loadInputStream(fileName: String): InputStream {
    return if (context.getFileStreamPath(fileName).exists()) {
        FileInputStream(context.getFileStreamPath(fileName))
    } else {
        …
    }
}

PARSING JSON

package rak.pixellwp.cycling.jsonModels

import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import rak.pixellwp.cycling.models.Cycle

@JsonIgnoreProperties(ignoreUnknown = true)
data class ImageJson(val width: Int, val height: Int, val colors: List<ColorJson>, val cycles: List<Cycle>, val pixels: List<Int>){

    fun getParsedColors() : List<Int> {
        return colors.map { c ->  c.rgb}.toList()
    }
}

BUILDING A CLASS FROM JSON

class ColorCyclingImage(img: ImageJson) : PaletteImage {
    private val width = img.width
    private val height = img.height
    private var palette = Palette(colors = img.getParsedColors(), cycles = img.cycles)
    private val pixels = img.pixels.toList()
    private var optimizedPixels = optimizePixels(pixels)
    private var drawUnOptimized = true

    private val bitmap: Bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)

    init {
        createBitmap()
    }

OPTIMIZING PIXELS

class ColorCyclingImage(img: ImageJson) : PaletteImage {

    private fun optimizePixels(pixels: List<Int>) : List<Pixel> {
        return optPixels
    }
}

Given a list of pixels (list of int pointers to spots on the palette)

  • Return list of ONLY pixels that change their color
  • Don’t return pixels that belong to a cycle with a rate of 0
  • Include the Pixel’s x, y, and index

OPTIMIZING PIXELS

class ColorCyclingImage(img: ImageJson) : PaletteImage {

    private fun optimizePixels(pixels: List<Int>) : List<Pixel> {
        val optPixels = mutableListOf<Pixel>()
        val optColors = BooleanArray(pixels.size, { _ -> false}).toMutableList()

        palette.cycles
                .filter { it.rate != 0 }
                .flatMap { it.low..it.high }
                .forEach { optColors[it] = true }

        var j = 0
        for (y in 0 until height) {
            for (x in 0 until width) {
                //If this pixel references an animated color
                if (optColors[pixels[j]]){
                    optPixels.add(Pixel(x.toFloat(), y.toFloat(), j))
                }
                j++
            }
        }
        return optPixels
    }
}

ALL THE TIME IN THE WORLD DAY

class DaySeconds {
    private val maxMilliseconds = getMilliFromSeconds(getSecondsFromHour(24))
    private var timeInMillis: Long = 0

    fun getMinutes(): Int {
        return getMinutesFromSeconds(getSecondsFromMilli(timeInMillis)) % 60
    }
}

fun getMinutesFromSeconds(seconds: Int): Int {
    return seconds / 60
}

private fun getSecondsFromMilli(milli: Long) : Int{
    return (milli / 1000).toInt()
}

ALL THE TIME IN THE WORLD DAY

class DaySeconds {
    private val maxMilliseconds = getMilliFromSeconds(getSecondsFromHour(24))
    private var timeInMillis: Long = 0

    fun setTime(time: Long) {
    }
}
  • Set time in milliseconds
  • Ignore time over 24 hours

ALL THE TIME IN THE WORLD DAY

class DaySeconds {
    private val maxMilliseconds = getMilliFromSeconds(getSecondsFromHour(24))
    private var timeInMillis: Long = 0

    fun setTime(time: Long) {
        val adjustedTime = if (time > maxMilliseconds){
            time % maxMilliseconds
        } else {
            time
        }
        timeInMillis = adjustedTime
        Log.d("dayseconds", "set time from $time to ${get24HourFormattedString()}")
    }
}

REVIEW

  • Android Studio
  • Kotlin
  • Live Wallpaper App

contain

Palette Cycle

Creating a Live Wallpaper Android App with Kotlin in Intellij