pull/32/head
parent
ad5fd8fb28
commit
f982626291
@ -0,0 +1,363 @@ |
||||
package io.legado.app.ui.widget.seekbar |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.content.Context |
||||
import android.graphics.Canvas |
||||
import android.graphics.drawable.Drawable |
||||
import android.util.AttributeSet |
||||
import android.view.KeyEvent |
||||
import android.view.MotionEvent |
||||
import android.widget.ProgressBar |
||||
import androidx.appcompat.widget.AppCompatSeekBar |
||||
import androidx.core.view.ViewCompat |
||||
import io.legado.app.R |
||||
import io.legado.app.lib.theme.ATH |
||||
import io.legado.app.lib.theme.ThemeStore |
||||
import java.lang.reflect.InvocationTargetException |
||||
import java.lang.reflect.Method |
||||
|
||||
class VerticalSeekBar : AppCompatSeekBar { |
||||
|
||||
private var mIsDragging: Boolean = false |
||||
private var mThumb: Drawable? = null |
||||
private var mMethodSetProgressFromUser: Method? = null |
||||
private var mRotationAngle = ROTATION_ANGLE_CW_90 |
||||
|
||||
var rotationAngle: Int |
||||
get() = mRotationAngle |
||||
set(angle) { |
||||
require(isValidRotationAngle(angle)) { "Invalid angle specified :$angle" } |
||||
|
||||
if (mRotationAngle == angle) { |
||||
return |
||||
} |
||||
|
||||
mRotationAngle = angle |
||||
|
||||
if (useViewRotation()) { |
||||
val wrapper = wrapper |
||||
wrapper?.applyViewRotation() |
||||
} else { |
||||
requestLayout() |
||||
} |
||||
} |
||||
|
||||
private val wrapper: VerticalSeekBarWrapper? |
||||
get() { |
||||
val parent = parent |
||||
|
||||
return if (parent is VerticalSeekBarWrapper) { |
||||
parent |
||||
} else { |
||||
null |
||||
} |
||||
} |
||||
|
||||
constructor(context: Context) : super(context) { |
||||
initialize(context, null, 0, 0) |
||||
} |
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { |
||||
initialize(context, attrs, 0, 0) |
||||
} |
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super( |
||||
context, |
||||
attrs, |
||||
defStyle |
||||
) { |
||||
initialize(context, attrs, defStyle, 0) |
||||
} |
||||
|
||||
private fun initialize( |
||||
context: Context, |
||||
attrs: AttributeSet?, |
||||
defStyleAttr: Int, |
||||
defStyleRes: Int |
||||
) { |
||||
ATH.setTint(this, ThemeStore.accentColor(context)) |
||||
ViewCompat.setLayoutDirection(this, ViewCompat.LAYOUT_DIRECTION_LTR) |
||||
|
||||
if (attrs != null) { |
||||
val a = context.obtainStyledAttributes( |
||||
attrs, |
||||
R.styleable.VerticalSeekBar, |
||||
defStyleAttr, |
||||
defStyleRes |
||||
) |
||||
val rotationAngle = a.getInteger(R.styleable.VerticalSeekBar_seekBarRotation, 0) |
||||
if (isValidRotationAngle(rotationAngle)) { |
||||
mRotationAngle = rotationAngle |
||||
} |
||||
a.recycle() |
||||
} |
||||
} |
||||
|
||||
override fun setThumb(thumb: Drawable) { |
||||
mThumb = thumb |
||||
super.setThumb(thumb) |
||||
} |
||||
|
||||
@SuppressLint("ClickableViewAccessibility") |
||||
override fun onTouchEvent(event: MotionEvent): Boolean { |
||||
return if (useViewRotation()) { |
||||
onTouchEventUseViewRotation(event) |
||||
} else { |
||||
onTouchEventTraditionalRotation(event) |
||||
} |
||||
} |
||||
|
||||
private fun onTouchEventTraditionalRotation(event: MotionEvent): Boolean { |
||||
if (!isEnabled) { |
||||
return false |
||||
} |
||||
|
||||
when (event.action) { |
||||
MotionEvent.ACTION_DOWN -> { |
||||
isPressed = true |
||||
onStartTrackingTouch() |
||||
trackTouchEvent(event) |
||||
attemptClaimDrag(true) |
||||
invalidate() |
||||
} |
||||
|
||||
MotionEvent.ACTION_MOVE -> if (mIsDragging) { |
||||
trackTouchEvent(event) |
||||
} |
||||
|
||||
MotionEvent.ACTION_UP -> { |
||||
if (mIsDragging) { |
||||
trackTouchEvent(event) |
||||
onStopTrackingTouch() |
||||
isPressed = false |
||||
} else { |
||||
// Touch up when we never crossed the touch slop threshold |
||||
// should |
||||
// be interpreted as a tap-seek to that location. |
||||
onStartTrackingTouch() |
||||
trackTouchEvent(event) |
||||
onStopTrackingTouch() |
||||
attemptClaimDrag(false) |
||||
} |
||||
// ProgressBar doesn't know to repaint the thumb drawable |
||||
// in its inactive state when the touch stops (because the |
||||
// value has not apparently changed) |
||||
invalidate() |
||||
} |
||||
|
||||
MotionEvent.ACTION_CANCEL -> { |
||||
if (mIsDragging) { |
||||
onStopTrackingTouch() |
||||
isPressed = false |
||||
} |
||||
invalidate() // see above explanation |
||||
} |
||||
} |
||||
return true |
||||
} |
||||
|
||||
private fun onTouchEventUseViewRotation(event: MotionEvent): Boolean { |
||||
val handled = super.onTouchEvent(event) |
||||
|
||||
if (handled) { |
||||
when (event.action) { |
||||
MotionEvent.ACTION_DOWN -> attemptClaimDrag(true) |
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> attemptClaimDrag(false) |
||||
} |
||||
} |
||||
|
||||
return handled |
||||
} |
||||
|
||||
private fun trackTouchEvent(event: MotionEvent) { |
||||
val paddingLeft = super.getPaddingLeft() |
||||
val paddingRight = super.getPaddingRight() |
||||
val height = height |
||||
|
||||
val available = height - paddingLeft - paddingRight |
||||
val y = event.y.toInt() |
||||
|
||||
val scale: Float |
||||
var value = 0f |
||||
|
||||
when (mRotationAngle) { |
||||
ROTATION_ANGLE_CW_90 -> value = (y - paddingLeft).toFloat() |
||||
ROTATION_ANGLE_CW_270 -> value = (height - paddingLeft - y).toFloat() |
||||
} |
||||
|
||||
scale = if (value < 0 || available == 0) { |
||||
0.0f |
||||
} else if (value > available) { |
||||
1.0f |
||||
} else { |
||||
value / available.toFloat() |
||||
} |
||||
|
||||
val max = max |
||||
val progress = scale * max |
||||
|
||||
setProgressFromUser(progress.toInt(), true) |
||||
} |
||||
|
||||
/** |
||||
* Tries to claim the user's drag motion, and requests disallowing any |
||||
* ancestors from stealing events in the drag. |
||||
*/ |
||||
private fun attemptClaimDrag(active: Boolean) { |
||||
val parent = parent |
||||
parent?.requestDisallowInterceptTouchEvent(active) |
||||
} |
||||
|
||||
/** |
||||
* This is called when the user has started touching this widget. |
||||
*/ |
||||
private fun onStartTrackingTouch() { |
||||
mIsDragging = true |
||||
} |
||||
|
||||
/** |
||||
* This is called when the user either releases his touch or the touch is |
||||
* canceled. |
||||
*/ |
||||
private fun onStopTrackingTouch() { |
||||
mIsDragging = false |
||||
} |
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { |
||||
if (isEnabled) { |
||||
val handled: Boolean |
||||
var direction = 0 |
||||
|
||||
when (keyCode) { |
||||
KeyEvent.KEYCODE_DPAD_DOWN -> { |
||||
direction = if (mRotationAngle == ROTATION_ANGLE_CW_90) 1 else -1 |
||||
handled = true |
||||
} |
||||
KeyEvent.KEYCODE_DPAD_UP -> { |
||||
direction = if (mRotationAngle == ROTATION_ANGLE_CW_270) 1 else -1 |
||||
handled = true |
||||
} |
||||
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT -> |
||||
// move view focus to previous/next view |
||||
return false |
||||
else -> handled = false |
||||
} |
||||
|
||||
if (handled) { |
||||
val keyProgressIncrement = keyProgressIncrement |
||||
var progress = progress |
||||
|
||||
progress += direction * keyProgressIncrement |
||||
|
||||
if (progress in 0..max) { |
||||
setProgressFromUser(progress, true) |
||||
} |
||||
|
||||
return true |
||||
} |
||||
} |
||||
|
||||
return super.onKeyDown(keyCode, event) |
||||
} |
||||
|
||||
@Synchronized |
||||
override fun setProgress(progress: Int) { |
||||
super.setProgress(progress) |
||||
if (!useViewRotation()) { |
||||
refreshThumb() |
||||
} |
||||
} |
||||
|
||||
@Synchronized |
||||
private fun setProgressFromUser(progress: Int, fromUser: Boolean) { |
||||
if (mMethodSetProgressFromUser == null) { |
||||
try { |
||||
val m: Method = ProgressBar::class.java.getDeclaredMethod( |
||||
"setProgress", |
||||
Int::class.javaPrimitiveType, |
||||
Boolean::class.javaPrimitiveType |
||||
) |
||||
m.isAccessible = true |
||||
mMethodSetProgressFromUser = m |
||||
} catch (e: NoSuchMethodException) { |
||||
} |
||||
|
||||
} |
||||
|
||||
if (mMethodSetProgressFromUser != null) { |
||||
try { |
||||
mMethodSetProgressFromUser!!.invoke(this, progress, fromUser) |
||||
} catch (e: IllegalArgumentException) { |
||||
} catch (e: IllegalAccessException) { |
||||
} catch (e: InvocationTargetException) { |
||||
} |
||||
|
||||
} else { |
||||
super.setProgress(progress) |
||||
} |
||||
refreshThumb() |
||||
} |
||||
|
||||
@Synchronized |
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { |
||||
if (useViewRotation()) { |
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec) |
||||
} else { |
||||
super.onMeasure(heightMeasureSpec, widthMeasureSpec) |
||||
|
||||
val lp = layoutParams |
||||
|
||||
if (isInEditMode && lp != null && lp.height >= 0) { |
||||
setMeasuredDimension(super.getMeasuredHeight(), lp.height) |
||||
} else { |
||||
setMeasuredDimension(super.getMeasuredHeight(), super.getMeasuredWidth()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { |
||||
if (useViewRotation()) { |
||||
super.onSizeChanged(w, h, oldw, oldh) |
||||
} else { |
||||
super.onSizeChanged(h, w, oldh, oldw) |
||||
} |
||||
} |
||||
|
||||
@Synchronized |
||||
override fun onDraw(canvas: Canvas) { |
||||
if (!useViewRotation()) { |
||||
when (mRotationAngle) { |
||||
ROTATION_ANGLE_CW_90 -> { |
||||
canvas.rotate(90f) |
||||
canvas.translate(0f, (-super.getWidth()).toFloat()) |
||||
} |
||||
ROTATION_ANGLE_CW_270 -> { |
||||
canvas.rotate(-90f) |
||||
canvas.translate((-super.getHeight()).toFloat(), 0f) |
||||
} |
||||
} |
||||
} |
||||
|
||||
super.onDraw(canvas) |
||||
} |
||||
|
||||
// refresh thumb position |
||||
private fun refreshThumb() { |
||||
onSizeChanged(super.getWidth(), super.getHeight(), 0, 0) |
||||
} |
||||
|
||||
/*package*/ |
||||
internal fun useViewRotation(): Boolean { |
||||
return !isInEditMode |
||||
} |
||||
|
||||
companion object { |
||||
const val ROTATION_ANGLE_CW_90 = 90 |
||||
const val ROTATION_ANGLE_CW_270 = 270 |
||||
|
||||
private fun isValidRotationAngle(angle: Int): Boolean { |
||||
return angle == ROTATION_ANGLE_CW_90 || angle == ROTATION_ANGLE_CW_270 |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,184 @@ |
||||
package io.legado.app.ui.widget.seekbar |
||||
|
||||
import android.annotation.SuppressLint |
||||
import android.content.Context |
||||
import android.util.AttributeSet |
||||
import android.view.Gravity |
||||
import android.view.View |
||||
import android.view.ViewGroup |
||||
import android.widget.FrameLayout |
||||
|
||||
import androidx.core.view.ViewCompat |
||||
import kotlin.math.max |
||||
|
||||
class VerticalSeekBarWrapper @JvmOverloads constructor( |
||||
context: Context, |
||||
attrs: AttributeSet? = null, |
||||
defStyleAttr: Int = 0 |
||||
) : FrameLayout(context, attrs, defStyleAttr) { |
||||
|
||||
private val childSeekBar: VerticalSeekBar? |
||||
get() { |
||||
val child = if (childCount > 0) getChildAt(0) else null |
||||
return if (child is VerticalSeekBar) child else null |
||||
} |
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { |
||||
if (useViewRotation()) { |
||||
onSizeChangedUseViewRotation(w, h, oldw, oldh) |
||||
} else { |
||||
onSizeChangedTraditionalRotation(w, h, oldw, oldh) |
||||
} |
||||
} |
||||
|
||||
@SuppressLint("RtlHardcoded") |
||||
private fun onSizeChangedTraditionalRotation(w: Int, h: Int, oldw: Int, oldh: Int) { |
||||
val seekBar = childSeekBar |
||||
|
||||
if (seekBar != null) { |
||||
val hPadding = paddingLeft + paddingRight |
||||
val vPadding = paddingTop + paddingBottom |
||||
val lp = seekBar.layoutParams as LayoutParams |
||||
|
||||
lp.width = ViewGroup.LayoutParams.WRAP_CONTENT |
||||
lp.height = max(0, h - vPadding) |
||||
seekBar.layoutParams = lp |
||||
|
||||
seekBar.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) |
||||
|
||||
val seekBarMeasuredWidth = seekBar.measuredWidth |
||||
seekBar.measure( |
||||
MeasureSpec.makeMeasureSpec( |
||||
max(0, w - hPadding), |
||||
MeasureSpec.AT_MOST |
||||
), |
||||
MeasureSpec.makeMeasureSpec( |
||||
max(0, h - vPadding), |
||||
MeasureSpec.EXACTLY |
||||
) |
||||
) |
||||
|
||||
lp.gravity = Gravity.TOP or Gravity.LEFT |
||||
lp.leftMargin = (max(0, w - hPadding) - seekBarMeasuredWidth) / 2 |
||||
seekBar.layoutParams = lp |
||||
} |
||||
|
||||
super.onSizeChanged(w, h, oldw, oldh) |
||||
} |
||||
|
||||
private fun onSizeChangedUseViewRotation(w: Int, h: Int, oldw: Int, oldh: Int) { |
||||
val seekBar = childSeekBar |
||||
|
||||
if (seekBar != null) { |
||||
val hPadding = paddingLeft + paddingRight |
||||
val vPadding = paddingTop + paddingBottom |
||||
seekBar.measure( |
||||
MeasureSpec.makeMeasureSpec( |
||||
max(0, h - vPadding), |
||||
MeasureSpec.EXACTLY |
||||
), |
||||
MeasureSpec.makeMeasureSpec( |
||||
max(0, w - hPadding), |
||||
MeasureSpec.AT_MOST |
||||
) |
||||
) |
||||
} |
||||
|
||||
applyViewRotation(w, h) |
||||
super.onSizeChanged(w, h, oldw, oldh) |
||||
} |
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { |
||||
val seekBar = childSeekBar |
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec) |
||||
val heightMode = MeasureSpec.getMode(heightMeasureSpec) |
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec) |
||||
val heightSize = MeasureSpec.getSize(heightMeasureSpec) |
||||
|
||||
if (seekBar != null && widthMode != MeasureSpec.EXACTLY) { |
||||
val seekBarWidth: Int |
||||
val seekBarHeight: Int |
||||
val hPadding = paddingLeft + paddingRight |
||||
val vPadding = paddingTop + paddingBottom |
||||
val innerContentWidthMeasureSpec = |
||||
MeasureSpec.makeMeasureSpec(max(0, widthSize - hPadding), widthMode) |
||||
val innerContentHeightMeasureSpec = |
||||
MeasureSpec.makeMeasureSpec(max(0, heightSize - vPadding), heightMode) |
||||
|
||||
if (useViewRotation()) { |
||||
seekBar.measure(innerContentHeightMeasureSpec, innerContentWidthMeasureSpec) |
||||
seekBarWidth = seekBar.measuredHeight |
||||
seekBarHeight = seekBar.measuredWidth |
||||
} else { |
||||
seekBar.measure(innerContentWidthMeasureSpec, innerContentHeightMeasureSpec) |
||||
seekBarWidth = seekBar.measuredWidth |
||||
seekBarHeight = seekBar.measuredHeight |
||||
} |
||||
|
||||
val measuredWidth = |
||||
View.resolveSizeAndState(seekBarWidth + hPadding, widthMeasureSpec, 0) |
||||
val measuredHeight = |
||||
View.resolveSizeAndState(seekBarHeight + vPadding, heightMeasureSpec, 0) |
||||
|
||||
setMeasuredDimension(measuredWidth, measuredHeight) |
||||
} else { |
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec) |
||||
} |
||||
} |
||||
|
||||
/*package*/ |
||||
internal fun applyViewRotation() { |
||||
applyViewRotation(width, height) |
||||
} |
||||
|
||||
private fun applyViewRotation(w: Int, h: Int) { |
||||
val seekBar = childSeekBar |
||||
|
||||
if (seekBar != null) { |
||||
val isLTR = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR |
||||
val rotationAngle = seekBar.rotationAngle |
||||
val seekBarMeasuredWidth = seekBar.measuredWidth |
||||
val seekBarMeasuredHeight = seekBar.measuredHeight |
||||
val hPadding = paddingLeft + paddingRight |
||||
val vPadding = paddingTop + paddingBottom |
||||
val hOffset = (max(0, w - hPadding) - seekBarMeasuredHeight) * 0.5f |
||||
val lp = seekBar.layoutParams |
||||
|
||||
lp.width = max(0, h - vPadding) |
||||
lp.height = ViewGroup.LayoutParams.WRAP_CONTENT |
||||
|
||||
seekBar.layoutParams = lp |
||||
|
||||
seekBar.pivotX = (if (isLTR) 0 else max(0, h - vPadding)).toFloat() |
||||
seekBar.pivotY = 0f |
||||
|
||||
when (rotationAngle) { |
||||
VerticalSeekBar.ROTATION_ANGLE_CW_90 -> { |
||||
seekBar.rotation = 90f |
||||
if (isLTR) { |
||||
seekBar.translationX = seekBarMeasuredHeight + hOffset |
||||
seekBar.translationY = 0f |
||||
} else { |
||||
seekBar.translationX = -hOffset |
||||
seekBar.translationY = seekBarMeasuredWidth.toFloat() |
||||
} |
||||
} |
||||
VerticalSeekBar.ROTATION_ANGLE_CW_270 -> { |
||||
seekBar.rotation = 270f |
||||
if (isLTR) { |
||||
seekBar.translationX = hOffset |
||||
seekBar.translationY = seekBarMeasuredWidth.toFloat() |
||||
} else { |
||||
seekBar.translationX = -(seekBarMeasuredHeight + hOffset) |
||||
seekBar.translationY = 0f |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun useViewRotation(): Boolean { |
||||
val seekBar = childSeekBar |
||||
return seekBar?.useViewRotation() ?: false |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="24dp" |
||||
android:height="24dp" |
||||
android:viewportWidth="1024" |
||||
android:viewportHeight="1024"> |
||||
|
||||
<path |
||||
android:fillColor="#FFFFFF" |
||||
android:pathData="M462.933333 539.733333h98.133334L512 384l-49.066667 155.733333z m390.4-170.666666V170.666667h-200.106666L512 29.44 370.773333 170.666667H170.666667v200.106666L29.44 512 170.666667 653.226667V853.333333h200.106666L512 994.56 653.226667 853.333333H853.333333v-200.106666L994.56 512 853.333333 370.773333zM610.133333 682.666667l-29.866666-85.333334h-136.533334l-29.866666 85.333334H332.8L469.333333 298.666667h85.333334l136.533333 384z" /> |
||||
</vector> |
Loading…
Reference in new issue