diff --git a/app/src/main/java/io/legado/app/ui/widget/BadgeView.kt b/app/src/main/java/io/legado/app/ui/widget/BadgeView.kt new file mode 100644 index 000000000..5f09a8744 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/BadgeView.kt @@ -0,0 +1,233 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape +import android.text.TextUtils +import android.util.AttributeSet +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.FrameLayout.LayoutParams +import android.widget.TabWidget +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R + + +/** + * Created by milad heydari on 5/6/2016. + */ +class BadgeView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = android.R.attr.textViewStyle +) : AppCompatTextView(context, attrs, defStyle) { + + /** + * @return Returns true if view is hidden on badge value 0 or null; + */ + /** + * @param hideOnNull the hideOnNull to set + */ + var isHideOnNull = true + set(hideOnNull) { + field = hideOnNull + text = text + } + private var radius: Float = 0.toFloat() + + val badgeCount: Int? + get() { + if (text == null) { + return null + } + val text = text.toString() + try { + return Integer.parseInt(text) + } catch (e: NumberFormatException) { + return null + } + + } + + var badgeGravity: Int + get() { + val params = layoutParams as LayoutParams + return params.gravity + } + set(gravity) { + val params = layoutParams as LayoutParams + params.gravity = gravity + layoutParams = params + } + + val badgeMargin: IntArray + get() { + val params = layoutParams as LayoutParams + return intArrayOf(params.leftMargin, params.topMargin, params.rightMargin, params.bottomMargin) + } + + init { + + init() + } + + private fun init() { + if (layoutParams !is LayoutParams) { + val layoutParams = LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) + setLayoutParams(layoutParams) + } + + // set default font + setTextColor(Color.WHITE) + //setTypeface(Typeface.DEFAULT_BOLD); + setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f) + setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f)) + radius = 8f + + // set default background + setBackground(radius, Color.parseColor("#d3321b")) + + gravity = Gravity.CENTER + + // default values + isHideOnNull = true + setBadgeCount(0) + minWidth = dip2Px(16f) + minHeight = dip2Px(16f) + } + + fun setBackground(dipRadius: Float, badgeColor: Int) { + val radius = dip2Px(dipRadius) + val radiusArray = floatArrayOf( + radius.toFloat(), + radius.toFloat(), + radius.toFloat(), + radius.toFloat(), + radius.toFloat(), + radius.toFloat(), + radius.toFloat(), + radius.toFloat() + ) + + val roundRect = RoundRectShape(radiusArray, null, null) + val bgDrawable = ShapeDrawable(roundRect) + bgDrawable.paint.color = badgeColor + background = bgDrawable + } + + fun setBackground(badgeColor: Int) { + setBackground(radius, badgeColor) + } + + /** + * @see android.widget.TextView.setText + */ + override fun setText(text: CharSequence, type: BufferType) { + if (isHideOnNull && TextUtils.isEmpty(text)) { + visibility = View.GONE + } else { + visibility = View.VISIBLE + } + super.setText(text, type) + } + + fun setBadgeCount(count: Int) { + text = count.toString() + if (count == 0) { + visibility = View.GONE + } + } + + fun setHighlight(highlight: Boolean) { + setBackground(resources.getColor(if (highlight) R.color.highlight else R.color.darker_gray)) + } + + fun setBadgeMargin(dipMargin: Int) { + setBadgeMargin(dipMargin, dipMargin, dipMargin, dipMargin) + } + + fun setBadgeMargin(leftDipMargin: Int, topDipMargin: Int, rightDipMargin: Int, bottomDipMargin: Int) { + val params = layoutParams as LayoutParams + params.leftMargin = dip2Px(leftDipMargin.toFloat()) + params.topMargin = dip2Px(topDipMargin.toFloat()) + params.rightMargin = dip2Px(rightDipMargin.toFloat()) + params.bottomMargin = dip2Px(bottomDipMargin.toFloat()) + layoutParams = params + } + + fun incrementBadgeCount(increment: Int) { + val count = badgeCount + if (count == null) { + setBadgeCount(increment) + } else { + setBadgeCount(increment + count) + } + } + + fun decrementBadgeCount(decrement: Int) { + incrementBadgeCount(-decrement) + } + + /** + * Attach the BadgeView to the TabWidget + * @param target the TabWidget to attach the BadgeView + * @param tabIndex index of the tab + */ + fun setTargetView(target: TabWidget, tabIndex: Int) { + val tabView = target.getChildTabViewAt(tabIndex) + setTargetView(tabView) + } + + /** + * Attach the BadgeView to the target view + * @param target the view to attach the BadgeView + */ + fun setTargetView(target: View?) { + if (parent != null) { + (parent as ViewGroup).removeView(this) + } + + if (target == null) { + return + } + + if (target.parent is FrameLayout) { + (target.parent as FrameLayout).addView(this) + + } else if (target.parent is ViewGroup) { + // use a new FrameLayout container for adding badge + val parentContainer = target.parent as ViewGroup + val groupIndex = parentContainer.indexOfChild(target) + parentContainer.removeView(target) + + val badgeContainer = FrameLayout(context) + val parentLayoutParams = target.layoutParams + + badgeContainer.layoutParams = parentLayoutParams + target.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + ) + + parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams) + badgeContainer.addView(target) + + badgeContainer.addView(this) + } + + } + + /** + * converts dip to px + */ + private fun dip2Px(dip: Float): Int { + return (dip * context.resources.displayMetrics.density + 0.5f).toInt() + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt b/app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt new file mode 100644 index 000000000..c17bc7b5d --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt @@ -0,0 +1,207 @@ +package io.legado.app.ui.widget + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import android.view.animation.LinearInterpolator +import io.legado.app.R + +/** + * RotateLoading + * Created by Victor on 2015/4/28. + */ +class RotateLoading : View { + + private var mPaint: Paint? = null + + private var loadingRectF: RectF? = null + private var shadowRectF: RectF? = null + + private var topDegree = 10 + private var bottomDegree = 190 + + private var arc: Float = 0.toFloat() + + private var thisWidth: Int = 0 + + private var changeBigger = true + + private var shadowPosition: Int = 0 + + var isStart = false + private set + + var loadingColor: Int = 0 + + private var speedOfDegree: Int = 0 + + private var speedOfArc: Float = 0.toFloat() + + constructor(context: Context) : super(context) { + initView(context, null) + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + initView(context, attrs) + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + initView(context, attrs) + } + + private fun initView(context: Context, attrs: AttributeSet?) { + loadingColor = Color.WHITE + thisWidth = dpToPx(context, DEFAULT_WIDTH.toFloat()) + shadowPosition = dpToPx(getContext(), DEFAULT_SHADOW_POSITION.toFloat()) + speedOfDegree = DEFAULT_SPEED_OF_DEGREE + + if (null != attrs) { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.RotateLoading) + loadingColor = typedArray.getColor(R.styleable.RotateLoading_loading_color, Color.WHITE) + thisWidth = typedArray.getDimensionPixelSize( + R.styleable.RotateLoading_loading_width, + dpToPx(context, DEFAULT_WIDTH.toFloat()) + ) + shadowPosition = typedArray.getInt(R.styleable.RotateLoading_shadow_position, DEFAULT_SHADOW_POSITION) + speedOfDegree = typedArray.getInt(R.styleable.RotateLoading_loading_speed, DEFAULT_SPEED_OF_DEGREE) + typedArray.recycle() + } + speedOfArc = (speedOfDegree / 4).toFloat() + mPaint = Paint() + mPaint!!.color = loadingColor + mPaint!!.isAntiAlias = true + mPaint!!.style = Paint.Style.STROKE + mPaint!!.strokeWidth = thisWidth.toFloat() + mPaint!!.strokeCap = Paint.Cap.ROUND + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + arc = 10f + + loadingRectF = + RectF((2 * thisWidth).toFloat(), (2 * thisWidth).toFloat(), (w - 2 * thisWidth).toFloat(), (h - 2 * thisWidth).toFloat()) + shadowRectF = RectF( + (2 * thisWidth + shadowPosition).toFloat(), + (2 * thisWidth + shadowPosition).toFloat(), + (w - 2 * thisWidth + shadowPosition).toFloat(), + (h - 2 * thisWidth + shadowPosition).toFloat() + ) + } + + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (!isStart) { + return + } + + mPaint!!.color = Color.parseColor("#1a000000") + canvas.drawArc(shadowRectF!!, topDegree.toFloat(), arc, false, mPaint!!) + canvas.drawArc(shadowRectF!!, bottomDegree.toFloat(), arc, false, mPaint!!) + + mPaint!!.color = loadingColor + canvas.drawArc(loadingRectF!!, topDegree.toFloat(), arc, false, mPaint!!) + canvas.drawArc(loadingRectF!!, bottomDegree.toFloat(), arc, false, mPaint!!) + + topDegree += speedOfDegree + bottomDegree += speedOfDegree + if (topDegree > 360) { + topDegree = topDegree - 360 + } + if (bottomDegree > 360) { + bottomDegree = bottomDegree - 360 + } + + if (changeBigger) { + if (arc < 160) { + arc += speedOfArc + invalidate() + } + } else { + if (arc > speedOfDegree) { + arc -= 2 * speedOfArc + invalidate() + } + } + if (arc >= 160 || arc <= 10) { + changeBigger = !changeBigger + invalidate() + } + } + + fun start() { + startAnimator() + isStart = true + invalidate() + } + + fun stop() { + stopAnimator() + invalidate() + } + + private fun startAnimator() { + val scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 0.0f, 1f) + val scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 0.0f, 1f) + scaleXAnimator.setDuration(300) + scaleXAnimator.setInterpolator(LinearInterpolator()) + scaleYAnimator.setDuration(300) + scaleYAnimator.setInterpolator(LinearInterpolator()) + val animatorSet = AnimatorSet() + animatorSet.playTogether(scaleXAnimator, scaleYAnimator) + animatorSet.start() + } + + private fun stopAnimator() { + val scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 1f, 0f) + val scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 1f, 0f) + scaleXAnimator.setDuration(300) + scaleXAnimator.setInterpolator(LinearInterpolator()) + scaleYAnimator.setDuration(300) + scaleYAnimator.setInterpolator(LinearInterpolator()) + val animatorSet = AnimatorSet() + animatorSet.playTogether(scaleXAnimator, scaleYAnimator) + animatorSet.addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animation: Animator) { + + } + + override fun onAnimationEnd(animation: Animator) { + isStart = false + } + + override fun onAnimationCancel(animation: Animator) { + + } + + override fun onAnimationRepeat(animation: Animator) { + + } + }) + animatorSet.start() + } + + + fun dpToPx(context: Context, dpVal: Float): Int { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.resources.displayMetrics).toInt() + } + + companion object { + + private val DEFAULT_WIDTH = 6 + private val DEFAULT_SHADOW_POSITION = 2 + private val DEFAULT_SPEED_OF_DEGREE = 10 + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt new file mode 100644 index 000000000..289790a1a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt @@ -0,0 +1,56 @@ +package io.legado.app.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Path +import android.util.AttributeSet + + +class CoverImageView : androidx.appcompat.widget.AppCompatImageView { + internal var width: Float = 0.toFloat() + internal var height: Float = 0.toFloat() + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + width = getWidth().toFloat() + height = getHeight().toFloat() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) + val measuredHeight = measuredWidth * 7 / 5 + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)) + } + + override fun onDraw(canvas: Canvas) { + if (width >= 10 && height > 10) { + @SuppressLint("DrawAllocation") + val path = Path() + //四个圆角 + path.moveTo(10f, 0f) + path.lineTo(width - 10, 0f) + path.quadTo(width, 0f, width, 10f) + path.lineTo(width, height - 10) + path.quadTo(width, height, width - 10, height) + path.lineTo(10f, height) + path.quadTo(0f, height, 0f, height - 10) + path.lineTo(0f, 10f) + path.quadTo(0f, 0f, 10f, 0f) + + canvas.clipPath(path) + } + super.onDraw(canvas) + } + + fun setHeight(height: Int) { + val width = height * 5 / 7 + minimumWidth = width + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt new file mode 100644 index 000000000..bb6a4461e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt @@ -0,0 +1,92 @@ +package io.legado.app.ui.widget.image + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Path +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import io.legado.app.R + +class FilletImageView : AppCompatImageView { + internal var width: Float = 0.toFloat() + internal var height: Float = 0.toFloat() + private var leftTopRadius: Int = 0 + private var rightTopRadius: Int = 0 + private var rightBottomRadius: Int = 0 + private var leftBottomRadius: Int = 0 + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init(context, attrs) + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + init(context, attrs) + } + + private fun init(context: Context, attrs: AttributeSet) { + // 读取配置 + val array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView) + val defaultRadius = 5 + val radius = array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius) + leftTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_top_radius, defaultRadius) + rightTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_top_radius, defaultRadius) + rightBottomRadius = + array.getDimensionPixelOffset(R.styleable.FilletImageView_right_bottom_radius, defaultRadius) + leftBottomRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_bottom_radius, defaultRadius) + + //如果四个角的值没有设置,那么就使用通用的radius的值。 + if (defaultRadius == leftTopRadius) { + leftTopRadius = radius + } + if (defaultRadius == rightTopRadius) { + rightTopRadius = radius + } + if (defaultRadius == rightBottomRadius) { + rightBottomRadius = radius + } + if (defaultRadius == leftBottomRadius) { + leftBottomRadius = radius + } + array.recycle() + + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + width = getWidth().toFloat() + height = getHeight().toFloat() + } + + override fun onDraw(canvas: Canvas) { + //这里做下判断,只有图片的宽高大于设置的圆角距离的时候才进行裁剪 + val maxLeft = Math.max(leftTopRadius, leftBottomRadius) + val maxRight = Math.max(rightTopRadius, rightBottomRadius) + val minWidth = maxLeft + maxRight + val maxTop = Math.max(leftTopRadius, rightTopRadius) + val maxBottom = Math.max(leftBottomRadius, rightBottomRadius) + val minHeight = maxTop + maxBottom + if (width >= minWidth && height > minHeight) { + @SuppressLint("DrawAllocation") val path = Path() + //四个角:右上,右下,左下,左上 + path.moveTo(leftTopRadius.toFloat(), 0f) + path.lineTo(width - rightTopRadius, 0f) + path.quadTo(width, 0f, width, rightTopRadius.toFloat()) + + path.lineTo(width, height - rightBottomRadius) + path.quadTo(width, height, width - rightBottomRadius, height) + + path.lineTo(leftBottomRadius.toFloat(), height) + path.quadTo(0f, height, 0f, height - leftBottomRadius) + + path.lineTo(0f, leftTopRadius.toFloat()) + path.quadTo(0f, 0f, leftTopRadius.toFloat(), 0f) + + canvas.clipPath(path) + } + super.onDraw(canvas) + } + +} diff --git a/app/src/main/res/layout/item_bookshelf_list.xml b/app/src/main/res/layout/item_bookshelf_list.xml new file mode 100644 index 000000000..03845fa18 --- /dev/null +++ b/app/src/main/res/layout/item_bookshelf_list.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 75b4d171f..42b4963d2 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -101,4 +101,10 @@ + + + + + + \ No newline at end of file