~oppen/mork

4a8442c0abcb2260b91305b70a60a34fc1fd5591 — Öppen 29 days ago 3a923a8 trunk
boilerplate
39 files changed, 1552 insertions(+), 41 deletions(-)

M .idea/misc.xml
M app/build.gradle
M app/src/main/AndroidManifest.xml
A app/src/main/java/oppen/mork/Extensions.kt
D app/src/main/java/oppen/mork/MainActivity.kt
A app/src/main/java/oppen/mork/io/ImageIO.kt
A app/src/main/java/oppen/mork/ui/main/MainActivity.kt
A app/src/main/java/oppen/mork/ui/main/MainPresenter.kt
A app/src/main/java/oppen/mork/ui/main/MainView.kt
A app/src/main/java/oppen/mork/ui/views/TouchImageView.java
A app/src/main/res/drawable/shape_floating_rounded.xml
A app/src/main/res/drawable/shape_white_circle.xml
A app/src/main/res/drawable/toggle_brightness.xml
A app/src/main/res/drawable/toggle_brightness_off.xml
A app/src/main/res/drawable/toggle_brightness_on.xml
A app/src/main/res/drawable/toggle_contrast.xml
A app/src/main/res/drawable/toggle_contrast_off.xml
A app/src/main/res/drawable/toggle_contrast_on.xml
A app/src/main/res/drawable/toggle_exposure.xml
A app/src/main/res/drawable/toggle_exposure_off.xml
A app/src/main/res/drawable/toggle_exposure_on.xml
A app/src/main/res/drawable/toggle_grain.xml
A app/src/main/res/drawable/toggle_grain_off.xml
A app/src/main/res/drawable/toggle_grain_on.xml
A app/src/main/res/drawable/toggle_saturation.xml
A app/src/main/res/drawable/toggle_saturation_off.xml
A app/src/main/res/drawable/toggle_saturation_on.xml
A app/src/main/res/drawable/toggle_shadows.xml
A app/src/main/res/drawable/toggle_shadows_off.xml
A app/src/main/res/drawable/toggle_shadows_on.xml
A app/src/main/res/drawable/toggle_vibrance.xml
A app/src/main/res/drawable/toggle_vibrance_off.xml
A app/src/main/res/drawable/toggle_vibrance_on.xml
A app/src/main/res/drawable/toggle_vignette.xml
A app/src/main/res/drawable/toggle_vignette_off.xml
A app/src/main/res/drawable/toggle_vignette_on.xml
M app/src/main/res/layout/activity_main.xml
A app/src/main/res/layout/levels.xml
M app/src/main/res/values/colors.xml
M .idea/misc.xml => .idea/misc.xml +1 -1
@@ 1,6 1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
    <output url="file://$PROJECT_DIR$/build/classes" />
  </component>
  <component name="ProjectType">

M app/build.gradle => app/build.gradle +5 -0
@@ 1,6 1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 30


@@ 22,6 23,10 @@ android {
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_1_8.toString()
    }
}

dependencies {

M app/src/main/AndroidManifest.xml => app/src/main/AndroidManifest.xml +1 -1
@@ 9,7 9,7 @@
        android:roundIcon="@drawable/vector_app_icon"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
        <activity android:name=".ui.main.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />


A app/src/main/java/oppen/mork/Extensions.kt => app/src/main/java/oppen/mork/Extensions.kt +143 -0
@@ 0,0 1,143 @@
package oppen.mork

/*

    Mörk - 1bit Filters for Android
    Copyright (C) 2020  Öppenlab

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

 */

import android.animation.Animator
import android.content.Context
import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.CountDownTimer
import android.view.View
import android.view.animation.*


fun Context.versionName(): String = this.packageManager.getPackageInfo(packageName, 0).versionName

fun View.fadeOutRetainSpace(ms: Long){
    val view = this
    view.animate()
        .alpha(0f)
        .setDuration(ms)
        .setListener(object: Animator.AnimatorListener{
            override fun onAnimationRepeat(animation: Animator?) {}

            override fun onAnimationEnd(animation: Animator?) {
                view.visibility = View.INVISIBLE
                view.alpha = 1f
                view.animate().setListener(null)
            }

            override fun onAnimationCancel(animation: Animator?) {}

            override fun onAnimationStart(animation: Animator?) {

            }

        }).start()
}


fun View.fadeOut(ms: Long){
    val view = this
    view.animate()
        .alpha(0f)
        .setDuration(ms)
        .setListener(object: Animator.AnimatorListener{
            override fun onAnimationRepeat(animation: Animator?) {}

            override fun onAnimationEnd(animation: Animator?) {
                view.hide()
                view.alpha = 1f
                view.animate().setListener(null)
            }

            override fun onAnimationCancel(animation: Animator?) {}

            override fun onAnimationStart(animation: Animator?) {

            }

        }).start()
}

fun View.transitionIn(){
    val view = this

    view.show()
    val set = AnimationSet(true)
    val translate = TranslateAnimation(0f, 0f, -50f, 0f)
    translate.duration = 150
    translate.interpolator = DecelerateInterpolator()
    set.addAnimation(translate)

    val fadeIn = AlphaAnimation(0f, 1f)
    fadeIn.duration = 300
    fadeIn.interpolator = AccelerateInterpolator()
    set.addAnimation(fadeIn)

    view.startAnimation(set)
}

fun View.hide(){
    this.visibility = View.GONE
}

fun View.invisible(){
    this.visibility = View.INVISIBLE
}

fun View.show(){
    this.alpha = 1f
    this.visibility = View.VISIBLE
}

fun View.visible(visible: Boolean) = when {
    visible -> {
        this.show()
    }
    else -> {
        this.hide()
    }
}

fun String.toLabel(): String{
    val label = this.replace("_", " ")
    return label.split(" ").joinToString(" ") { it.capitalize() }
}

fun Context.isDarkThemeOn(): Boolean{
    return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
}

fun delay(ms: Long, action: () -> Unit){
    object: CountDownTimer(ms, ms/2) {
        override fun onFinish() = action.invoke()
        override fun onTick(millisUntilFinished: Long) = Unit
    }.start()
}

fun percentToValue(percent: Int, min: Float, max: Float): Float{
    return (max - min) * (percent / 100.0f) + min
}

fun valueToPercent(value: Float, min: Float, max: Float): Int{
    return (100 / ((max - min) / value) + min).toInt()
}
\ No newline at end of file

D app/src/main/java/oppen/mork/MainActivity.kt => app/src/main/java/oppen/mork/MainActivity.kt +0 -35
@@ 1,35 0,0 @@
package oppen.mork

/*

    Mörk - 1bit Filters for Android
    Copyright (C) 2020  Öppenlab

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

 */

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.WindowManager

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)

        setContentView(R.layout.activity_main)
    }
}
\ No newline at end of file

A app/src/main/java/oppen/mork/io/ImageIO.kt => app/src/main/java/oppen/mork/io/ImageIO.kt +215 -0
@@ 0,0 1,215 @@
package oppen.mork.io

/*

    Mörk - 1bit Filters for Android
    Copyright (C) 2020  Öppenlab

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

 */

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.bumptech.glide.Glide
import com.bumptech.glide.request.FutureTarget
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream

class ImageIO(private val context: Context) {

    companion object{
        const val LIVE_PREVIEW_FILENAME = "live_preview.png"
        const val LIVE_THUMBNAIL_SOURCE_FILENAME = "live_thumbnail_source.png"
    }

    var futureTarget: FutureTarget<Bitmap>? = null

    fun getAndResize(uri: Uri, width: Int, onComplete: (bitmap: Bitmap) -> Unit){
        GlobalScope.launch {
            val bounds = BitmapFactory.Options()
            bounds.inJustDecodeBounds = true
            val inputStream = context.contentResolver.openInputStream(uri)
            BitmapFactory.decodeStream(inputStream, null, bounds)

            val sourceWidth = bounds.outWidth
            val sourceHeight = bounds.outHeight

            var previewWidth = sourceWidth
            var previewHeight = sourceHeight

            if (previewWidth > width) {
                val ratio = previewWidth / width
                previewWidth = width
                previewHeight /= ratio
            }

            val sourceTarget: FutureTarget<Bitmap> = Glide.with(context)
                .asBitmap()
                .load(uri)
                .submit(previewWidth, previewHeight)

            onComplete(sourceTarget.get())
        }
    }

    fun get(uri: Uri, onLoaded: (bitmap: Bitmap?) -> Unit){
        GlobalScope.launch {
            futureTarget = Glide.with(context)
                .asBitmap()
                .load(uri)
                .submit()

            @Suppress("BlockingMethodInNonBlockingContext")//We're in a coroutine
            val bitmap = futureTarget?.get()
            onLoaded.invoke(bitmap)
        }
    }

    fun recycleLast(){
        if(futureTarget != null) Glide.with(context).clear(futureTarget)
    }

    /**
     *
     * Just saves bitmap to internal cache directory
     * @param bitmap - not recycled by this method
     * @param filename
     * @param onComplete
     *
     */
    fun put(bitmap: Bitmap?, filename: String, onComplete: (uri: Uri) -> Unit){
        val cacheFile = File(ContextCompat.getDataDir(context), filename)
        if(cacheFile.exists()) cacheFile.delete()

        FileOutputStream(cacheFile).also {
            bitmap?.compress(Bitmap.CompressFormat.PNG, 90, it)
            it.close()
            onComplete(cacheFile.toUri())
        }
    }

    /**
     *
     * Convenience overload
     * @see calculate
     *
     */
    fun buildAndStoreLivePreview(uri: Uri, width: Int, onPreview: (uri: Uri) -> Unit){
        calculate(uri, width, LIVE_PREVIEW_FILENAME, onPreview)
    }

    /**
     *
     * Convenience overload
     * @see calculate
     *
     */
    fun buildAndStoreLiveThumbnail(uri: Uri, width: Int, onPreview: (uri: Uri) -> Unit){
        calculate(uri, width, LIVE_THUMBNAIL_SOURCE_FILENAME, onPreview)
    }

    /**
     *
     * Convenience overload
     * @see calculate
     *
     */
    fun build(uri: Uri, width: Int, filename: String, onComplete: (uri: Uri) -> Unit){
        calculate(uri, width, filename, onComplete)
    }

    /**
     *
     * Generates a smaller (if needed) image from the source image file.
     * @param uri Storage Access Framework content: uri - NOT a file: uri
     * @param width Max width of the preview image, likely the screen width
     * @param onComplete Lambda callback with resulting preview image Uri
     */
    private fun calculate(uri: Uri, width: Int, filename: String, onComplete: (uri: Uri) -> Unit){
        GlobalScope.launch {
            val bounds = BitmapFactory.Options()
            bounds.inJustDecodeBounds = true
            val inputStream = context.contentResolver.openInputStream(uri)
            BitmapFactory.decodeStream(inputStream, null, bounds)

            val sourceWidth = bounds.outWidth
            val sourceHeight = bounds.outHeight

            var previewWidth = sourceWidth
            var previewHeight = sourceHeight

            if (previewWidth > width) {
                val ratio = previewWidth / width
                previewWidth = width
                previewHeight /= ratio

                //Arbitrary value, only reduce image quality for (probably/possibly) full screen width images
                /*
                    if(width > 800) {
                        previewWidth = (previewWidth / 1.5).toInt()
                        previewHeight = (previewHeight / 1.5).toInt()
                    }
                */
            }

            generate(uri,previewWidth, previewHeight, filename){ outputUri ->
                onComplete(outputUri)
            }
        }
    }

    private fun generate(source: Uri, targetWidth: Int, targetHeight: Int, targetFilename: String, onComplete: (uri: Uri) -> Unit){
        val sourceTarget: FutureTarget<Bitmap> = Glide.with(context)
            .asBitmap()
            .load(source)
            .submit(targetWidth, targetHeight)

        val sourceBitmap = sourceTarget.get()

        val outputFile = File(context.cacheDir, targetFilename)
        if (outputFile.exists()) outputFile.delete()
        FileOutputStream(outputFile).also {
            sourceBitmap.compress(Bitmap.CompressFormat.PNG, 90, it)
            it.close()
        }

        Glide.with(context).clear(sourceTarget)//Recycle

        onComplete(outputFile.toUri())
    }

    /**
     *
     * Save bitmap using Storage Access Framework Uri
     * @param bitmap
     * @param uri - must be a SAF Uri
     * @param onComplete
     */
    fun publicExport(bitmap: Bitmap?, uri: Uri, onComplete: (uri: Uri) -> Unit) {
        context.contentResolver.openFileDescriptor(uri, "w")?.use {
            FileOutputStream(it.fileDescriptor).use { outputStream ->
                bitmap?.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
            }
            bitmap?.recycle()
            onComplete(uri)
        }
    }
}
\ No newline at end of file

A app/src/main/java/oppen/mork/ui/main/MainActivity.kt => app/src/main/java/oppen/mork/ui/main/MainActivity.kt +98 -0
@@ 0,0 1,98 @@
package oppen.mork.ui.main

/*

    Mörk - 1bit Filters for Android
    Copyright (C) 2020  Öppenlab

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

 */

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.WindowManager
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.welcome_layout.*
import oppen.mork.R
import oppen.mork.hide
import oppen.mork.show
import oppen.mork.versionName
import java.io.File

const val CHOOSE_IMAGE_REQUEST = 1000
const val CREATE_FILE_REQUEST = 1001

class MainActivity : AppCompatActivity(), MainView {

    private lateinit var presenter: MainPresenter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)

        setContentView(R.layout.activity_main)

        version_label.text = versionName()

        val cacheFile = File(ContextCompat.getDataDir(this), "cache_image.png")
        if(cacheFile.exists()) {
            Glide.with(this)
                .load(cacheFile)
                .into(last_export_image)
        }else{
            last_export_image.hide()
        }

        presenter = MainPresenter()

        empty_layout.setOnClickListener {
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
            intent.addCategory(Intent.CATEGORY_OPENABLE)
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
            intent.type = "image/*"
            startActivityForResult(intent, CHOOSE_IMAGE_REQUEST)
        }
    }

    private fun uiLog(message: String){
        ui_log.text = message
    }

    private fun loadImage(uri: Uri){
        loading_layout.show()
        (preview_image).setImageURI(null)
        val cacheDeleted = cacheDir.deleteRecursively()//todo - this behaviour should be elsewhere, new image so delete cache directory
        Log.d("FilterActivity", "cacheDeleted: $cacheDeleted")
        presenter.chooseImageResult(uri, resources.displayMetrics.widthPixels)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(resultCode == RESULT_OK && requestCode == CHOOSE_IMAGE_REQUEST){
            if(data?.data != null) loadImage(data.data!!)
        }else if(resultCode == RESULT_OK && requestCode == CREATE_FILE_REQUEST){
            if(data?.data != null) {
                loading_layout.show()
                //presenter.exportImage(data.data!!)
            }
        }
    }
}
\ No newline at end of file

A app/src/main/java/oppen/mork/ui/main/MainPresenter.kt => app/src/main/java/oppen/mork/ui/main/MainPresenter.kt +9 -0
@@ 0,0 1,9 @@
package oppen.mork.ui.main

import android.net.Uri

class MainPresenter {
    fun chooseImageResult(uri: Uri, widthPixels: Int) {

    }
}
\ No newline at end of file

A app/src/main/java/oppen/mork/ui/main/MainView.kt => app/src/main/java/oppen/mork/ui/main/MainView.kt +4 -0
@@ 0,0 1,4 @@
package oppen.mork.ui.main

interface MainView {
}
\ No newline at end of file

A app/src/main/java/oppen/mork/ui/views/TouchImageView.java => app/src/main/java/oppen/mork/ui/views/TouchImageView.java +305 -0
@@ 0,0 1,305 @@
package oppen.mork.ui.views;

import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;

/**
 *
 *  From SO: https://stackoverflow.com/a/54474590/7641428
 *
 *  todo - Rewrite in Kotlin at some point, possibly, maybe, never
 *
 */
public class TouchImageView extends androidx.appcompat.widget.AppCompatImageView implements GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener {

    Matrix matrix;

    // We can be in one of these 3 states
    static final int NONE = 0;
    static final int DRAG = 1;
    static final int ZOOM = 2;
    int mode = NONE;

    // Remember some things for zooming
    PointF last = new PointF();
    PointF start = new PointF();
    float minScale = 0.9f;
    float maxScale = 3f;
    float[] m;

    int viewWidth, viewHeight;
    static final int CLICK = 3;
    float saveScale = 1f;
    protected float origWidth, origHeight;
    int oldMeasuredWidth, oldMeasuredHeight;

    ScaleGestureDetector mScaleDetector;

    Context context;

    public TouchImageView(Context context) {
        super(context);
        sharedConstructing(context);
    }

    public TouchImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        sharedConstructing(context);
    }

    GestureDetector mGestureDetector;

    private void sharedConstructing(Context context) {
        super.setClickable(true);
        this.context = context;
        mGestureDetector = new GestureDetector(context, this);
        mGestureDetector.setOnDoubleTapListener(this);

        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
        matrix = new Matrix();
        m = new float[9];
        setImageMatrix(matrix);
        setScaleType(ScaleType.MATRIX);

        setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {
                mScaleDetector.onTouchEvent(event);
                mGestureDetector.onTouchEvent(event);

                PointF curr = new PointF(event.getX(), event.getY());

                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        last.set(curr);
                        start.set(last);
                        mode = DRAG;
                        break;

                    case MotionEvent.ACTION_MOVE:
                        if (mode == DRAG) {
                            float deltaX = curr.x - last.x;
                            float deltaY = curr.y - last.y;
                            float fixTransX = getFixDragTrans(deltaX, viewWidth,
                                    origWidth * saveScale);
                            float fixTransY = getFixDragTrans(deltaY, viewHeight,
                                    origHeight * saveScale);
                            matrix.postTranslate(fixTransX, fixTransY);
                            fixTrans();
                            last.set(curr.x, curr.y);
                        }
                        break;

                    case MotionEvent.ACTION_UP:
                        mode = NONE;
                        int xDiff = (int) Math.abs(curr.x - start.x);
                        int yDiff = (int) Math.abs(curr.y - start.y);
                        if (xDiff < CLICK && yDiff < CLICK)
                            performClick();
                        break;

                    case MotionEvent.ACTION_POINTER_UP:
                        mode = NONE;
                        break;
                }

                setImageMatrix(matrix);
                invalidate();
                return true; // indicate event was handled
            }

        });

        invalidate();
    }

    public void setMaxZoom(float x) {
        maxScale = x;
    }

    @Override
    public boolean onSingleTapConfirmed(MotionEvent e) {
        return false;
    }

    public void reset(){
        saveScale = 1f;
        fixTrans();
    }

    @Override
    public boolean onDoubleTap(MotionEvent e) {
        // Double tap is detected
        Log.i("MAIN_TAG", "Double tap behaviour disabled");
        /*
            float origScale = saveScale;
            float mScaleFactor;

            if (saveScale == maxScale) {
                saveScale = minScale;
                mScaleFactor = minScale / origScale;
            } else {
                saveScale = maxScale;
                mScaleFactor = maxScale / origScale;
            }

            matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2f, viewHeight / 2f);

            fixTrans();
        */
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onDown(MotionEvent e) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        return false;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScaleBegin(ScaleGestureDetector detector) {
            mode = ZOOM;
            return true;
        }

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            float mScaleFactor = detector.getScaleFactor();
            float origScale = saveScale;
            saveScale *= mScaleFactor;
            if (saveScale > maxScale) {
                saveScale = maxScale;
                mScaleFactor = maxScale / origScale;
            } else if (saveScale < minScale) {
                saveScale = minScale;
                mScaleFactor = minScale / origScale;
            }

            if (origWidth * saveScale <= viewWidth || origHeight * saveScale <= viewHeight) {
                matrix.postScale(mScaleFactor, mScaleFactor, viewWidth / 2f, viewHeight / 2f);
            }else {
                matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY());
            }

            fixTrans();
            return true;
        }
    }

    void fixTrans() {
        matrix.getValues(m);
        float transX = m[Matrix.MTRANS_X];
        float transY = m[Matrix.MTRANS_Y];

        float fixTransX = getFixTrans(transX, viewWidth, origWidth * saveScale);
        float fixTransY = getFixTrans(transY, viewHeight, origHeight * saveScale);

        if (fixTransX != 0 || fixTransY != 0) matrix.postTranslate(fixTransX, fixTransY);
    }

    float getFixTrans(float trans, float viewSize, float contentSize) {
        float minTrans, maxTrans;

        if (contentSize <= viewSize) {
            minTrans = 0;
            maxTrans = viewSize - contentSize;
        } else {
            minTrans = viewSize - contentSize;
            maxTrans = 0;
        }

        if (trans < minTrans) return -trans + minTrans;
        if (trans > maxTrans) return -trans + maxTrans;
        return 0;
    }

    float getFixDragTrans(float delta, float viewSize, float contentSize) {
        if (contentSize <= viewSize) {
            return 0;
        }
        return delta;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        viewWidth = MeasureSpec.getSize(widthMeasureSpec);
        viewHeight = MeasureSpec.getSize(heightMeasureSpec);

        //
        // Rescales image on rotation
        //
        if (oldMeasuredHeight == viewWidth && oldMeasuredHeight == viewHeight || viewWidth == 0 || viewHeight == 0) return;
        oldMeasuredHeight = viewHeight;
        oldMeasuredWidth = viewWidth;

        if (saveScale == 1) {
            // Fit to screen.
            float scale;

            Drawable drawable = getDrawable();
            if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) return;
            int bmWidth = drawable.getIntrinsicWidth();
            int bmHeight = drawable.getIntrinsicHeight();

            Log.d("bmSize", "bmWidth: " + bmWidth + " bmHeight : " + bmHeight);

            float scaleX = (float) viewWidth / (float) bmWidth;
            float scaleY = (float) viewHeight / (float) bmHeight;
            scale = Math.min(scaleX, scaleY);
            matrix.setScale(scale, scale);

            // Center the image
            float redundantYSpace = (float) viewHeight - (scale * (float) bmHeight);
            float redundantXSpace = (float) viewWidth - (scale * (float) bmWidth);
            redundantYSpace /= (float) 2;
            redundantXSpace /= (float) 2;

            matrix.postTranslate(redundantXSpace, redundantYSpace);

            origWidth = viewWidth - 2 * redundantXSpace;
            origHeight = viewHeight - 2 * redundantYSpace;
            setImageMatrix(matrix);
        }
        fixTrans();
    }
}

A app/src/main/res/drawable/shape_floating_rounded.xml => app/src/main/res/drawable/shape_floating_rounded.xml +6 -0
@@ 0,0 1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="?attr/colorSurface"/>
    <corners android:radius="16dp"/>
    <padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
</shape>
\ No newline at end of file

A app/src/main/res/drawable/shape_white_circle.xml => app/src/main/res/drawable/shape_white_circle.xml +12 -0
@@ 0,0 1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">

    <solid
        android:color="?attr/colorSurface"/>

    <size
        android:width="120dp"
        android:height="120dp"/>
</shape>
\ No newline at end of file

A app/src/main/res/drawable/toggle_brightness.xml => app/src/main/res/drawable/toggle_brightness.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/toggle_brightness_on"
        android:state_checked="true" />
    <item
        android:drawable="@drawable/toggle_brightness_off"
        android:state_checked="false"/>
</selector>
\ No newline at end of file

A app/src/main/res/drawable/toggle_brightness_off.xml => app/src/main/res/drawable/toggle_brightness_off.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="#00ff0000"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_brightness"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_brightness_on.xml => app/src/main/res/drawable/toggle_brightness_on.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/toggle_on"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_brightness"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_contrast.xml => app/src/main/res/drawable/toggle_contrast.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/toggle_contrast_on"
        android:state_checked="true" />
    <item
        android:drawable="@drawable/toggle_contrast_off"
        android:state_checked="false"/>
</selector>
\ No newline at end of file

A app/src/main/res/drawable/toggle_contrast_off.xml => app/src/main/res/drawable/toggle_contrast_off.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="#00ff0000"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_contrast"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_contrast_on.xml => app/src/main/res/drawable/toggle_contrast_on.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/toggle_on"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_contrast"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_exposure.xml => app/src/main/res/drawable/toggle_exposure.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/toggle_exposure_on"
        android:state_checked="true" />
    <item
        android:drawable="@drawable/toggle_exposure_off"
        android:state_checked="false"/>
</selector>
\ No newline at end of file

A app/src/main/res/drawable/toggle_exposure_off.xml => app/src/main/res/drawable/toggle_exposure_off.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="#00ff0000"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_exposure"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_exposure_on.xml => app/src/main/res/drawable/toggle_exposure_on.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/toggle_on"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_exposure"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_grain.xml => app/src/main/res/drawable/toggle_grain.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/toggle_grain_on"
        android:state_checked="true" />
    <item
        android:drawable="@drawable/toggle_grain_off"
        android:state_checked="false"/>
</selector>
\ No newline at end of file

A app/src/main/res/drawable/toggle_grain_off.xml => app/src/main/res/drawable/toggle_grain_off.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="#00ff0000"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_grain"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_grain_on.xml => app/src/main/res/drawable/toggle_grain_on.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/toggle_on"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_grain"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_saturation.xml => app/src/main/res/drawable/toggle_saturation.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/toggle_saturation_on"
        android:state_checked="true" />
    <item
        android:drawable="@drawable/toggle_saturation_off"
        android:state_checked="false"/>
</selector>
\ No newline at end of file

A app/src/main/res/drawable/toggle_saturation_off.xml => app/src/main/res/drawable/toggle_saturation_off.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="#00ff0000"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_saturation"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_saturation_on.xml => app/src/main/res/drawable/toggle_saturation_on.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/toggle_on"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_saturation"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_shadows.xml => app/src/main/res/drawable/toggle_shadows.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/toggle_shadows_on"
        android:state_checked="true" />
    <item
        android:drawable="@drawable/toggle_shadows_off"
        android:state_checked="false"/>
</selector>
\ No newline at end of file

A app/src/main/res/drawable/toggle_shadows_off.xml => app/src/main/res/drawable/toggle_shadows_off.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="#00ff0000"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_shadows"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_shadows_on.xml => app/src/main/res/drawable/toggle_shadows_on.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/toggle_on"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_shadows"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_vibrance.xml => app/src/main/res/drawable/toggle_vibrance.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/toggle_vibrance_on"
        android:state_checked="true" />
    <item
        android:drawable="@drawable/toggle_vibrance_off"
        android:state_checked="false"/>
</selector>
\ No newline at end of file

A app/src/main/res/drawable/toggle_vibrance_off.xml => app/src/main/res/drawable/toggle_vibrance_off.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="#00ff0000"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_vibrance"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_vibrance_on.xml => app/src/main/res/drawable/toggle_vibrance_on.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/toggle_on"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_vibrance"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_vignette.xml => app/src/main/res/drawable/toggle_vignette.xml +8 -0
@@ 0,0 1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/toggle_vignette_on"
        android:state_checked="true" />
    <item
        android:drawable="@drawable/toggle_vignette_off"
        android:state_checked="false"/>
</selector>
\ No newline at end of file

A app/src/main/res/drawable/toggle_vignette_off.xml => app/src/main/res/drawable/toggle_vignette_off.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="#00ff0000"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_vignette"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

A app/src/main/res/drawable/toggle_vignette_on.xml => app/src/main/res/drawable/toggle_vignette_on.xml +16 -0
@@ 0,0 1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
    <item>
        <shape android:shape="oval">
            <solid android:color="@color/toggle_on"/>
            <size
                android:width="@dimen/levels_toggle_container_diam"
                android:height="@dimen/levels_toggle_container_diam"/>
        </shape>
    </item>
    <item
        android:drawable="@drawable/vector_vignette"
        android:gravity="center"
        android:width="@dimen/leveks_toggle_icon_diam"
        android:height="@dimen/leveks_toggle_icon_diam"/>
</layer-list>
\ No newline at end of file

M app/src/main/res/layout/activity_main.xml => app/src/main/res/layout/activity_main.xml +186 -1
@@ 6,9 6,194 @@
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">
    tools:context=".ui.main.MainActivity">

    <!-- Empty layout -->
    <include layout="@layout/welcome_layout" />

    <!-- Live layout -->
    <RelativeLayout
        android:id="@+id/filter_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:visibility="gone">

        <oppen.mork.ui.views.TouchImageView
            android:id="@+id/preview_image"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="fitCenter" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/ui_log"
            android:paddingTop="@dimen/def_padding_big"
            android:paddingLeft="@dimen/def_padding"
            android:paddingRight="@dimen/def_padding"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textAlignment="viewStart"
            android:layout_toStartOf="@+id/overflow_button"
            android:shadowColor="@color/floating"
            android:shadowDx="1"
            android:shadowDy="1"
            android:shadowRadius="2"
            android:textSize="16sp"
            android:text="Lätt"/>

        <androidx.appcompat.widget.AppCompatImageButton
            android:id="@+id/overflow_button"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_alignParentTop="true"
            android:layout_alignParentEnd="true"
            android:layout_marginTop="@dimen/def_padding_big"
            android:layout_marginEnd="@dimen/def_padding"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:src="@drawable/vector_overflow" />

        <!-- Floating filter sliders -->
        <include layout="@layout/levels" />

        <!-- Footer buttons -->
        <LinearLayout
            android:id="@+id/footer_buttons_layout"
            android:layout_width="match_parent"
            android:layout_height="@dimen/button_touch_area"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="@dimen/def_padding"
            android:orientation="horizontal"
            android:layout_centerHorizontal="true"
            android:weightSum="4">

            <RelativeLayout
                android:id="@+id/new_image_button"
                android:layout_width="@dimen/button_touch_area"
                android:layout_height="@dimen/button_touch_area"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:clickable="true"
                android:focusable="true"
                android:layout_weight="1">

                <View
                    android:layout_width="@dimen/background_circle"
                    android:layout_height="@dimen/background_circle"
                    android:alpha="0.5"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:background="@drawable/shape_white_circle" />

                <androidx.appcompat.widget.AppCompatImageView
                    android:layout_width="24dp"
                    android:layout_height="24dp"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:src="@drawable/vector_open" />

            </RelativeLayout>

            <RelativeLayout
                android:id="@+id/filter_category_button"
                android:layout_width="@dimen/button_touch_area"
                android:layout_height="@dimen/button_touch_area"
                android:clickable="true"
                android:focusable="true"
                android:layout_weight="1">

                <View
                    android:layout_width="@dimen/background_circle"
                    android:layout_height="@dimen/background_circle"
                    android:alpha="0.5"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:background="@drawable/shape_white_circle" />

                <androidx.appcompat.widget.AppCompatImageView
                    android:layout_width="24dp"
                    android:layout_height="24dp"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:background="?attr/selectableItemBackgroundBorderless"
                    android:src="@drawable/vector_filters" />

            </RelativeLayout>

            <RelativeLayout
                android:id="@+id/image_tone_button"
                android:layout_width="@dimen/button_touch_area"
                android:layout_height="@dimen/button_touch_area"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:clickable="true"
                android:focusable="true"
                android:layout_weight="1">

                <View
                    android:layout_width="@dimen/background_circle"
                    android:layout_height="@dimen/background_circle"
                    android:alpha="0.5"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:background="@drawable/shape_white_circle" />

                <androidx.appcompat.widget.AppCompatImageView
                    android:layout_width="24dp"
                    android:layout_height="24dp"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:background="?attr/selectableItemBackgroundBorderless"
                    android:src="@drawable/vector_levels" />

            </RelativeLayout>

            <RelativeLayout
                android:id="@+id/export_button"
                android:layout_width="@dimen/button_touch_area"
                android:layout_height="@dimen/button_touch_area"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:clickable="true"
                android:focusable="true"
                android:gravity="center_horizontal"
                android:layout_weight="1">

                <View
                    android:layout_width="@dimen/background_circle"
                    android:layout_height="@dimen/background_circle"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:alpha="0.5"
                    android:background="@drawable/shape_white_circle" />

                <androidx.appcompat.widget.AppCompatImageView
                    android:layout_width="24dp"
                    android:layout_height="24dp"
                    android:layout_centerVertical="true"
                    android:layout_centerHorizontal="true"
                    android:src="@drawable/vector_export" />


            </RelativeLayout>


        </LinearLayout>

    </RelativeLayout>

    <!-- Loading layout -->
    <RelativeLayout
        android:id="@+id/loading_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_centerInParent="true"
        android:background="#66000000"
        android:visibility="gone">

        <ProgressBar
            android:layout_width="@dimen/loading_animation_diam"
            android:layout_height="@dimen/loading_animation_diam"
            android:layout_centerInParent="true"
            android:indeterminate="true"
            android:indeterminateTint="#ffffff" />

    </RelativeLayout>

</RelativeLayout>
\ No newline at end of file

A app/src/main/res/layout/levels.xml => app/src/main/res/layout/levels.xml +241 -0
@@ 0,0 1,241 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/floating_slider_layout"
    android:layout_width="@dimen/slider_window_width"
    android:layout_height="wrap_content"
    android:visibility="gone"
    tools:visibility="visible"
    android:orientation="vertical"
    android:clickable="true"
    android:focusable="true"
    android:layout_centerInParent="true"
    android:background="@drawable/shape_floating_rounded"
    android:padding="@dimen/def_padding">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="@dimen/def_padding_small">

        <ImageButton
            android:id="@+id/docked_expand_button"
            android:layout_width="@dimen/levels_toggle_container_diam"
            android:layout_height="@dimen/levels_toggle_container_diam"
            android:background="?attr/selectableItemBackgroundBorderless"
            android:layout_centerVertical="true"
            android:src="@drawable/vector_show" />

        <androidx.appcompat.widget.AppCompatTextView
            android:id="@+id/levels_label"
            android:layout_centerVertical="true"
            android:layout_centerInParent="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:maxLines="1"
            tools:text="Brightness: 99"
            style="@style/floating_text_view"
            android:ellipsize="end"
            tools:ignore="RelativeOverlap" />


        <androidx.appcompat.widget.AppCompatImageView
            android:id="@+id/float_icon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
           android:layout_alignParentEnd="true"
            android:src="@drawable/vector_move" />
    </RelativeLayout>

    <androidx.appcompat.widget.AppCompatTextView
        android:id="@+id/no_controls_label"
        android:visibility="gone"
        android:layout_gravity="center_horizontal"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="No controls active, turn on in settings"/>

    <LinearLayout
        android:id="@+id/controls_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <RelativeLayout
            android:id="@+id/brightness"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/levels_spacing">

        <androidx.appcompat.widget.AppCompatToggleButton
            android:id="@+id/brightness_toggle"
            android:layout_width="@dimen/levels_toggle_container_diam"
            android:layout_height="@dimen/levels_toggle_container_diam"
            android:layout_centerVertical="true"
            android:button="@drawable/toggle_brightness"
            android:background="@null"/>

        <androidx.appcompat.widget.AppCompatSeekBar
            android:id="@+id/brightness_slider"
            android:layout_width="@dimen/slider_width"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:progress="50"
            android:layout_toRightOf="@+id/brightness_toggle"
            android:thumbTint="@color/white"
            android:padding="8dp" />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/contrast"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/levels_spacing">

            <androidx.appcompat.widget.AppCompatToggleButton
                android:id="@+id/contrast_toggle"
                android:layout_width="@dimen/levels_toggle_container_diam"
                android:layout_height="@dimen/levels_toggle_container_diam"
                android:layout_centerVertical="true"
                android:button="@drawable/toggle_contrast"
                android:background="@null"/>

            <androidx.appcompat.widget.AppCompatSeekBar
                android:id="@+id/contrast_slider"
                android:layout_width="@dimen/slider_width"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:progress="50"
                android:layout_toRightOf="@+id/contrast_toggle"
                android:thumbTint="@color/white"
                android:padding="8dp" />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/shadows"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/levels_spacing">

        <androidx.appcompat.widget.AppCompatToggleButton
            android:id="@+id/shadows_toggle"
            android:layout_width="@dimen/levels_toggle_container_diam"
            android:layout_height="@dimen/levels_toggle_container_diam"
            android:layout_centerVertical="true"
            android:button="@drawable/toggle_shadows"
            android:background="@null"/>

        <androidx.appcompat.widget.AppCompatSeekBar
            android:id="@+id/shadows_slider"
            android:layout_width="@dimen/slider_width"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:progress="50"
            android:layout_toRightOf="@+id/shadows_toggle"
            android:thumbTint="@color/white"
            android:padding="8dp" />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/saturation"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/levels_spacing">

            <androidx.appcompat.widget.AppCompatToggleButton
                android:id="@+id/saturation_toggle"
                android:layout_width="@dimen/levels_toggle_container_diam"
                android:layout_height="@dimen/levels_toggle_container_diam"
                android:layout_centerVertical="true"
                android:button="@drawable/toggle_saturation"
                android:background="@null"/>

            <androidx.appcompat.widget.AppCompatSeekBar
                android:id="@+id/saturation_slider"
                android:layout_width="@dimen/slider_width"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_toRightOf="@+id/saturation_toggle"
                android:progress="50"
                android:thumbTint="@color/white"
                android:padding="8dp" />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/exposure"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/levels_spacing">

            <androidx.appcompat.widget.AppCompatToggleButton
                android:id="@+id/exposure_toggle"
                android:layout_width="@dimen/levels_toggle_container_diam"
                android:layout_height="@dimen/levels_toggle_container_diam"
                android:layout_centerVertical="true"
                android:button="@drawable/toggle_exposure"
                android:background="@null"/>

            <androidx.appcompat.widget.AppCompatSeekBar
                android:id="@+id/exposure_slider"
                android:layout_width="@dimen/slider_width"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:layout_toRightOf="@+id/exposure_toggle"
                android:progress="50"
                android:thumbTint="@color/white"
                android:padding="8dp" />

        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/grain"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/levels_spacing">

            <androidx.appcompat.widget.AppCompatToggleButton
                android:id="@+id/grain_toggle"
                android:layout_width="@dimen/levels_toggle_container_diam"
                android:layout_height="@dimen/levels_toggle_container_diam"
                android:layout_centerVertical="true"
                android:button="@drawable/toggle_grain"
                android:background="@null"/>

        <androidx.appcompat.widget.AppCompatSeekBar
            android:id="@+id/grain_slider"
            android:layout_width="@dimen/slider_width"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:progress="50"
            android:layout_toRightOf="@+id/grain_toggle"
            android:thumbTint="@color/white"
            android:padding="8dp" />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/vignette"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/levels_spacing">

            <androidx.appcompat.widget.AppCompatToggleButton
                android:id="@+id/vignette_toggle"
                android:layout_width="@dimen/levels_toggle_container_diam"
                android:layout_height="@dimen/levels_toggle_container_diam"
                android:layout_centerVertical="true"
                android:button="@drawable/toggle_vignette"
                android:background="@null"/>

            <androidx.appcompat.widget.AppCompatSeekBar
                android:id="@+id/vignette_slider"
                android:layout_width="@dimen/slider_width"
                android:layout_height="wrap_content"
                android:layout_centerVertical="true"
                android:progress="50"
                android:layout_toRightOf="@+id/vignette_toggle"
                android:thumbTint="@color/white"
                android:padding="8dp" />
        </RelativeLayout>
    </LinearLayout>
</LinearLayout>
\ No newline at end of file

M app/src/main/res/values/colors.xml => app/src/main/res/values/colors.xml +6 -3
@@ 1,6 1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#6200EE</color>
    <color name="colorPrimaryDark">#3700B3</color>
    <color name="colorAccent">#03DAC5</color>
    <color name="colorPrimary">#585858</color>
    <color name="colorPrimaryDark">#000000</color>
    <color name="colorAccent">#529CF6</color>
    <color name="white">#FFFFFF</color>
    <color name="floating">#BF000000</color>
    <color name="toggle_on">#529CF6</color>
</resources>
\ No newline at end of file