Custom color filter for reader (#434)

* [WIP] Custom color filter for reader

* Improvements

* temp image to prevent build error

* Shift all the bits

* Some improvements. Removed DiscreteSeekBar

* Improvements

* API 16 + fixes

* Reduced lag. Fixed brightness value being reset to 0

* Small fixes
This commit is contained in:
Bram van de Kerkhof
2016-09-21 21:26:08 +02:00
committed by inorichi
parent 58a2f7a874
commit 8be67a4431
20 changed files with 746 additions and 51 deletions

View File

@ -26,6 +26,10 @@ class PreferenceKeys(context: Context) {
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
val colorFilter = context.getString(R.string.pref_color_filter_key)
val colorFilterValue = context.getString(R.string.pref_color_filter_value_key)
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)

View File

@ -52,6 +52,10 @@ class PreferencesHelper(context: Context) {
fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false)
fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0)
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
fun imageScaleType() = rxPrefs.getInteger(keys.imageScaleType, 1)

View File

@ -38,10 +38,12 @@ import me.zhanghai.android.systemuihelper.SystemUiHelper
import me.zhanghai.android.systemuihelper.SystemUiHelper.*
import nucleus.factory.RequiresPresenter
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
import java.util.concurrent.TimeUnit
@RequiresPresenter(ReaderPresenter::class)
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
@ -70,6 +72,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private var customBrightnessSubscription: Subscription? = null
private var customFilterColorSubscription: Subscription? = null
var readerTheme: Int = 0
private set
@ -140,6 +144,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings")
R.id.action_custom_filter -> ReaderCustomFilterDialog().show(supportFragmentManager, "filter")
else -> return super.onOptionsItemSelected(item)
}
return true
@ -354,9 +359,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
reader_menu_bottom.setOnTouchListener { v, event -> true }
page_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
gotoPageInCurrentChapter(progress)
gotoPageInCurrentChapter(value)
}
}
})
@ -378,6 +383,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
subscriptions += preferences.customBrightness().asObservable()
.subscribe { setCustomBrightness(it) }
subscriptions += preferences.colorFilter().asObservable()
.subscribe { setColorFilter(it) }
subscriptions += preferences.readerTheme().asObservable()
.distinctUntilChanged()
.subscribe { applyTheme(it) }
@ -424,6 +432,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private fun setCustomBrightness(enabled: Boolean) {
if (enabled) {
customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
.sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe { setCustomBrightnessValue(it) }
subscriptions.add(customBrightnessSubscription)
@ -433,6 +442,19 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
}
}
private fun setColorFilter(enabled: Boolean) {
if (enabled) {
customFilterColorSubscription = preferences.colorFilterValue().asObservable()
.sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe { setColorFilterValue(it) }
subscriptions.add(customFilterColorSubscription)
} else {
customFilterColorSubscription?.let { subscriptions.remove(it) }
color_overlay.visibility = View.GONE
}
}
/**
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
@ -459,6 +481,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
}
}
private fun setColorFilterValue(value: Int) {
color_overlay.visibility = View.VISIBLE
color_overlay.setBackgroundColor(value)
}
private fun applyTheme(theme: Int) {
readerTheme = theme
val rootView = window.decorView.rootView

View File

@ -0,0 +1,329 @@
package eu.kanade.tachiyomi.ui.reader
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.support.annotation.ColorInt
import android.support.v4.app.DialogFragment
import android.view.View
import android.widget.SeekBar
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import kotlinx.android.synthetic.main.dialog_reader_custom_filter.view.*
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
/**
* Custom dialog which can be used to set overlay value's
*/
class ReaderCustomFilterDialog : DialogFragment() {
companion object {
/** Integer mask of alpha value **/
private const val ALPHA_MASK: Long = 0xFF000000
/** Integer mask of red value **/
private const val RED_MASK: Long = 0x00FF0000
/** Integer mask of green value **/
private const val GREEN_MASK: Long = 0x0000FF00
/** Integer mask of blue value **/
private const val BLUE_MASK: Long = 0x000000FF
}
/**
* Provides operations to manage preferences
*/
private val preferences by injectLazy<PreferencesHelper>()
/**
* Subscription used for filter overlay
*/
private lateinit var subscriptions: CompositeSubscription
/**
* Subscription used for custom brightness overlay
*/
private var customBrightnessSubscription: Subscription? = null
/**
* Subscription used for color filter overlay
*/
private var customFilterColorSubscription: Subscription? = null
/**
* This method will be called after onCreate(Bundle)
* @param savedState The last saved instance state of the Fragment.
*/
override fun onCreateDialog(savedState: Bundle?): Dialog {
val dialog = MaterialDialog.Builder(activity)
.customView(R.layout.dialog_reader_custom_filter, false)
.positiveText(android.R.string.ok)
.build()
subscriptions = CompositeSubscription()
onViewCreated(dialog.view, savedState)
return dialog
}
/**
* Called immediately after onCreateView()
* @param view The View returned by onCreateDialog.
* @param savedInstanceState If non-null, this fragment is being re-constructed
*/
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(view) {
// Initialize subscriptions.
subscriptions += preferences.colorFilter().asObservable()
.subscribe { setColorFilter(it, view) }
subscriptions += preferences.customBrightness().asObservable()
.subscribe { setCustomBrightness(it, view) }
// Get color and update values
val color = preferences.colorFilterValue().getOrDefault()
val brightness = preferences.customBrightnessValue().getOrDefault()
val argb = setValues(color, view)
// Set brightness value
txt_brightness_seekbar_value.text = brightness.toString()
// Initialize seekBar progress
seekbar_color_filter_alpha.progress = argb[0]
seekbar_color_filter_red.progress = argb[1]
seekbar_color_filter_green.progress = argb[2]
seekbar_color_filter_blue.progress = argb[3]
// Set listeners
switch_color_filter.isChecked = preferences.colorFilter().getOrDefault()
switch_color_filter.setOnCheckedChangeListener { v, isChecked ->
preferences.colorFilter().set(isChecked)
}
custom_brightness.isChecked = preferences.customBrightness().getOrDefault()
custom_brightness.setOnCheckedChangeListener { v, isChecked ->
preferences.customBrightness().set(isChecked)
}
seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
setColorValue(value, ALPHA_MASK, 24)
}
}
})
seekbar_color_filter_red.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
setColorValue(value, RED_MASK, 16)
}
}
})
seekbar_color_filter_green.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
setColorValue(value, GREEN_MASK, 8)
}
}
})
seekbar_color_filter_blue.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
setColorValue(value, BLUE_MASK, 0)
}
}
})
brightness_seekbar.progress = preferences.customBrightnessValue().getOrDefault()
brightness_seekbar.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
preferences.customBrightnessValue().set(value)
}
}
})
}
/**
* Set enabled status of seekBars belonging to color filter
* @param enabled determines if seekBar gets enabled
* @param view view of the dialog
*/
private fun setColorFilterSeekBar(enabled: Boolean, view: View) = with(view) {
seekbar_color_filter_red.isEnabled = enabled
seekbar_color_filter_green.isEnabled = enabled
seekbar_color_filter_blue.isEnabled = enabled
seekbar_color_filter_alpha.isEnabled = enabled
}
/**
* Set enabled status of seekBars belonging to custom brightness
* @param enabled value which determines if seekBar gets enabled
* @param view view of the dialog
*/
private fun setCustomBrightnessSeekBar(enabled: Boolean, view: View) = with(view) {
brightness_seekbar.isEnabled = enabled
}
/**
* Set the text value's of color filter
* @param color integer containing color information
* @param view view of the dialog
*/
fun setValues(color: Int, view: View): Array<Int> {
val alpha = getAlphaFromColor(color)
val red = getRedFromColor(color)
val green = getGreenFromColor(color)
val blue = getBlueFromColor(color)
//Initialize values
with(view) {
txt_color_filter_alpha_value.text = alpha.toString()
txt_color_filter_red_value.text = red.toString()
txt_color_filter_green_value.text = green.toString()
txt_color_filter_blue_value.text = blue.toString()
}
return arrayOf(alpha, red, green, blue)
}
/**
* Manages the custom brightness value subscription
* @param enabled determines if the subscription get (un)subscribed
* @param view view of the dialog
*/
private fun setCustomBrightness(enabled: Boolean, view: View) {
if (enabled) {
customBrightnessSubscription = preferences.customBrightnessValue().asObservable()
.sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe { setCustomBrightnessValue(it, view) }
subscriptions.add(customBrightnessSubscription)
} else {
customBrightnessSubscription?.let { subscriptions.remove(it) }
setCustomBrightnessValue(0, view, true)
}
setCustomBrightnessSeekBar(enabled, view)
}
/**
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
* From 1 to 100 it sets that value as brightness.
* 0 sets system brightness and hides the overlay.
*/
private fun setCustomBrightnessValue(value: Int, view: View, isDisabled: Boolean = false) = with(view) {
// Set black overlay visibility.
if (value < 0) {
brightness_overlay.visibility = View.VISIBLE
val alpha = (Math.abs(value) * 2.56).toInt()
brightness_overlay.setBackgroundColor(Color.argb(alpha, 0, 0, 0))
} else {
brightness_overlay.visibility = View.GONE
}
if (!isDisabled)
txt_brightness_seekbar_value.text = value.toString()
}
/**
* Manages the color filter value subscription
* @param enabled determines if the subscription get (un)subscribed
* @param view view of the dialog
*/
private fun setColorFilter(enabled: Boolean, view: View) {
if (enabled) {
customFilterColorSubscription = preferences.colorFilterValue().asObservable()
.sample(100, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe { setColorFilterValue(it, view) }
subscriptions.add(customFilterColorSubscription)
} else {
customFilterColorSubscription?.let { subscriptions.remove(it) }
view.color_overlay.visibility = View.GONE
}
setColorFilterSeekBar(enabled, view)
}
/**
* Sets the color filter overlay of the screen. Determined by HEX of integer
* @param color hex of color.
* @param view view of the dialog
*/
private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) {
color_overlay.visibility = View.VISIBLE
color_overlay.setBackgroundColor(color)
setValues(color, view)
}
/**
* Updates the color value in preference
* @param color value of color range [0,255]
* @param mask contains hex mask of chosen color
* @param bitShift amounts of bits that gets shifted to receive value
*/
fun setColorValue(color: Int, mask: Long, bitShift: Int) {
val currentColor = preferences.colorFilterValue().getOrDefault()
val updatedColor = (color shl bitShift) or (currentColor and mask.inv().toInt())
preferences.colorFilterValue().set(updatedColor)
}
/**
* Returns the alpha value from the Color Hex
* @param color color hex as int
* @return alpha of color
*/
fun getAlphaFromColor(color: Int): Int {
return color shr 24 and 0xFF
}
/**
* Returns the red value from the Color Hex
* @param color color hex as int
* @return red of color
*/
fun getRedFromColor(color: Int): Int {
return color shr 16 and 0xFF
}
/**
* Returns the green value from the Color Hex
* @param color color hex as int
* @return green of color
*/
fun getGreenFromColor(color: Int): Int {
return color shr 8 and 0xFF
}
/**
* Returns the blue value from the Color Hex
* @param color color hex as int
* @return blue of color
*/
fun getBlueFromColor(color: Int): Int {
return color and 0xFF
}
/**
* Called when dialog is dismissed
*/
override fun onDestroyView() {
subscriptions.unsubscribe()
super.onDestroyView()
}
}

View File

@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
import kotlinx.android.synthetic.main.dialog_reader_settings.view.*
import org.adw.library.widgets.discreteseekbar.DiscreteSeekBar
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.subscriptions.CompositeSubscription
@ -84,24 +83,6 @@ class ReaderSettingsDialog : DialogFragment() {
fullscreen.setOnCheckedChangeListener { v, isChecked ->
preferences.fullscreen().set(isChecked)
}
custom_brightness.isChecked = preferences.customBrightness().getOrDefault()
custom_brightness.setOnCheckedChangeListener { v, isChecked ->
preferences.customBrightness().set(isChecked)
}
brightness_seekbar.progress = preferences.customBrightnessValue().getOrDefault()
brightness_seekbar.setOnProgressChangeListener(object : DiscreteSeekBar.OnProgressChangeListener {
override fun onProgressChanged(seekBar: DiscreteSeekBar, value: Int, fromUser: Boolean) {
if (fromUser) {
preferences.customBrightnessValue().set(value)
}
}
override fun onStartTrackingTouch(seekBar: DiscreteSeekBar) {}
override fun onStopTrackingTouch(seekBar: DiscreteSeekBar) {}
})
}
override fun onDestroyView() {

View File

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.widget
import android.content.Context
import android.util.AttributeSet
import android.widget.SeekBar
import eu.kanade.tachiyomi.R
class NegativeSeekBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
SeekBar(context, attrs) {
private var minValue: Int = 0
private var maxValue: Int = 0
private var listener: OnSeekBarChangeListener? = null
init {
val styledAttributes = context.obtainStyledAttributes(
attrs,
R.styleable.NegativeSeekBar, 0, 0)
try {
setMinSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_min_seek, 0))
setMaxSeek(styledAttributes.getInt(R.styleable.NegativeSeekBar_max_seek, 0))
} finally {
styledAttributes.recycle()
}
super.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, value: Int, fromUser: Boolean) {
listener?.let { it.onProgressChanged(seekBar, minValue + value, fromUser) }
}
override fun onStartTrackingTouch(p0: SeekBar?) {
listener?.let { it.onStartTrackingTouch(p0) }
}
override fun onStopTrackingTouch(p0: SeekBar?) {
listener?.let { it.onStopTrackingTouch(p0) }
}
})
}
override fun setProgress(progress: Int) {
super.setProgress(Math.abs(minValue) + progress)
}
fun setMinSeek(minValue: Int) {
this.minValue = minValue
max = (this.maxValue - this.minValue)
}
fun setMaxSeek(maxValue: Int) {
this.maxValue = maxValue
max = (this.maxValue - this.minValue)
}
override fun setOnSeekBarChangeListener(listener: OnSeekBarChangeListener?) {
this.listener = listener
}
}

View File

@ -1,11 +1,13 @@
package eu.kanade.tachiyomi.widget
import android.widget.SeekBar
open class SimpleSeekBarListener : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStartTrackingTouch(seekBar: SeekBar) {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {
}
}