update
This commit is contained in:
parent
d305c7809c
commit
9e889a2f14
1
TabLayout/.gitignore
vendored
Normal file
1
TabLayout/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
29
TabLayout/build.gradle
Normal file
29
TabLayout/build.gradle
Normal file
@ -0,0 +1,29 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "30.0.2"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 28
|
||||
targetSdkVersion 33
|
||||
versionCode 1
|
||||
versionName "1.2"
|
||||
|
||||
consumerProguardFiles 'consumer-rules.pro'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
}
|
||||
|
||||
//apply from: "$gradleHost/master/publish.gradle"
|
0
TabLayout/consumer-rules.pro
Normal file
0
TabLayout/consumer-rules.pro
Normal file
21
TabLayout/proguard-rules.pro
vendored
Normal file
21
TabLayout/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
TabLayout/src/main/AndroidManifest.xml
Normal file
2
TabLayout/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.angcyo.tablayout" />
|
200
TabLayout/src/main/java/com/angcyo/tablayout/AbsDslDrawable.kt
Normal file
200
TabLayout/src/main/java/com/angcyo/tablayout/AbsDslDrawable.kt
Normal file
@ -0,0 +1,200 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
/**
|
||||
* 基础自绘Drawable
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/11/25
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
|
||||
abstract class AbsDslDrawable : Drawable() {
|
||||
|
||||
companion object {
|
||||
/**不绘制*/
|
||||
const val DRAW_TYPE_DRAW_NONE = 0x00
|
||||
|
||||
/**[android.view.View.draw]*/
|
||||
const val DRAW_TYPE_DRAW_AFTER = 0x01
|
||||
const val DRAW_TYPE_DRAW_BEFORE = 0x02
|
||||
|
||||
/**[android.view.View.onDraw]*/
|
||||
const val DRAW_TYPE_ON_DRAW_AFTER = 0x04
|
||||
const val DRAW_TYPE_ON_DRAW_BEFORE = 0x08
|
||||
}
|
||||
|
||||
/**画笔*/
|
||||
val textPaint: TextPaint by lazy {
|
||||
TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
isFilterBitmap = true
|
||||
style = Paint.Style.FILL
|
||||
textSize = 12 * dp
|
||||
}
|
||||
}
|
||||
|
||||
val drawRect = Rect()
|
||||
val drawRectF = RectF()
|
||||
|
||||
/**需要在那个方法中触发绘制*/
|
||||
var drawType = DRAW_TYPE_ON_DRAW_AFTER
|
||||
|
||||
/**xml属性读取*/
|
||||
open fun initAttribute(
|
||||
context: Context,
|
||||
attributeSet: AttributeSet? = null
|
||||
) {
|
||||
//val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.xxx)
|
||||
//typedArray.recycle()
|
||||
}
|
||||
|
||||
//<editor-fold desc="View相关方法">
|
||||
|
||||
/**附着的[View]*/
|
||||
val attachView: View?
|
||||
get() = if (callback is View) callback as? View else null
|
||||
|
||||
val isInEditMode: Boolean
|
||||
get() = attachView?.isInEditMode ?: false
|
||||
val paddingLeft: Int
|
||||
get() = attachView?.paddingLeft ?: 0
|
||||
val paddingRight: Int
|
||||
get() = attachView?.paddingRight ?: 0
|
||||
val paddingTop: Int
|
||||
get() = attachView?.paddingTop ?: 0
|
||||
val paddingBottom: Int
|
||||
get() = attachView?.paddingBottom ?: 0
|
||||
val viewHeight: Int
|
||||
get() = attachView?.measuredHeight ?: 0
|
||||
val viewWidth: Int
|
||||
get() = attachView?.measuredWidth ?: 0
|
||||
val viewDrawHeight: Int
|
||||
get() = viewHeight - paddingTop - paddingBottom
|
||||
val viewDrawWidth: Int
|
||||
get() = viewWidth - paddingLeft - paddingRight
|
||||
|
||||
val isViewRtl: Boolean
|
||||
get() = attachView != null && ViewCompat.getLayoutDirection(attachView!!) == ViewCompat.LAYOUT_DIRECTION_RTL
|
||||
|
||||
//</editor-fold desc="View相关方法">
|
||||
|
||||
//<editor-fold desc="基类方法">
|
||||
|
||||
/**核心方法, 绘制*/
|
||||
override fun draw(canvas: Canvas) {
|
||||
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int {
|
||||
return super.getIntrinsicWidth()
|
||||
}
|
||||
|
||||
override fun getMinimumWidth(): Int {
|
||||
return super.getMinimumWidth()
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int {
|
||||
return super.getIntrinsicHeight()
|
||||
}
|
||||
|
||||
override fun getMinimumHeight(): Int {
|
||||
return super.getMinimumHeight()
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
if (textPaint.alpha != alpha) {
|
||||
textPaint.alpha = alpha
|
||||
invalidateSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAlpha(): Int {
|
||||
return textPaint.alpha
|
||||
}
|
||||
|
||||
override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.setBounds(left, top, right, bottom)
|
||||
}
|
||||
|
||||
override fun setBounds(bounds: Rect) {
|
||||
super.setBounds(bounds)
|
||||
}
|
||||
|
||||
//不透明度
|
||||
override fun getOpacity(): Int {
|
||||
return if (alpha < 255) PixelFormat.TRANSLUCENT else PixelFormat.OPAQUE
|
||||
}
|
||||
|
||||
override fun getColorFilter(): ColorFilter? {
|
||||
return textPaint.colorFilter
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
textPaint.colorFilter = colorFilter
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun mutate(): Drawable {
|
||||
return super.mutate()
|
||||
}
|
||||
|
||||
override fun setDither(dither: Boolean) {
|
||||
textPaint.isDither = dither
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun setFilterBitmap(filter: Boolean) {
|
||||
textPaint.isFilterBitmap = filter
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun isFilterBitmap(): Boolean {
|
||||
return textPaint.isFilterBitmap
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
super.onBoundsChange(bounds)
|
||||
}
|
||||
|
||||
override fun onLevelChange(level: Int): Boolean {
|
||||
return super.onLevelChange(level)
|
||||
}
|
||||
|
||||
override fun onStateChange(state: IntArray): Boolean {
|
||||
return super.onStateChange(state)
|
||||
}
|
||||
|
||||
override fun setTintList(tint: ColorStateList?) {
|
||||
super.setTintList(tint)
|
||||
}
|
||||
|
||||
override fun setTintMode(tintMode: PorterDuff.Mode?) {
|
||||
super.setTintMode(tintMode)
|
||||
}
|
||||
|
||||
override fun setTintBlendMode(blendMode: BlendMode?) {
|
||||
super.setTintBlendMode(blendMode)
|
||||
}
|
||||
|
||||
override fun setHotspot(x: Float, y: Float) {
|
||||
super.setHotspot(x, y)
|
||||
}
|
||||
|
||||
override fun setHotspotBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.setHotspotBounds(left, top, right, bottom)
|
||||
}
|
||||
|
||||
override fun getHotspotBounds(outRect: Rect) {
|
||||
super.getHotspotBounds(outRect)
|
||||
}
|
||||
|
||||
//</editor-fold desc="基类方法">
|
||||
}
|
275
TabLayout/src/main/java/com/angcyo/tablayout/DslBadgeDrawable.kt
Normal file
275
TabLayout/src/main/java/com/angcyo/tablayout/DslBadgeDrawable.kt
Normal file
@ -0,0 +1,275 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.text.TextUtils
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* 未读数, 未读小红点, 角标绘制Drawable
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/12/13
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
open class DslBadgeDrawable : DslGradientDrawable() {
|
||||
|
||||
val dslGravity = DslGravity()
|
||||
|
||||
/**重力*/
|
||||
var badgeGravity: Int = Gravity.CENTER //Gravity.TOP or Gravity.RIGHT
|
||||
|
||||
/**角标文本颜色*/
|
||||
var badgeTextColor = Color.WHITE
|
||||
|
||||
/**角标的文本(默认居中绘制文本,暂不支持修改), 空字符串会绘制成小圆点
|
||||
* null 不绘制角标
|
||||
* "" 空字符绘制圆点
|
||||
* 其他 正常绘制
|
||||
* */
|
||||
var badgeText: String? = null
|
||||
|
||||
/**角标的文本大小*/
|
||||
var badgeTextSize: Float = 12 * dp
|
||||
set(value) {
|
||||
field = value
|
||||
textPaint.textSize = field
|
||||
}
|
||||
|
||||
/**当[badgeText]只有1个字符时, 使用圆形背景*/
|
||||
var badgeAutoCircle: Boolean = true
|
||||
|
||||
/**圆点状态时的半径大小*/
|
||||
var badgeCircleRadius = 4 * dpi
|
||||
|
||||
/**原点状态下, 单独配置的偏移*/
|
||||
var badgeCircleOffsetX: Int = 0
|
||||
var badgeCircleOffsetY: Int = 0
|
||||
|
||||
/**额外偏移距离, 会根据[Gravity]自动取负值*/
|
||||
var badgeOffsetX: Int = 0
|
||||
var badgeOffsetY: Int = 0
|
||||
|
||||
/**文本偏移*/
|
||||
var badgeTextOffsetX: Int = 0
|
||||
var badgeTextOffsetY: Int = 0
|
||||
|
||||
/**圆点状态时无效*/
|
||||
var badgePaddingLeft = 0
|
||||
var badgePaddingRight = 0
|
||||
var badgePaddingTop = 0
|
||||
var badgePaddingBottom = 0
|
||||
|
||||
/**最小的高度大小, px. 大于0生效; 圆点时属性无效*/
|
||||
var badgeMinHeight = -2
|
||||
|
||||
/**最小的宽度大小, px. 大于0生效; 圆点时属性无效;
|
||||
* -1 表示使用使用计算出来的高度值*/
|
||||
var badgeMinWidth = -2
|
||||
|
||||
//计算属性
|
||||
val textWidth: Float
|
||||
get() = textPaint.textWidth(badgeText)
|
||||
|
||||
//最大的宽度
|
||||
val maxWidth: Int
|
||||
get() = max(
|
||||
textWidth.toInt(),
|
||||
originDrawable?.minimumWidth ?: 0
|
||||
) + badgePaddingLeft + badgePaddingRight
|
||||
|
||||
//最大的高度
|
||||
val maxHeight: Int
|
||||
get() = max(
|
||||
textHeight.toInt(),
|
||||
originDrawable?.minimumHeight ?: 0
|
||||
) + badgePaddingTop + badgePaddingBottom
|
||||
|
||||
val textHeight: Float
|
||||
get() = textPaint.textHeight()
|
||||
|
||||
//原型状态
|
||||
val isCircle: Boolean
|
||||
get() = TextUtils.isEmpty(badgeText)
|
||||
|
||||
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||
super.initAttribute(context, attributeSet)
|
||||
updateOriginDrawable()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
//super.draw(canvas)
|
||||
|
||||
if (badgeText == null) {
|
||||
return
|
||||
}
|
||||
|
||||
with(dslGravity) {
|
||||
gravity = if (isViewRtl) {
|
||||
when (badgeGravity) {
|
||||
Gravity.RIGHT -> {
|
||||
Gravity.LEFT
|
||||
}
|
||||
Gravity.LEFT -> {
|
||||
Gravity.RIGHT
|
||||
}
|
||||
else -> {
|
||||
badgeGravity
|
||||
}
|
||||
}
|
||||
} else {
|
||||
badgeGravity
|
||||
}
|
||||
|
||||
setGravityBounds(bounds)
|
||||
|
||||
if (isCircle) {
|
||||
gravityOffsetX = badgeCircleOffsetX
|
||||
gravityOffsetY = badgeCircleOffsetY
|
||||
} else {
|
||||
gravityOffsetX = badgeOffsetX
|
||||
gravityOffsetY = badgeOffsetY
|
||||
}
|
||||
|
||||
val textWidth = textPaint.textWidth(badgeText)
|
||||
val textHeight = textPaint.textHeight()
|
||||
|
||||
val drawHeight = if (isCircle) {
|
||||
badgeCircleRadius.toFloat()
|
||||
} else {
|
||||
val height = textHeight + badgePaddingTop + badgePaddingBottom
|
||||
if (badgeMinHeight > 0) {
|
||||
max(height, badgeMinHeight.toFloat())
|
||||
} else {
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
val drawWidth = if (isCircle) {
|
||||
badgeCircleRadius.toFloat()
|
||||
} else {
|
||||
val width = textWidth + badgePaddingLeft + badgePaddingRight
|
||||
if (badgeMinWidth == -1) {
|
||||
max(width, drawHeight)
|
||||
} else if (badgeMinWidth > 0) {
|
||||
max(width, badgeMinWidth.toFloat())
|
||||
} else {
|
||||
width
|
||||
}
|
||||
}
|
||||
|
||||
applyGravity(drawWidth, drawHeight) { centerX, centerY ->
|
||||
|
||||
if (isCircle) {
|
||||
textPaint.color = gradientSolidColor
|
||||
|
||||
//圆心计算
|
||||
val cx: Float
|
||||
val cy: Float
|
||||
if (gravity.isCenter()) {
|
||||
cx = centerX.toFloat()
|
||||
cy = centerY.toFloat()
|
||||
} else {
|
||||
cx = centerX.toFloat() + _gravityOffsetX
|
||||
cy = centerY.toFloat() + _gravityOffsetY
|
||||
}
|
||||
|
||||
//绘制圆
|
||||
textPaint.color = gradientSolidColor
|
||||
canvas.drawCircle(
|
||||
cx,
|
||||
cy,
|
||||
badgeCircleRadius.toFloat(),
|
||||
textPaint
|
||||
)
|
||||
|
||||
//圆的描边
|
||||
if (gradientStrokeWidth > 0 && gradientStrokeColor != Color.TRANSPARENT) {
|
||||
val oldWidth = textPaint.strokeWidth
|
||||
val oldStyle = textPaint.style
|
||||
|
||||
textPaint.color = gradientStrokeColor
|
||||
textPaint.strokeWidth = gradientStrokeWidth.toFloat()
|
||||
textPaint.style = Paint.Style.STROKE
|
||||
|
||||
canvas.drawCircle(
|
||||
cx,
|
||||
cy,
|
||||
badgeCircleRadius.toFloat(),
|
||||
textPaint
|
||||
)
|
||||
|
||||
textPaint.strokeWidth = oldWidth
|
||||
textPaint.style = oldStyle
|
||||
}
|
||||
|
||||
} else {
|
||||
textPaint.color = badgeTextColor
|
||||
|
||||
val textDrawX: Float = centerX - textWidth / 2
|
||||
val textDrawY: Float = centerY + textHeight / 2
|
||||
|
||||
val bgLeft = _gravityLeft
|
||||
val bgTop = _gravityTop
|
||||
|
||||
//绘制背景
|
||||
if (badgeAutoCircle && badgeText?.length == 1) {
|
||||
if (gradientSolidColor != Color.TRANSPARENT) {
|
||||
textPaint.color = gradientSolidColor
|
||||
canvas.drawCircle(
|
||||
centerX.toFloat(),
|
||||
centerY.toFloat(),
|
||||
max(maxWidth, maxHeight).toFloat() / 2,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
} else {
|
||||
originDrawable?.apply {
|
||||
setBounds(
|
||||
bgLeft, bgTop,
|
||||
(bgLeft + drawWidth).toInt(),
|
||||
(bgTop + drawHeight).toInt()
|
||||
)
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
//绘制文本
|
||||
textPaint.color = badgeTextColor
|
||||
canvas.drawText(
|
||||
badgeText!!,
|
||||
textDrawX + badgeTextOffsetX,
|
||||
textDrawY - textPaint.descent() + badgeTextOffsetY,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int {
|
||||
val width = if (isCircle) {
|
||||
badgeCircleRadius * 2
|
||||
} else if (badgeAutoCircle && badgeText?.length == 1) {
|
||||
max(maxWidth, maxHeight)
|
||||
} else {
|
||||
maxWidth
|
||||
}
|
||||
return max(badgeMinWidth, width)
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int {
|
||||
val height = if (isCircle) {
|
||||
badgeCircleRadius * 2
|
||||
} else if (badgeAutoCircle && badgeText?.length == 1) {
|
||||
max(maxWidth, maxHeight)
|
||||
} else {
|
||||
maxHeight
|
||||
}
|
||||
return max(badgeMinHeight, height)
|
||||
}
|
||||
}
|
@ -0,0 +1,306 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import androidx.annotation.IntDef
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 用来构建GradientDrawable
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/11/27
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
open class DslGradientDrawable : AbsDslDrawable() {
|
||||
|
||||
/**形状*/
|
||||
@Shape
|
||||
var gradientShape = GradientDrawable.RECTANGLE
|
||||
|
||||
/**填充的颜色*/
|
||||
var gradientSolidColor = Color.TRANSPARENT
|
||||
|
||||
/**边框的颜色*/
|
||||
var gradientStrokeColor = Color.TRANSPARENT
|
||||
|
||||
/**边框的宽度*/
|
||||
var gradientStrokeWidth = 0
|
||||
|
||||
/**蚂蚁线的宽度*/
|
||||
var gradientDashWidth = 0f
|
||||
|
||||
/**蚂蚁线之间的间距*/
|
||||
var gradientDashGap = 0f
|
||||
|
||||
/**
|
||||
* 四个角, 8个设置点的圆角信息
|
||||
* 从 左上y轴->左上x轴->右上x轴->右上y轴..., 开始设置.
|
||||
*/
|
||||
var gradientRadii = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
|
||||
|
||||
/**颜色渐变*/
|
||||
var gradientColors: IntArray? = null
|
||||
var gradientColorsOffsets: FloatArray? = null
|
||||
|
||||
/**渐变中心点坐标*/
|
||||
var gradientCenterX = 0.5f
|
||||
var gradientCenterY = 0.5f
|
||||
|
||||
/**渐变半径, 非比例值, 是px值. [GradientDrawable.RADIAL_GRADIENT]类型才有效*/
|
||||
var gradientRadius = 0.5f
|
||||
|
||||
/** 渐变方向, 默认从左到右 */
|
||||
var gradientOrientation = GradientDrawable.Orientation.LEFT_RIGHT
|
||||
|
||||
/** 渐变类型 */
|
||||
@GradientType
|
||||
var gradientType = GradientDrawable.LINEAR_GRADIENT
|
||||
|
||||
/**真正绘制的[Drawable]*/
|
||||
var originDrawable: Drawable? = null
|
||||
|
||||
/**宽度补偿*/
|
||||
var gradientWidthOffset: Int = 0
|
||||
|
||||
/**高度补偿*/
|
||||
var gradientHeightOffset: Int = 0
|
||||
|
||||
/**当前的配置, 是否能生成有效的[GradientDrawable]*/
|
||||
open fun isValidConfig(): Boolean {
|
||||
return gradientSolidColor != Color.TRANSPARENT ||
|
||||
gradientStrokeColor != Color.TRANSPARENT ||
|
||||
gradientColors != null
|
||||
}
|
||||
|
||||
fun _fillRadii(array: FloatArray, radii: String?) {
|
||||
if (radii.isNullOrEmpty()) {
|
||||
return
|
||||
}
|
||||
val split = radii.split(",")
|
||||
if (split.size != 8) {
|
||||
throw IllegalArgumentException("radii 需要8个值.")
|
||||
} else {
|
||||
val dp = Resources.getSystem().displayMetrics.density
|
||||
for (i in split.indices) {
|
||||
array[i] = split[i].toFloat() * dp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun fillRadii(radius: Float) {
|
||||
Arrays.fill(gradientRadii, radius)
|
||||
}
|
||||
|
||||
fun fillRadii(radius: Int) {
|
||||
_fillRadii(gradientRadii, radius.toFloat())
|
||||
}
|
||||
|
||||
fun _fillRadii(array: FloatArray, radius: Float) {
|
||||
Arrays.fill(array, radius)
|
||||
}
|
||||
|
||||
fun _fillRadii(array: FloatArray, radius: Int) {
|
||||
_fillRadii(array, radius.toFloat())
|
||||
}
|
||||
|
||||
fun _fillColor(colors: String?): IntArray? {
|
||||
if (colors.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
val split = colors.split(",")
|
||||
|
||||
return IntArray(split.size) {
|
||||
val str = split[it]
|
||||
if (str.startsWith("#")) {
|
||||
Color.parseColor(str)
|
||||
} else {
|
||||
str.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**构建或者更新[originDrawable]*/
|
||||
open fun updateOriginDrawable(): GradientDrawable? {
|
||||
val drawable: GradientDrawable? = when (originDrawable) {
|
||||
null -> GradientDrawable()
|
||||
is GradientDrawable -> originDrawable as GradientDrawable
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
drawable?.apply {
|
||||
bounds = this@DslGradientDrawable.bounds
|
||||
|
||||
shape = gradientShape
|
||||
setStroke(
|
||||
gradientStrokeWidth,
|
||||
gradientStrokeColor,
|
||||
gradientDashWidth,
|
||||
gradientDashGap
|
||||
)
|
||||
setColor(gradientSolidColor)
|
||||
cornerRadii = gradientRadii
|
||||
|
||||
if (gradientColors != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
setGradientCenter(
|
||||
this@DslGradientDrawable.gradientCenterX,
|
||||
this@DslGradientDrawable.gradientCenterY
|
||||
)
|
||||
}
|
||||
gradientRadius = this@DslGradientDrawable.gradientRadius
|
||||
gradientType = this@DslGradientDrawable.gradientType
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
orientation = gradientOrientation
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
setColors(gradientColors, gradientColorsOffsets)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
colors = gradientColors
|
||||
}
|
||||
}
|
||||
|
||||
originDrawable = this
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
return drawable
|
||||
}
|
||||
|
||||
open fun configDrawable(config: DslGradientDrawable.() -> Unit): DslGradientDrawable {
|
||||
this.config()
|
||||
updateOriginDrawable()
|
||||
return this
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
super.draw(canvas)
|
||||
originDrawable?.apply {
|
||||
setBounds(
|
||||
this@DslGradientDrawable.bounds.left - gradientWidthOffset / 2,
|
||||
this@DslGradientDrawable.bounds.top - gradientHeightOffset / 2,
|
||||
this@DslGradientDrawable.bounds.right + gradientWidthOffset / 2,
|
||||
this@DslGradientDrawable.bounds.bottom + gradientHeightOffset / 2
|
||||
)
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
//<editor-fold desc="圆角相关配置">
|
||||
|
||||
/**
|
||||
* 4个角, 8个点 圆角配置
|
||||
*/
|
||||
fun cornerRadii(radii: FloatArray) {
|
||||
gradientRadii = radii
|
||||
}
|
||||
|
||||
fun cornerRadius(radii: Float) {
|
||||
Arrays.fill(gradientRadii, radii)
|
||||
}
|
||||
|
||||
fun cornerRadius(
|
||||
leftTop: Float = 0f,
|
||||
rightTop: Float = 0f,
|
||||
rightBottom: Float = 0f,
|
||||
leftBottom: Float = 0f
|
||||
) {
|
||||
gradientRadii[0] = leftTop
|
||||
gradientRadii[1] = leftTop
|
||||
gradientRadii[2] = rightTop
|
||||
gradientRadii[3] = rightTop
|
||||
gradientRadii[4] = rightBottom
|
||||
gradientRadii[5] = rightBottom
|
||||
gradientRadii[6] = leftBottom
|
||||
gradientRadii[7] = leftBottom
|
||||
}
|
||||
|
||||
/**
|
||||
* 只配置左边的圆角
|
||||
*/
|
||||
fun cornerRadiiLeft(radii: Float) {
|
||||
gradientRadii[0] = radii
|
||||
gradientRadii[1] = radii
|
||||
gradientRadii[6] = radii
|
||||
gradientRadii[7] = radii
|
||||
}
|
||||
|
||||
fun cornerRadiiRight(radii: Float) {
|
||||
gradientRadii[2] = radii
|
||||
gradientRadii[3] = radii
|
||||
gradientRadii[4] = radii
|
||||
gradientRadii[5] = radii
|
||||
}
|
||||
|
||||
fun cornerRadiiTop(radii: Float) {
|
||||
gradientRadii[0] = radii
|
||||
gradientRadii[1] = radii
|
||||
gradientRadii[2] = radii
|
||||
gradientRadii[3] = radii
|
||||
}
|
||||
|
||||
fun cornerRadiiBottom(radii: Float) {
|
||||
gradientRadii[4] = radii
|
||||
gradientRadii[5] = radii
|
||||
gradientRadii[6] = radii
|
||||
gradientRadii[7] = radii
|
||||
}
|
||||
|
||||
//</editor-fold desc="圆角相关配置">
|
||||
|
||||
//<editor-fold desc="传递属性">
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
super.setColorFilter(colorFilter)
|
||||
originDrawable?.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
override fun setTintList(tint: ColorStateList?) {
|
||||
super.setTintList(tint)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
originDrawable?.setTintList(tint)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setState(stateSet: IntArray): Boolean {
|
||||
return originDrawable?.setState(stateSet) ?: super.setState(stateSet)
|
||||
}
|
||||
|
||||
override fun getState(): IntArray {
|
||||
return originDrawable?.state ?: super.getState()
|
||||
}
|
||||
|
||||
//</editor-fold desc="传递属性">
|
||||
}
|
||||
|
||||
@IntDef(
|
||||
GradientDrawable.RECTANGLE,
|
||||
GradientDrawable.OVAL,
|
||||
GradientDrawable.LINE,
|
||||
GradientDrawable.RING
|
||||
)
|
||||
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
||||
annotation class Shape
|
||||
|
||||
@IntDef(
|
||||
GradientDrawable.LINEAR_GRADIENT,
|
||||
GradientDrawable.RADIAL_GRADIENT,
|
||||
GradientDrawable.SWEEP_GRADIENT
|
||||
)
|
||||
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
||||
annotation class GradientType
|
||||
|
||||
/**快速创建[GradientDrawable]*/
|
||||
fun dslGradientDrawable(action: DslGradientDrawable.() -> Unit): GradientDrawable {
|
||||
return DslGradientDrawable().run {
|
||||
action()
|
||||
updateOriginDrawable()!!
|
||||
}
|
||||
}
|
215
TabLayout/src/main/java/com/angcyo/tablayout/DslGravity.kt
Normal file
215
TabLayout/src/main/java/com/angcyo/tablayout/DslGravity.kt
Normal file
@ -0,0 +1,215 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.view.Gravity
|
||||
|
||||
/**
|
||||
* [android.view.Gravity] 辅助计算类
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/12/13
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
class DslGravity {
|
||||
|
||||
/**束缚范围*/
|
||||
val gravityBounds: RectF = RectF()
|
||||
|
||||
/**束缚重力*/
|
||||
var gravity: Int = Gravity.LEFT or Gravity.TOP
|
||||
|
||||
/**使用中心坐标作为定位参考, 否则就是四条边*/
|
||||
var gravityRelativeCenter: Boolean = true
|
||||
|
||||
/**额外偏移距离, 会根据[Gravity]自动取负值*/
|
||||
var gravityOffsetX: Int = 0
|
||||
var gravityOffsetY: Int = 0
|
||||
|
||||
fun setGravityBounds(rectF: RectF) {
|
||||
gravityBounds.set(rectF)
|
||||
}
|
||||
|
||||
fun setGravityBounds(rect: Rect) {
|
||||
gravityBounds.set(rect)
|
||||
}
|
||||
|
||||
fun setGravityBounds(
|
||||
left: Int,
|
||||
top: Int,
|
||||
right: Int,
|
||||
bottom: Int
|
||||
) {
|
||||
gravityBounds.set(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
|
||||
}
|
||||
|
||||
fun setGravityBounds(
|
||||
left: Float,
|
||||
top: Float,
|
||||
right: Float,
|
||||
bottom: Float
|
||||
) {
|
||||
gravityBounds.set(left, top, right, bottom)
|
||||
}
|
||||
|
||||
//计算后的属性
|
||||
var _horizontalGravity: Int = Gravity.LEFT
|
||||
var _verticalGravity: Int = Gravity.TOP
|
||||
var _isCenterGravity: Boolean = false
|
||||
var _targetWidth = 0f
|
||||
var _targetHeight = 0f
|
||||
var _gravityLeft = 0
|
||||
var _gravityTop = 0
|
||||
var _gravityRight = 0
|
||||
var _gravityBottom = 0
|
||||
|
||||
//根据gravity调整后的offset
|
||||
var _gravityOffsetX = 0
|
||||
var _gravityOffsetY = 0
|
||||
|
||||
/**根据[gravity]返回在[gravityBounds]中的[left] [top]位置*/
|
||||
fun applyGravity(
|
||||
width: Float = _targetWidth,
|
||||
height: Float = _targetHeight,
|
||||
callback: (centerX: Int, centerY: Int) -> Unit
|
||||
) {
|
||||
|
||||
_targetWidth = width
|
||||
_targetHeight = height
|
||||
|
||||
val layoutDirection = 0
|
||||
val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)
|
||||
val verticalGravity = gravity and Gravity.VERTICAL_GRAVITY_MASK
|
||||
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||
|
||||
//调整offset
|
||||
_gravityOffsetX = when (horizontalGravity) {
|
||||
Gravity.RIGHT -> -gravityOffsetX
|
||||
Gravity.END -> -gravityOffsetX
|
||||
else -> gravityOffsetX
|
||||
}
|
||||
_gravityOffsetY = when (verticalGravity) {
|
||||
Gravity.BOTTOM -> -gravityOffsetY
|
||||
else -> gravityOffsetY
|
||||
}
|
||||
|
||||
//计算居中的坐标
|
||||
val centerX = when (horizontalGravity) {
|
||||
Gravity.CENTER_HORIZONTAL -> (gravityBounds.left + gravityBounds.width() / 2 + _gravityOffsetX).toInt()
|
||||
Gravity.RIGHT -> (gravityBounds.right + _gravityOffsetX - if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
|
||||
Gravity.END -> (gravityBounds.right + _gravityOffsetX - if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
|
||||
else -> (gravityBounds.left + _gravityOffsetX + if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
|
||||
}
|
||||
|
||||
val centerY = when (verticalGravity) {
|
||||
Gravity.CENTER_VERTICAL -> (gravityBounds.top + gravityBounds.height() / 2 + _gravityOffsetY).toInt()
|
||||
Gravity.BOTTOM -> (gravityBounds.bottom + _gravityOffsetY - if (gravityRelativeCenter) 0f else _targetHeight / 2).toInt()
|
||||
else -> (gravityBounds.top + _gravityOffsetY + if (gravityRelativeCenter) 0f else _targetHeight / 2).toInt()
|
||||
}
|
||||
|
||||
//缓存
|
||||
_horizontalGravity = horizontalGravity
|
||||
_verticalGravity = verticalGravity
|
||||
_isCenterGravity = horizontalGravity == Gravity.CENTER_HORIZONTAL &&
|
||||
verticalGravity == Gravity.CENTER_VERTICAL
|
||||
|
||||
_gravityLeft = (centerX - _targetWidth / 2).toInt()
|
||||
_gravityRight = (centerX + _targetWidth / 2).toInt()
|
||||
_gravityTop = (centerY - _targetHeight / 2).toInt()
|
||||
_gravityBottom = (centerY + _targetHeight / 2).toInt()
|
||||
|
||||
//回调
|
||||
callback(centerX, centerY)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认计算出的是目标中心点坐标参考距离
|
||||
* [com.angcyo.drawable.DslGravity.getGravityRelativeCenter]
|
||||
* */
|
||||
fun dslGravity(
|
||||
rect: RectF, //计算的矩形
|
||||
gravity: Int, //重力
|
||||
width: Float, //放置目标的宽度
|
||||
height: Float, //放置目标的高度
|
||||
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
|
||||
offsetY: Int = 0,
|
||||
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
|
||||
): DslGravity {
|
||||
val _dslGravity = DslGravity()
|
||||
_dslGravity.setGravityBounds(rect)
|
||||
_config(_dslGravity, gravity, width, height, offsetX, offsetY, callback)
|
||||
return _dslGravity
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认计算出的是目标中心点坐标参考距离
|
||||
* [com.angcyo.drawable.DslGravity.getGravityRelativeCenter]
|
||||
* */
|
||||
fun dslGravity(
|
||||
rect: Rect, //计算的矩形
|
||||
gravity: Int, //重力
|
||||
width: Float, //放置目标的宽度
|
||||
height: Float, //放置目标的高度
|
||||
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
|
||||
offsetY: Int = 0,
|
||||
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
|
||||
): DslGravity {
|
||||
val _dslGravity = DslGravity()
|
||||
_dslGravity.setGravityBounds(rect)
|
||||
_config(_dslGravity, gravity, width, height, offsetX, offsetY, callback)
|
||||
return _dslGravity
|
||||
}
|
||||
|
||||
private fun _config(
|
||||
_dslGravity: DslGravity,
|
||||
gravity: Int, //重力
|
||||
width: Float, //放置目标的宽度
|
||||
height: Float, //放置目标的高度
|
||||
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
|
||||
offsetY: Int = 0,
|
||||
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
|
||||
) {
|
||||
_dslGravity.gravity = gravity
|
||||
_dslGravity.gravityOffsetX = offsetX
|
||||
_dslGravity.gravityOffsetY = offsetY
|
||||
_dslGravity.applyGravity(width, height) { centerX, centerY ->
|
||||
callback(_dslGravity, centerX, centerY)
|
||||
}
|
||||
}
|
||||
|
||||
/**居中*/
|
||||
fun Int.isCenter(): Boolean {
|
||||
val layoutDirection = 0
|
||||
val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection)
|
||||
val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK
|
||||
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||
|
||||
return verticalGravity == Gravity.CENTER_VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL
|
||||
}
|
||||
|
||||
fun Int.isLeft(): Boolean {
|
||||
val layoutDirection = 0
|
||||
val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection)
|
||||
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||
|
||||
return horizontalGravity == Gravity.LEFT
|
||||
}
|
||||
|
||||
fun Int.isRight(): Boolean {
|
||||
val layoutDirection = 0
|
||||
val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection)
|
||||
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||
|
||||
return horizontalGravity == Gravity.RIGHT
|
||||
}
|
||||
|
||||
fun Int.isTop(): Boolean {
|
||||
val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK
|
||||
return verticalGravity == Gravity.TOP
|
||||
}
|
||||
|
||||
fun Int.isBottom(): Boolean {
|
||||
val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK
|
||||
return verticalGravity == Gravity.BOTTOM
|
||||
}
|
438
TabLayout/src/main/java/com/angcyo/tablayout/DslSelector.kt
Normal file
438
TabLayout/src/main/java/com/angcyo/tablayout/DslSelector.kt
Normal file
@ -0,0 +1,438 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
|
||||
/**
|
||||
* 用来操作[ViewGroup]中的[child], 支持单选, 多选, 拦截.
|
||||
* 操作的都是可见性为[VISIBLE]的[View]
|
||||
*
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/11/24
|
||||
*/
|
||||
|
||||
open class DslSelector {
|
||||
|
||||
var parent: ViewGroup? = null
|
||||
var dslSelectorConfig: DslSelectorConfig = DslSelectorConfig()
|
||||
|
||||
//可见view列表
|
||||
val visibleViewList: MutableList<View> = mutableListOf()
|
||||
|
||||
/**
|
||||
* 选中的索引列表
|
||||
* */
|
||||
val selectorIndexList: MutableList<Int> = mutableListOf()
|
||||
get() {
|
||||
field.clear()
|
||||
visibleViewList.forEachIndexed { index, view ->
|
||||
if (view.isSe()) {
|
||||
field.add(index)
|
||||
}
|
||||
}
|
||||
|
||||
return field
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中的View列表
|
||||
* */
|
||||
val selectorViewList: MutableList<View> = mutableListOf()
|
||||
get() {
|
||||
field.clear()
|
||||
visibleViewList.forEachIndexed { index, view ->
|
||||
if (view.isSe() || index == dslSelectIndex) {
|
||||
field.add(view)
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
//child 点击事件处理
|
||||
val _onChildClickListener = View.OnClickListener {
|
||||
val index = visibleViewList.indexOf(it)
|
||||
val select =
|
||||
if (dslSelectorConfig.dslMultiMode || dslSelectorConfig.dslMinSelectLimit < 1) {
|
||||
!it.isSe()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
|
||||
if (!interceptSelector(index, select, true)) {
|
||||
selector(
|
||||
visibleViewList.indexOf(it),
|
||||
select,
|
||||
notify = true,
|
||||
fromUser = true,
|
||||
forceNotify = it is CompoundButton && dslSelectorConfig.dslMultiMode
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**兼容[CompoundButton]*/
|
||||
val _onCheckedChangeListener = CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
|
||||
buttonView.isChecked = buttonView.isSelected //恢复状态 不做任何处理, 在[OnClickListener]中处理
|
||||
/*val index = visibleViewList.indexOf(buttonView)
|
||||
|
||||
if (interceptSelector(index, isChecked, false)) {
|
||||
//拦截了此操作
|
||||
buttonView.isChecked = !isChecked //恢复状态
|
||||
}
|
||||
|
||||
val selectorViewList = selectorViewList
|
||||
val sum = selectorViewList.size
|
||||
//Limit 过滤
|
||||
if (buttonView.isChecked) {
|
||||
if (sum > dslSelectorConfig.dslMaxSelectLimit) {
|
||||
//不允许选择
|
||||
buttonView.isChecked = false //恢复状态
|
||||
}
|
||||
} else {
|
||||
//取消选择, 检查是否达到了 limit
|
||||
if (sum < dslSelectorConfig.dslMinSelectLimit) {
|
||||
//不允许取消选择
|
||||
buttonView.isChecked = true //恢复状态
|
||||
}
|
||||
}
|
||||
|
||||
if (isChecked) {
|
||||
//已经选中了控件
|
||||
} else {
|
||||
//已经取消了控件
|
||||
}*/
|
||||
}
|
||||
|
||||
/**当前选中的索引*/
|
||||
var dslSelectIndex = -1
|
||||
|
||||
/**安装*/
|
||||
fun install(viewGroup: ViewGroup, config: DslSelectorConfig.() -> Unit = {}): DslSelector {
|
||||
dslSelectIndex = -1
|
||||
parent = viewGroup
|
||||
updateVisibleList()
|
||||
dslSelectorConfig.config()
|
||||
|
||||
updateStyle()
|
||||
updateClickListener()
|
||||
|
||||
if (dslSelectIndex in 0 until visibleViewList.size) {
|
||||
selector(dslSelectIndex)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**更新样式*/
|
||||
fun updateStyle() {
|
||||
visibleViewList.forEachIndexed { index, view ->
|
||||
val selector = dslSelectIndex == index || view.isSe()
|
||||
dslSelectorConfig.onStyleItemView(view, index, selector)
|
||||
}
|
||||
}
|
||||
|
||||
/**更新child的点击事件*/
|
||||
fun updateClickListener() {
|
||||
parent?.apply {
|
||||
for (i in 0 until childCount) {
|
||||
getChildAt(i)?.let {
|
||||
it.setOnClickListener(_onChildClickListener)
|
||||
if (it is CompoundButton) {
|
||||
it.setOnCheckedChangeListener(_onCheckedChangeListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**更新可见视图列表*/
|
||||
fun updateVisibleList(): List<View> {
|
||||
visibleViewList.clear()
|
||||
parent?.apply {
|
||||
for (i in 0 until childCount) {
|
||||
getChildAt(i)?.let {
|
||||
if (it.visibility == View.VISIBLE) {
|
||||
visibleViewList.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dslSelectIndex in visibleViewList.indices) {
|
||||
if (!visibleViewList[dslSelectIndex].isSe()) {
|
||||
visibleViewList[dslSelectIndex].setSe(true)
|
||||
}
|
||||
} else {
|
||||
//如果当前选中的索引, 不在可见视图列表中
|
||||
dslSelectIndex = -1
|
||||
}
|
||||
return visibleViewList
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作单个
|
||||
* @param index 操作目标的索引值
|
||||
* @param select 选中 or 取消选中
|
||||
* @param notify 是否需要通知事件
|
||||
* @param forceNotify 是否强制通知事件.child使用[CompoundButton]时, 推荐使用
|
||||
* */
|
||||
fun selector(
|
||||
index: Int,
|
||||
select: Boolean = true,
|
||||
notify: Boolean = true,
|
||||
fromUser: Boolean = false,
|
||||
forceNotify: Boolean = false
|
||||
) {
|
||||
val oldSelectorList = selectorIndexList.toList()
|
||||
val lastSelectorIndex: Int? = oldSelectorList.lastOrNull()
|
||||
val reselect = !dslSelectorConfig.dslMultiMode &&
|
||||
oldSelectorList.isNotEmpty() &&
|
||||
oldSelectorList.contains(index)
|
||||
|
||||
var needNotify = _selector(index, select, fromUser) || forceNotify
|
||||
|
||||
if (!oldSelectorList.isChange(selectorIndexList)) {
|
||||
//选中项, 未改变时不通知
|
||||
needNotify = false
|
||||
}
|
||||
|
||||
if (needNotify || reselect) {
|
||||
dslSelectIndex = selectorIndexList.lastOrNull() ?: -1
|
||||
if (notify) {
|
||||
notifySelectChange(lastSelectorIndex ?: -1, reselect, fromUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**选择所有
|
||||
* [select] true:选择所有, false:取消所有*/
|
||||
fun selectorAll(
|
||||
select: Boolean = true,
|
||||
notify: Boolean = true,
|
||||
fromUser: Boolean = true
|
||||
) {
|
||||
val indexList = visibleViewList.mapIndexedTo(mutableListOf()) { index, _ ->
|
||||
index
|
||||
}
|
||||
selector(indexList, select, notify, fromUser)
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作多个
|
||||
* @param select 选中 or 取消选中
|
||||
* [selector]
|
||||
* */
|
||||
fun selector(
|
||||
indexList: MutableList<Int>,
|
||||
select: Boolean = true,
|
||||
notify: Boolean = true,
|
||||
fromUser: Boolean = false
|
||||
) {
|
||||
val oldSelectorIndexList = selectorIndexList
|
||||
val lastSelectorIndex: Int? = oldSelectorIndexList.lastOrNull()
|
||||
|
||||
var result = false
|
||||
|
||||
indexList.forEach {
|
||||
result = _selector(it, select, fromUser) || result
|
||||
}
|
||||
|
||||
if (result) {
|
||||
dslSelectIndex = selectorIndexList.lastOrNull() ?: -1
|
||||
if (notify) {
|
||||
notifySelectChange(lastSelectorIndex ?: -1, false, fromUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**通知事件*/
|
||||
fun notifySelectChange(lastSelectorIndex: Int, reselect: Boolean, fromUser: Boolean) {
|
||||
val indexSelectorList = selectorIndexList
|
||||
dslSelectorConfig.onSelectViewChange(
|
||||
visibleViewList.getOrNull(lastSelectorIndex),
|
||||
selectorViewList,
|
||||
reselect,
|
||||
fromUser
|
||||
)
|
||||
dslSelectorConfig.onSelectIndexChange(
|
||||
lastSelectorIndex,
|
||||
indexSelectorList,
|
||||
reselect,
|
||||
fromUser
|
||||
)
|
||||
}
|
||||
|
||||
/**当前的操作是否被拦截*/
|
||||
fun interceptSelector(index: Int, select: Boolean, fromUser: Boolean): Boolean {
|
||||
val visibleViewList = visibleViewList
|
||||
if (index !in 0 until visibleViewList.size) {
|
||||
return true
|
||||
}
|
||||
return dslSelectorConfig.onSelectItemView(visibleViewList[index], index, select, fromUser)
|
||||
}
|
||||
|
||||
/**@return 是否发生过改变*/
|
||||
fun _selector(index: Int, select: Boolean, fromUser: Boolean): Boolean {
|
||||
val visibleViewList = visibleViewList
|
||||
//超范围过滤
|
||||
if (index !in 0 until visibleViewList.size) {
|
||||
"index out of list.".logi()
|
||||
return false
|
||||
}
|
||||
|
||||
val selectorIndexList = selectorIndexList
|
||||
val selectorViewList = selectorViewList
|
||||
|
||||
if (selectorIndexList.isNotEmpty()) {
|
||||
if (select) {
|
||||
//需要选中某项
|
||||
|
||||
if (dslSelectorConfig.dslMultiMode) {
|
||||
//多选模式
|
||||
if (selectorIndexList.contains(index)) {
|
||||
//已经选中
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
//单选模式
|
||||
|
||||
//取消之前选中
|
||||
selectorIndexList.forEach {
|
||||
if (it != index) {
|
||||
visibleViewList[it].setSe(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectorIndexList.contains(index)) {
|
||||
//已经选中
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
//需要取消选中
|
||||
if (!selectorIndexList.contains(index)) {
|
||||
//目标已经是未选中
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Limit 过滤
|
||||
if (select) {
|
||||
val sum = selectorViewList.size + 1
|
||||
if (sum > dslSelectorConfig.dslMaxSelectLimit) {
|
||||
//不允许选择
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
//取消选择, 检查是否达到了 limit
|
||||
val sum = selectorViewList.size - 1
|
||||
if (sum < dslSelectorConfig.dslMinSelectLimit) {
|
||||
//不允许取消选择
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
val selectorView = visibleViewList[index]
|
||||
|
||||
//更新选中样式
|
||||
selectorView.setSe(select)
|
||||
|
||||
if (dslSelectorConfig.dslMultiMode) {
|
||||
//多选
|
||||
} else {
|
||||
//单选
|
||||
|
||||
//取消之前
|
||||
selectorViewList.forEach { view ->
|
||||
//更新样式
|
||||
val indexOf = visibleViewList.indexOf(view)
|
||||
if (indexOf != index &&
|
||||
!dslSelectorConfig.onSelectItemView(view, indexOf, false, fromUser)
|
||||
) {
|
||||
view.setSe(false)
|
||||
dslSelectorConfig.onStyleItemView(view, indexOf, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dslSelectorConfig.onStyleItemView(selectorView, index, select)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**是否选中状态*/
|
||||
fun View.isSe(): Boolean {
|
||||
return isSelected || if (this is CompoundButton) isChecked else false
|
||||
}
|
||||
|
||||
fun View.setSe(se: Boolean) {
|
||||
isSelected = se
|
||||
if (this is CompoundButton) isChecked = se
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dsl配置项
|
||||
* */
|
||||
open class DslSelectorConfig {
|
||||
|
||||
/**取消选择时, 最小需要保持多个选中. 可以决定单选时, 是否允许取消所有选中*/
|
||||
var dslMinSelectLimit = 1
|
||||
|
||||
/**多选时, 最大允许多个选中*/
|
||||
var dslMaxSelectLimit = Int.MAX_VALUE
|
||||
|
||||
/**是否是多选模式*/
|
||||
var dslMultiMode: Boolean = false
|
||||
|
||||
/**
|
||||
* 用来初始化[itemView]的样式
|
||||
* [onSelectItemView]
|
||||
* */
|
||||
var onStyleItemView: (itemView: View, index: Int, select: Boolean) -> Unit =
|
||||
{ _, _, _ ->
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中[View]改变回调, 优先于[onSelectIndexChange]触发, 区别在于参数类型不一样
|
||||
* @param fromView 单选模式下有效, 表示之前选中的[View]
|
||||
* @param reselect 是否是重复选择, 只在单选模式下有效
|
||||
* @param fromUser 是否是用户产生的回调, 而非代码设置
|
||||
* */
|
||||
var onSelectViewChange: (fromView: View?, selectViewList: List<View>, reselect: Boolean, fromUser: Boolean) -> Unit =
|
||||
{ _, _, _, _ ->
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中改变回调
|
||||
* [onSelectViewChange]
|
||||
* @param fromIndex 单选模式下有效, 表示之前选中的[View], 在可见性[child]列表中的索引
|
||||
* */
|
||||
var onSelectIndexChange: (fromIndex: Int, selectIndexList: List<Int>, reselect: Boolean, fromUser: Boolean) -> Unit =
|
||||
{ fromIndex, selectList, reselect, fromUser ->
|
||||
"选择:[$fromIndex]->${selectList} reselect:$reselect fromUser:$fromUser".logi()
|
||||
}
|
||||
|
||||
/**
|
||||
* 当需要选中[itemView]时回调, 返回[true]表示拦截默认处理
|
||||
* @param itemView 操作的[View]
|
||||
* @param index [itemView]在可见性view列表中的索引. 非ViewGroup中的索引
|
||||
* @param select 选中 or 取消选中
|
||||
* @return true 表示拦截默认处理
|
||||
* */
|
||||
var onSelectItemView: (itemView: View, index: Int, select: Boolean, fromUser: Boolean) -> Boolean =
|
||||
{ _, _, _, _ ->
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**[DslSelector]组件*/
|
||||
fun dslSelector(viewGroup: ViewGroup, config: DslSelectorConfig.() -> Unit = {}): DslSelector {
|
||||
return DslSelector().apply {
|
||||
install(viewGroup, config)
|
||||
}
|
||||
}
|
222
TabLayout/src/main/java/com/angcyo/tablayout/DslTabBadge.kt
Normal file
222
TabLayout/src/main/java/com/angcyo/tablayout/DslTabBadge.kt
Normal file
@ -0,0 +1,222 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import androidx.annotation.Px
|
||||
|
||||
/**
|
||||
* 角标
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/12/13
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
open class DslTabBadge : DslBadgeDrawable() {
|
||||
|
||||
/**角标默认配置项*/
|
||||
val defaultBadgeConfig = TabBadgeConfig()
|
||||
|
||||
/**预览的角标属性*/
|
||||
var xmlBadgeText: String? = null
|
||||
|
||||
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||
val typedArray =
|
||||
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||
gradientSolidColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_badge_solid_color,
|
||||
defaultBadgeConfig.badgeSolidColor
|
||||
)
|
||||
defaultBadgeConfig.badgeSolidColor = gradientSolidColor
|
||||
|
||||
badgeTextColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_badge_text_color,
|
||||
defaultBadgeConfig.badgeTextColor
|
||||
)
|
||||
defaultBadgeConfig.badgeTextColor = badgeTextColor
|
||||
|
||||
gradientStrokeColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_badge_stroke_color,
|
||||
defaultBadgeConfig.badgeStrokeColor
|
||||
)
|
||||
defaultBadgeConfig.badgeStrokeColor = gradientStrokeColor
|
||||
|
||||
gradientStrokeWidth =
|
||||
typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_stroke_width,
|
||||
defaultBadgeConfig.badgeStrokeWidth
|
||||
)
|
||||
defaultBadgeConfig.badgeStrokeWidth = gradientStrokeWidth
|
||||
|
||||
badgeGravity = typedArray.getInt(
|
||||
R.styleable.DslTabLayout_tab_badge_gravity,
|
||||
defaultBadgeConfig.badgeGravity
|
||||
)
|
||||
defaultBadgeConfig.badgeGravity = badgeGravity
|
||||
|
||||
badgeOffsetX = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_offset_x,
|
||||
defaultBadgeConfig.badgeOffsetX
|
||||
)
|
||||
defaultBadgeConfig.badgeOffsetX = badgeOffsetX
|
||||
badgeOffsetY = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_offset_y,
|
||||
defaultBadgeConfig.badgeOffsetY
|
||||
)
|
||||
defaultBadgeConfig.badgeOffsetY = badgeOffsetY
|
||||
|
||||
badgeCircleOffsetX = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_circle_offset_x,
|
||||
defaultBadgeConfig.badgeOffsetX
|
||||
)
|
||||
defaultBadgeConfig.badgeCircleOffsetX = badgeCircleOffsetX
|
||||
badgeCircleOffsetY = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_circle_offset_y,
|
||||
defaultBadgeConfig.badgeOffsetY
|
||||
)
|
||||
defaultBadgeConfig.badgeCircleOffsetY = badgeCircleOffsetY
|
||||
|
||||
badgeCircleRadius = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_circle_radius,
|
||||
defaultBadgeConfig.badgeCircleRadius
|
||||
)
|
||||
defaultBadgeConfig.badgeCircleRadius = badgeCircleRadius
|
||||
|
||||
val badgeRadius = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_radius,
|
||||
defaultBadgeConfig.badgeRadius
|
||||
)
|
||||
cornerRadius(badgeRadius.toFloat())
|
||||
defaultBadgeConfig.badgeRadius = badgeRadius
|
||||
|
||||
badgePaddingLeft = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_padding_left,
|
||||
defaultBadgeConfig.badgePaddingLeft
|
||||
)
|
||||
defaultBadgeConfig.badgePaddingLeft = badgePaddingLeft
|
||||
|
||||
badgePaddingRight = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_padding_right,
|
||||
defaultBadgeConfig.badgePaddingRight
|
||||
)
|
||||
defaultBadgeConfig.badgePaddingRight = badgePaddingRight
|
||||
|
||||
badgePaddingTop = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_padding_top,
|
||||
defaultBadgeConfig.badgePaddingTop
|
||||
)
|
||||
defaultBadgeConfig.badgePaddingTop = badgePaddingTop
|
||||
|
||||
badgePaddingBottom = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_padding_bottom,
|
||||
defaultBadgeConfig.badgePaddingBottom
|
||||
)
|
||||
defaultBadgeConfig.badgePaddingBottom = badgePaddingBottom
|
||||
|
||||
xmlBadgeText = typedArray.getString(R.styleable.DslTabLayout_tab_badge_text)
|
||||
|
||||
badgeTextSize = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_badge_text_size,
|
||||
defaultBadgeConfig.badgeTextSize.toInt()
|
||||
).toFloat()
|
||||
defaultBadgeConfig.badgeTextSize = badgeTextSize
|
||||
|
||||
defaultBadgeConfig.badgeAnchorChildIndex =
|
||||
typedArray.getInteger(
|
||||
R.styleable.DslTabLayout_tab_badge_anchor_child_index,
|
||||
defaultBadgeConfig.badgeAnchorChildIndex
|
||||
)
|
||||
defaultBadgeConfig.badgeIgnoreChildPadding = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_badge_ignore_child_padding,
|
||||
defaultBadgeConfig.badgeIgnoreChildPadding
|
||||
)
|
||||
|
||||
defaultBadgeConfig.badgeMinWidth = typedArray.getLayoutDimension(
|
||||
R.styleable.DslTabLayout_tab_badge_min_width,
|
||||
defaultBadgeConfig.badgeMinWidth
|
||||
)
|
||||
|
||||
defaultBadgeConfig.badgeMinHeight = typedArray.getLayoutDimension(
|
||||
R.styleable.DslTabLayout_tab_badge_min_height,
|
||||
defaultBadgeConfig.badgeMinHeight
|
||||
)
|
||||
|
||||
typedArray.recycle()
|
||||
super.initAttribute(context, attributeSet)
|
||||
}
|
||||
|
||||
/**使用指定配置, 更新[DslBadgeDrawable]*/
|
||||
fun updateBadgeConfig(badgeConfig: TabBadgeConfig) {
|
||||
gradientSolidColor = badgeConfig.badgeSolidColor
|
||||
gradientStrokeColor = badgeConfig.badgeStrokeColor
|
||||
gradientStrokeWidth = badgeConfig.badgeStrokeWidth
|
||||
badgeTextColor = badgeConfig.badgeTextColor
|
||||
badgeGravity = badgeConfig.badgeGravity
|
||||
badgeOffsetX = badgeConfig.badgeOffsetX
|
||||
badgeOffsetY = badgeConfig.badgeOffsetY
|
||||
badgeCircleOffsetX = badgeConfig.badgeCircleOffsetX
|
||||
badgeCircleOffsetY = badgeConfig.badgeCircleOffsetY
|
||||
badgeCircleRadius = badgeConfig.badgeCircleRadius
|
||||
badgePaddingLeft = badgeConfig.badgePaddingLeft
|
||||
badgePaddingRight = badgeConfig.badgePaddingRight
|
||||
badgePaddingTop = badgeConfig.badgePaddingTop
|
||||
badgePaddingBottom = badgeConfig.badgePaddingBottom
|
||||
badgeTextSize = badgeConfig.badgeTextSize
|
||||
cornerRadius(badgeConfig.badgeRadius.toFloat())
|
||||
badgeMinHeight = badgeConfig.badgeMinHeight
|
||||
badgeMinWidth = badgeConfig.badgeMinWidth
|
||||
badgeText = badgeConfig.badgeText
|
||||
}
|
||||
}
|
||||
|
||||
/**角标绘制参数配置*/
|
||||
data class TabBadgeConfig(
|
||||
/**角标的文本(默认居中绘制文本,暂不支持修改), 空字符串会绘制成小圆点
|
||||
* null 不绘制角标
|
||||
* "" 空字符绘制圆点
|
||||
* 其他 正常绘制
|
||||
* */
|
||||
var badgeText: String? = null,
|
||||
/**重力*/
|
||||
var badgeGravity: Int = Gravity.CENTER,
|
||||
/**角标背景颜色*/
|
||||
var badgeSolidColor: Int = Color.RED,
|
||||
/**角标边框颜色*/
|
||||
var badgeStrokeColor: Int = Color.TRANSPARENT,
|
||||
/**角标边框宽度*/
|
||||
var badgeStrokeWidth: Int = 0,
|
||||
|
||||
/**角标文本颜色*/
|
||||
var badgeTextColor: Int = Color.WHITE,
|
||||
/**角标文本字体大小*/
|
||||
@Px
|
||||
var badgeTextSize: Float = 12 * dp,
|
||||
/**圆点状态时的半径大小*/
|
||||
var badgeCircleRadius: Int = 4 * dpi,
|
||||
/**圆角大小*/
|
||||
var badgeRadius: Int = 10 * dpi,
|
||||
/**额外偏移距离, 会根据[Gravity]自动取负值*/
|
||||
var badgeOffsetX: Int = 0,
|
||||
var badgeOffsetY: Int = 0,
|
||||
var badgeCircleOffsetX: Int = 0,
|
||||
var badgeCircleOffsetY: Int = 0,
|
||||
/**圆点状态时无效*/
|
||||
var badgePaddingLeft: Int = 4 * dpi,
|
||||
var badgePaddingRight: Int = 4 * dpi,
|
||||
var badgePaddingTop: Int = 0,
|
||||
var badgePaddingBottom: Int = 0,
|
||||
|
||||
var badgeAnchorChildIndex: Int = -1,
|
||||
var badgeIgnoreChildPadding: Boolean = true,
|
||||
|
||||
/**最小的高度大小, px. 大于0生效; 圆点时属性无效*/
|
||||
var badgeMinHeight: Int = -2,
|
||||
|
||||
/**最小的宽度大小, px. 大于0生效; 圆点时属性无效;
|
||||
* -1 表示使用使用计算出来的高度值*/
|
||||
var badgeMinWidth: Int = -1
|
||||
)
|
279
TabLayout/src/main/java/com/angcyo/tablayout/DslTabBorder.kt
Normal file
279
TabLayout/src/main/java/com/angcyo/tablayout/DslTabBorder.kt
Normal file
@ -0,0 +1,279 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.view.ViewCompat
|
||||
|
||||
/**
|
||||
* 边框绘制, 支持首尾圆角中间不圆角的样式
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/11/27
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
open class DslTabBorder : DslGradientDrawable() {
|
||||
|
||||
/**
|
||||
* 是否要接管[itemView]背景的绘制
|
||||
* [updateItemBackground]
|
||||
* */
|
||||
var borderDrawItemBackground: Boolean = true
|
||||
|
||||
/**是否保持每个[itemView]的圆角都一样, 否则首尾有圆角, 中间没有圆角*/
|
||||
var borderKeepItemRadius: Boolean = false
|
||||
|
||||
var borderBackgroundDrawable: Drawable? = null
|
||||
|
||||
/**宽度补偿*/
|
||||
var borderBackgroundWidthOffset: Int = 0
|
||||
|
||||
/**高度补偿*/
|
||||
var borderBackgroundHeightOffset: Int = 0
|
||||
|
||||
/**强制指定选中item的背景颜色*/
|
||||
var borderItemBackgroundSolidColor: Int? = null
|
||||
|
||||
/**当item不可选中时的背景绘制颜色
|
||||
* [com.angcyo.tablayout.DslTabLayout.itemEnableSelector]
|
||||
* [borderItemBackgroundSolidColor]*/
|
||||
var borderItemBackgroundSolidDisableColor: Int? = null
|
||||
|
||||
/**强制指定选中item的背景渐变颜色*/
|
||||
var borderItemBackgroundGradientColors: IntArray? = null
|
||||
|
||||
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||
val typedArray =
|
||||
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||
|
||||
val borderBackgroundColor =
|
||||
typedArray.getColor(R.styleable.DslTabLayout_tab_border_solid_color, gradientSolidColor)
|
||||
|
||||
gradientStrokeColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_border_stroke_color,
|
||||
gradientStrokeColor
|
||||
)
|
||||
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_border_stroke_width,
|
||||
2 * dpi
|
||||
)
|
||||
val radiusSize =
|
||||
typedArray.getDimensionPixelOffset(R.styleable.DslTabLayout_tab_border_radius_size, 0)
|
||||
|
||||
cornerRadius(radiusSize.toFloat())
|
||||
|
||||
originDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_border_drawable)
|
||||
|
||||
borderDrawItemBackground = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_border_draw_item_background,
|
||||
borderDrawItemBackground
|
||||
)
|
||||
|
||||
borderKeepItemRadius = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_border_keep_item_radius,
|
||||
borderKeepItemRadius
|
||||
)
|
||||
|
||||
borderBackgroundWidthOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_border_item_background_width_offset,
|
||||
borderBackgroundWidthOffset
|
||||
)
|
||||
|
||||
borderBackgroundHeightOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_border_item_background_height_offset,
|
||||
borderBackgroundHeightOffset
|
||||
)
|
||||
|
||||
//
|
||||
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_solid_color)) {
|
||||
borderItemBackgroundSolidColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_border_item_background_solid_color,
|
||||
gradientStrokeColor
|
||||
)
|
||||
}
|
||||
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_solid_disable_color)) {
|
||||
borderItemBackgroundSolidDisableColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_border_item_background_solid_disable_color,
|
||||
borderItemBackgroundSolidColor ?: gradientStrokeColor
|
||||
)
|
||||
}
|
||||
|
||||
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_gradient_start_color) ||
|
||||
typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_gradient_end_color)
|
||||
) {
|
||||
val startColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_border_item_background_gradient_start_color,
|
||||
gradientStrokeColor
|
||||
)
|
||||
val endColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_border_item_background_gradient_end_color,
|
||||
gradientStrokeColor
|
||||
)
|
||||
borderItemBackgroundGradientColors = intArrayOf(startColor, endColor)
|
||||
}
|
||||
|
||||
typedArray.recycle()
|
||||
|
||||
if (originDrawable == null) {
|
||||
//无自定义的drawable, 那么自绘.
|
||||
borderBackgroundDrawable = DslGradientDrawable().configDrawable {
|
||||
gradientSolidColor = borderBackgroundColor
|
||||
gradientRadii = this@DslTabBorder.gradientRadii
|
||||
}.originDrawable
|
||||
|
||||
updateOriginDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
super.draw(canvas)
|
||||
|
||||
originDrawable?.apply {
|
||||
setBounds(
|
||||
paddingLeft,
|
||||
paddingBottom,
|
||||
viewWidth - paddingRight,
|
||||
viewHeight - paddingBottom
|
||||
)
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
fun drawBorderBackground(canvas: Canvas) {
|
||||
super.draw(canvas)
|
||||
|
||||
borderBackgroundDrawable?.apply {
|
||||
setBounds(
|
||||
paddingLeft,
|
||||
paddingBottom,
|
||||
viewWidth - paddingRight,
|
||||
viewHeight - paddingBottom
|
||||
)
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
var itemSelectBgDrawable: Drawable? = null
|
||||
var itemDeselectBgDrawable: Drawable? = null
|
||||
|
||||
/**开启边框绘制后, [itemView]的背景也需要负责设置*/
|
||||
open fun updateItemBackground(
|
||||
tabLayout: DslTabLayout,
|
||||
itemView: View,
|
||||
index: Int,
|
||||
select: Boolean
|
||||
) {
|
||||
|
||||
if (!borderDrawItemBackground) {
|
||||
return
|
||||
}
|
||||
|
||||
if (select) {
|
||||
|
||||
val isFirst = index == 0
|
||||
val isLast = index == tabLayout.dslSelector.visibleViewList.size - 1
|
||||
|
||||
val drawable = DslGradientDrawable().configDrawable {
|
||||
gradientWidthOffset = borderBackgroundWidthOffset
|
||||
gradientHeightOffset = borderBackgroundHeightOffset
|
||||
|
||||
gradientSolidColor =
|
||||
borderItemBackgroundSolidColor ?: this@DslTabBorder.gradientStrokeColor
|
||||
if (!tabLayout.itemEnableSelector) {
|
||||
if (borderItemBackgroundSolidDisableColor != null) {
|
||||
gradientSolidColor = borderItemBackgroundSolidDisableColor!!
|
||||
}
|
||||
}
|
||||
|
||||
gradientColors = borderItemBackgroundGradientColors
|
||||
|
||||
if ((isFirst && isLast) || borderKeepItemRadius) {
|
||||
//只有一个child
|
||||
gradientRadii = this@DslTabBorder.gradientRadii
|
||||
} else if (isFirst) {
|
||||
if (tabLayout.isHorizontal()) {
|
||||
if (tabLayout.isLayoutRtl) {
|
||||
gradientRadii = floatArrayOf(
|
||||
0f,
|
||||
0f,
|
||||
this@DslTabBorder.gradientRadii[2],
|
||||
this@DslTabBorder.gradientRadii[3],
|
||||
this@DslTabBorder.gradientRadii[4],
|
||||
this@DslTabBorder.gradientRadii[5],
|
||||
0f,
|
||||
0f
|
||||
)
|
||||
} else {
|
||||
gradientRadii = floatArrayOf(
|
||||
this@DslTabBorder.gradientRadii[0],
|
||||
this@DslTabBorder.gradientRadii[1],
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
this@DslTabBorder.gradientRadii[6],
|
||||
this@DslTabBorder.gradientRadii[7]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
gradientRadii = floatArrayOf(
|
||||
this@DslTabBorder.gradientRadii[0],
|
||||
this@DslTabBorder.gradientRadii[1],
|
||||
this@DslTabBorder.gradientRadii[2],
|
||||
this@DslTabBorder.gradientRadii[3],
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f
|
||||
)
|
||||
}
|
||||
} else if (isLast) {
|
||||
if (tabLayout.isHorizontal()) {
|
||||
if (tabLayout.isLayoutRtl) {
|
||||
gradientRadii = floatArrayOf(
|
||||
this@DslTabBorder.gradientRadii[0],
|
||||
this@DslTabBorder.gradientRadii[1],
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
this@DslTabBorder.gradientRadii[6],
|
||||
this@DslTabBorder.gradientRadii[7]
|
||||
)
|
||||
} else {
|
||||
gradientRadii = floatArrayOf(
|
||||
0f,
|
||||
0f,
|
||||
this@DslTabBorder.gradientRadii[2],
|
||||
this@DslTabBorder.gradientRadii[3],
|
||||
this@DslTabBorder.gradientRadii[4],
|
||||
this@DslTabBorder.gradientRadii[5],
|
||||
0f,
|
||||
0f
|
||||
)
|
||||
}
|
||||
} else {
|
||||
gradientRadii = floatArrayOf(
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
this@DslTabBorder.gradientRadii[4],
|
||||
this@DslTabBorder.gradientRadii[5],
|
||||
this@DslTabBorder.gradientRadii[6],
|
||||
this@DslTabBorder.gradientRadii[7]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemSelectBgDrawable = drawable
|
||||
|
||||
ViewCompat.setBackground(itemView, itemSelectBgDrawable)
|
||||
} else {
|
||||
ViewCompat.setBackground(itemView, itemDeselectBgDrawable)
|
||||
}
|
||||
}
|
||||
}
|
153
TabLayout/src/main/java/com/angcyo/tablayout/DslTabDivider.kt
Normal file
153
TabLayout/src/main/java/com/angcyo/tablayout/DslTabDivider.kt
Normal file
@ -0,0 +1,153 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.widget.LinearLayout
|
||||
|
||||
/**
|
||||
* 垂直分割线
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/11/27
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
open class DslTabDivider : DslGradientDrawable() {
|
||||
|
||||
var dividerWidth = 2 * dpi
|
||||
var dividerHeight = 2 * dpi
|
||||
var dividerMarginLeft = 0
|
||||
var dividerMarginRight = 0
|
||||
var dividerMarginTop = 0
|
||||
var dividerMarginBottom = 0
|
||||
|
||||
/**
|
||||
* [LinearLayout.SHOW_DIVIDER_BEGINNING]
|
||||
* [LinearLayout.SHOW_DIVIDER_MIDDLE]
|
||||
* [LinearLayout.SHOW_DIVIDER_END]
|
||||
* */
|
||||
var dividerShowMode = LinearLayout.SHOW_DIVIDER_MIDDLE
|
||||
|
||||
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||
super.initAttribute(context, attributeSet)
|
||||
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||
|
||||
dividerWidth = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_divider_width,
|
||||
dividerWidth
|
||||
)
|
||||
dividerHeight = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_divider_height,
|
||||
dividerHeight
|
||||
)
|
||||
dividerMarginLeft = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_divider_margin_left,
|
||||
dividerMarginLeft
|
||||
)
|
||||
dividerMarginRight = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_divider_margin_right,
|
||||
dividerMarginRight
|
||||
)
|
||||
dividerMarginTop = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_divider_margin_top,
|
||||
dividerMarginTop
|
||||
)
|
||||
dividerMarginBottom = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_divider_margin_bottom,
|
||||
dividerMarginBottom
|
||||
)
|
||||
|
||||
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_divider_solid_color)) {
|
||||
gradientSolidColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_divider_solid_color,
|
||||
gradientSolidColor
|
||||
)
|
||||
} else if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_stroke_color)) {
|
||||
gradientSolidColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_border_stroke_color,
|
||||
gradientSolidColor
|
||||
)
|
||||
} else {
|
||||
gradientSolidColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_deselect_color,
|
||||
gradientSolidColor
|
||||
)
|
||||
}
|
||||
|
||||
gradientStrokeColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_divider_stroke_color,
|
||||
gradientStrokeColor
|
||||
)
|
||||
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_divider_stroke_width,
|
||||
0
|
||||
)
|
||||
val radiusSize =
|
||||
typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_divider_radius_size,
|
||||
2 * dpi
|
||||
)
|
||||
|
||||
cornerRadius(radiusSize.toFloat())
|
||||
|
||||
originDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_divider_drawable)
|
||||
|
||||
dividerShowMode =
|
||||
typedArray.getInt(R.styleable.DslTabLayout_tab_divider_show_mode, dividerShowMode)
|
||||
|
||||
typedArray.recycle()
|
||||
|
||||
if (originDrawable == null) {
|
||||
//无自定义的drawable, 那么自绘.
|
||||
|
||||
updateOriginDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
super.draw(canvas)
|
||||
|
||||
originDrawable?.apply {
|
||||
bounds = this@DslTabDivider.bounds
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
val _tabLayout: DslTabLayout?
|
||||
get() = if (callback is DslTabLayout) callback as DslTabLayout else null
|
||||
|
||||
/**
|
||||
* [childIndex]位置前面是否需要分割线
|
||||
* */
|
||||
open fun haveBeforeDivider(childIndex: Int, childCount: Int): Boolean {
|
||||
val tabLayout = _tabLayout
|
||||
if (tabLayout != null && tabLayout.isHorizontal() && tabLayout.isLayoutRtl) {
|
||||
if (childIndex == 0) {
|
||||
return dividerShowMode and LinearLayout.SHOW_DIVIDER_END != 0
|
||||
}
|
||||
return dividerShowMode and LinearLayout.SHOW_DIVIDER_MIDDLE != 0
|
||||
}
|
||||
|
||||
if (childIndex == 0) {
|
||||
return dividerShowMode and LinearLayout.SHOW_DIVIDER_BEGINNING != 0
|
||||
}
|
||||
return dividerShowMode and LinearLayout.SHOW_DIVIDER_MIDDLE != 0
|
||||
}
|
||||
|
||||
/**
|
||||
* [childIndex]位置后面是否需要分割线
|
||||
* */
|
||||
open fun haveAfterDivider(childIndex: Int, childCount: Int): Boolean {
|
||||
val tabLayout = _tabLayout
|
||||
if (tabLayout != null && tabLayout.isHorizontal() && tabLayout.isLayoutRtl) {
|
||||
if (childIndex == childCount - 1) {
|
||||
return dividerShowMode and LinearLayout.SHOW_DIVIDER_BEGINNING != 0
|
||||
}
|
||||
}
|
||||
|
||||
if (childIndex == childCount - 1) {
|
||||
return dividerShowMode and LinearLayout.SHOW_DIVIDER_END != 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
118
TabLayout/src/main/java/com/angcyo/tablayout/DslTabHighlight.kt
Normal file
118
TabLayout/src/main/java/com/angcyo/tablayout/DslTabHighlight.kt
Normal file
@ -0,0 +1,118 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
|
||||
/**
|
||||
*
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2021/05/19
|
||||
* Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved.
|
||||
*/
|
||||
open class DslTabHighlight(val tabLayout: DslTabLayout) : DslGradientDrawable() {
|
||||
|
||||
/**需要绘制的Drawable*/
|
||||
var highlightDrawable: Drawable? = null
|
||||
|
||||
/**宽度测量模式*/
|
||||
var highlightWidth = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|
||||
/**高度测量模式*/
|
||||
var highlightHeight = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|
||||
/**宽度补偿*/
|
||||
var highlightWidthOffset = 0
|
||||
|
||||
/**高度补偿*/
|
||||
var highlightHeightOffset = 0
|
||||
|
||||
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||
//super.initAttribute(context, attributeSet)
|
||||
|
||||
val typedArray =
|
||||
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||
highlightDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_highlight_drawable)
|
||||
|
||||
highlightWidth = typedArray.getLayoutDimension(
|
||||
R.styleable.DslTabLayout_tab_highlight_width,
|
||||
highlightWidth
|
||||
)
|
||||
highlightHeight = typedArray.getLayoutDimension(
|
||||
R.styleable.DslTabLayout_tab_highlight_height,
|
||||
highlightHeight
|
||||
)
|
||||
|
||||
highlightWidthOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_highlight_width_offset,
|
||||
highlightWidthOffset
|
||||
)
|
||||
highlightHeightOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_highlight_height_offset,
|
||||
highlightHeightOffset
|
||||
)
|
||||
|
||||
typedArray.recycle()
|
||||
|
||||
if (highlightDrawable == null && isValidConfig()) {
|
||||
updateOriginDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateOriginDrawable(): GradientDrawable? {
|
||||
val drawable = super.updateOriginDrawable()
|
||||
highlightDrawable = originDrawable
|
||||
return drawable
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
//super.draw(canvas)
|
||||
val itemView = tabLayout.currentItemView
|
||||
if (itemView != null) {
|
||||
val lp = itemView.layoutParams
|
||||
|
||||
if (lp is DslTabLayout.LayoutParams) {
|
||||
lp.highlightDrawable ?: highlightDrawable
|
||||
} else {
|
||||
highlightDrawable
|
||||
}?.apply {
|
||||
|
||||
val drawWidth: Int = when (highlightWidth) {
|
||||
ViewGroup.LayoutParams.MATCH_PARENT -> itemView.measuredWidth
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT -> intrinsicWidth
|
||||
else -> highlightWidth
|
||||
} + highlightWidthOffset
|
||||
|
||||
val drawHeight: Int = when (highlightHeight) {
|
||||
ViewGroup.LayoutParams.MATCH_PARENT -> itemView.measuredHeight
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT -> intrinsicHeight
|
||||
else -> highlightHeight
|
||||
} + highlightHeightOffset
|
||||
|
||||
val centerX: Int = itemView.left + (itemView.right - itemView.left) / 2
|
||||
val centerY: Int = itemView.top + (itemView.bottom - itemView.top) / 2
|
||||
|
||||
setBounds(
|
||||
centerX - drawWidth / 2,
|
||||
centerY - drawHeight / 2,
|
||||
centerX + drawWidth / 2,
|
||||
centerY + drawHeight / 2
|
||||
)
|
||||
|
||||
draw(canvas)
|
||||
canvas.save()
|
||||
if (tabLayout.isHorizontal()) {
|
||||
canvas.translate(itemView.left.toFloat(), 0f)
|
||||
} else {
|
||||
canvas.translate(0f, itemView.top.toFloat())
|
||||
}
|
||||
itemView.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
931
TabLayout/src/main/java/com/angcyo/tablayout/DslTabIndicator.kt
Normal file
931
TabLayout/src/main/java/com/angcyo/tablayout/DslTabIndicator.kt
Normal file
@ -0,0 +1,931 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.withSave
|
||||
import java.util.*
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* 指示器
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/11/25
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
open class DslTabIndicator(val tabLayout: DslTabLayout) : DslGradientDrawable() {
|
||||
|
||||
companion object {
|
||||
|
||||
/**非颜色值*/
|
||||
const val NO_COLOR = -2
|
||||
|
||||
//---style---
|
||||
|
||||
/**不绘制指示器*/
|
||||
const val INDICATOR_STYLE_NONE = 0
|
||||
|
||||
/**指示器绘制在[itemView]的顶部*/
|
||||
const val INDICATOR_STYLE_TOP = 0x1
|
||||
|
||||
/**指示器绘制在[itemView]的底部*/
|
||||
const val INDICATOR_STYLE_BOTTOM = 0x2
|
||||
|
||||
/**默认样式,指示器绘制在[itemView]的中心*/
|
||||
const val INDICATOR_STYLE_CENTER = 0x4
|
||||
|
||||
/**前景绘制,
|
||||
* 默认是背景绘制, 指示器绘制[itemView]的背部, [itemView] 请不要设置background, 否则可能看不见*/
|
||||
const val INDICATOR_STYLE_FOREGROUND = 0x1000
|
||||
|
||||
//---gravity---
|
||||
|
||||
/**指示器重力在开始的位置(横向左边, 纵向上边)*/
|
||||
const val INDICATOR_GRAVITY_START = 0x1
|
||||
|
||||
/**指示器重力在结束的位置(横向右边, 纵向下边)*/
|
||||
const val INDICATOR_GRAVITY_END = 0x2
|
||||
|
||||
/**指示器重力在中间*/
|
||||
const val INDICATOR_GRAVITY_CENTER = 0x4
|
||||
}
|
||||
|
||||
/**指示器绘制的样式*/
|
||||
var indicatorStyle = INDICATOR_STYLE_NONE //初始化
|
||||
|
||||
/**[indicatorStyle]*/
|
||||
val _indicatorDrawStyle: Int
|
||||
get() = indicatorStyle.remove(INDICATOR_STYLE_FOREGROUND)
|
||||
|
||||
/**优先将指示器显示在[DslTabLayout]的什么位置
|
||||
* [INDICATOR_GRAVITY_START] 开始的位置
|
||||
* [INDICATOR_GRAVITY_END] 结束的位置
|
||||
* [INDICATOR_GRAVITY_CENTER] 中间的位置*/
|
||||
var indicatorGravity = INDICATOR_GRAVITY_CENTER
|
||||
|
||||
/**
|
||||
* 指示器在流向下一个位置时, 是否采用[Flow]流线的方式改变宽度
|
||||
* */
|
||||
var indicatorEnableFlow: Boolean = false
|
||||
|
||||
/**指示器闪现效果, 从当前位置直接跨越到目标位置*/
|
||||
var indicatorEnableFlash: Boolean = false
|
||||
|
||||
/**使用clip的方式绘制闪现效果*/
|
||||
var indicatorEnableFlashClip: Boolean = true
|
||||
|
||||
/**当目标和当前的索引差值<=此值时, [Flow]效果才有效*/
|
||||
var indicatorFlowStep: Int = 1
|
||||
|
||||
/**指示器绘制实体*/
|
||||
var indicatorDrawable: Drawable? = null
|
||||
set(value) {
|
||||
field = tintDrawableColor(value, indicatorColor)
|
||||
}
|
||||
|
||||
/**过滤[indicatorDrawable]的颜色*/
|
||||
var indicatorColor: Int = NO_COLOR
|
||||
set(value) {
|
||||
field = value
|
||||
indicatorDrawable = indicatorDrawable
|
||||
}
|
||||
|
||||
/**
|
||||
* 指示器的宽度
|
||||
* WRAP_CONTENT: [childView]内容的宽度,
|
||||
* MATCH_PARENT: [childView]的宽度
|
||||
* 40dp: 固定值
|
||||
* */
|
||||
var indicatorWidth = 0 //初始化
|
||||
|
||||
/**宽度补偿*/
|
||||
var indicatorWidthOffset = 0
|
||||
|
||||
/**
|
||||
* 指示器的高度
|
||||
* WRAP_CONTENT: [childView]内容的高度,
|
||||
* MATCH_PARENT: [childView]的高度
|
||||
* 40dp: 固定值
|
||||
* */
|
||||
var indicatorHeight = 0 //初始化
|
||||
|
||||
/**高度补偿*/
|
||||
var indicatorHeightOffset = 0
|
||||
|
||||
/**XY轴方向补偿*/
|
||||
var indicatorXOffset = 0
|
||||
|
||||
/**会根据[indicatorStyle]自动取负值*/
|
||||
var indicatorYOffset = 0
|
||||
|
||||
/**
|
||||
* 宽高[WRAP_CONTENT]时, 内容view的定位索引
|
||||
* */
|
||||
var indicatorContentIndex = -1
|
||||
var indicatorContentId = View.NO_ID
|
||||
|
||||
/**切换时是否需要动画的支持*/
|
||||
var indicatorAnim = true
|
||||
|
||||
/**在获取锚点view的宽高时, 是否需要忽略对应的padding属性*/
|
||||
var ignoreChildPadding: Boolean = true
|
||||
|
||||
init {
|
||||
callback = tabLayout
|
||||
}
|
||||
|
||||
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||
val typedArray =
|
||||
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||
|
||||
indicatorDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_indicator_drawable)
|
||||
indicatorColor =
|
||||
typedArray.getColor(R.styleable.DslTabLayout_tab_indicator_color, indicatorColor)
|
||||
indicatorStyle = typedArray.getInt(
|
||||
R.styleable.DslTabLayout_tab_indicator_style,
|
||||
if (tabLayout.isHorizontal()) INDICATOR_STYLE_BOTTOM else INDICATOR_STYLE_TOP
|
||||
)
|
||||
indicatorGravity = typedArray.getInt(
|
||||
R.styleable.DslTabLayout_tab_indicator_gravity,
|
||||
indicatorGravity
|
||||
)
|
||||
|
||||
//初始化指示器的高度和宽度
|
||||
if (indicatorStyle.have(INDICATOR_STYLE_FOREGROUND)) {
|
||||
//前景绘制
|
||||
indicatorWidth = typedArray.getLayoutDimension(
|
||||
R.styleable.DslTabLayout_tab_indicator_width,
|
||||
if (tabLayout.isHorizontal()) ViewGroup.LayoutParams.MATCH_PARENT else 3 * dpi
|
||||
)
|
||||
indicatorHeight = typedArray.getLayoutDimension(
|
||||
R.styleable.DslTabLayout_tab_indicator_height,
|
||||
if (tabLayout.isHorizontal()) 3 * dpi else ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
indicatorXOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_x_offset,
|
||||
if (tabLayout.isHorizontal()) 0 else 2 * dpi
|
||||
)
|
||||
indicatorYOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_y_offset,
|
||||
if (tabLayout.isHorizontal()) 2 * dpi else 0
|
||||
)
|
||||
} else {
|
||||
//背景绘制样式
|
||||
if (tabLayout.isHorizontal()) {
|
||||
indicatorWidth = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
indicatorHeight = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
} else {
|
||||
indicatorHeight = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
indicatorWidth = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
indicatorWidth = typedArray.getLayoutDimension(
|
||||
R.styleable.DslTabLayout_tab_indicator_width,
|
||||
indicatorWidth
|
||||
)
|
||||
indicatorHeight = typedArray.getLayoutDimension(
|
||||
R.styleable.DslTabLayout_tab_indicator_height,
|
||||
indicatorHeight
|
||||
)
|
||||
indicatorXOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_x_offset,
|
||||
indicatorXOffset
|
||||
)
|
||||
indicatorYOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_y_offset,
|
||||
indicatorYOffset
|
||||
)
|
||||
}
|
||||
|
||||
ignoreChildPadding = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_indicator_ignore_child_padding,
|
||||
!indicatorStyle.have(INDICATOR_STYLE_CENTER)
|
||||
)
|
||||
|
||||
indicatorFlowStep =
|
||||
typedArray.getInt(R.styleable.DslTabLayout_tab_indicator_flow_step, indicatorFlowStep)
|
||||
indicatorEnableFlow = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_indicator_enable_flow,
|
||||
indicatorEnableFlow
|
||||
)
|
||||
indicatorEnableFlash = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_indicator_enable_flash,
|
||||
indicatorEnableFlash
|
||||
)
|
||||
indicatorEnableFlashClip = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_indicator_enable_flash_clip,
|
||||
indicatorEnableFlashClip
|
||||
)
|
||||
|
||||
indicatorWidthOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_width_offset,
|
||||
indicatorWidthOffset
|
||||
)
|
||||
indicatorHeightOffset = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_height_offset,
|
||||
indicatorHeightOffset
|
||||
)
|
||||
indicatorContentIndex = typedArray.getInt(
|
||||
R.styleable.DslTabLayout_tab_indicator_content_index,
|
||||
indicatorContentIndex
|
||||
)
|
||||
indicatorContentId = typedArray.getResourceId(
|
||||
R.styleable.DslTabLayout_tab_indicator_content_id,
|
||||
indicatorContentId
|
||||
)
|
||||
indicatorAnim = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_indicator_anim,
|
||||
indicatorAnim
|
||||
)
|
||||
|
||||
//代码构建Drawable
|
||||
gradientShape =
|
||||
typedArray.getInt(R.styleable.DslTabLayout_tab_indicator_shape, gradientShape)
|
||||
gradientSolidColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_indicator_solid_color,
|
||||
gradientSolidColor
|
||||
)
|
||||
gradientStrokeColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_indicator_stroke_color,
|
||||
gradientStrokeColor
|
||||
)
|
||||
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_stroke_width,
|
||||
gradientStrokeWidth
|
||||
)
|
||||
gradientDashWidth = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_dash_width,
|
||||
gradientDashWidth.toInt()
|
||||
).toFloat()
|
||||
gradientDashGap = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_indicator_dash_gap,
|
||||
gradientDashGap.toInt()
|
||||
).toFloat()
|
||||
|
||||
val gradientRadius =
|
||||
typedArray.getDimensionPixelOffset(R.styleable.DslTabLayout_tab_indicator_radius, 0)
|
||||
if (gradientRadius > 0) {
|
||||
Arrays.fill(gradientRadii, gradientRadius.toFloat())
|
||||
} else {
|
||||
typedArray.getString(R.styleable.DslTabLayout_tab_indicator_radii)?.let {
|
||||
_fillRadii(gradientRadii, it)
|
||||
}
|
||||
}
|
||||
|
||||
val gradientColors =
|
||||
typedArray.getString(R.styleable.DslTabLayout_tab_indicator_gradient_colors)
|
||||
|
||||
this.gradientColors = if (gradientColors.isNullOrEmpty()) {
|
||||
val startColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_indicator_gradient_start_color,
|
||||
Color.TRANSPARENT
|
||||
)
|
||||
val endColor = typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_indicator_gradient_end_color,
|
||||
Color.TRANSPARENT
|
||||
)
|
||||
if (startColor != endColor) {
|
||||
intArrayOf(startColor, endColor)
|
||||
} else {
|
||||
this.gradientColors
|
||||
}
|
||||
} else {
|
||||
_fillColor(gradientColors) ?: this.gradientColors
|
||||
}
|
||||
//...end
|
||||
|
||||
typedArray.recycle()
|
||||
|
||||
if (indicatorDrawable == null && isValidConfig()) {
|
||||
updateOriginDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateOriginDrawable(): GradientDrawable? {
|
||||
val drawable = super.updateOriginDrawable()
|
||||
indicatorDrawable = originDrawable
|
||||
return drawable
|
||||
}
|
||||
|
||||
open fun tintDrawableColor(drawable: Drawable?, color: Int): Drawable? {
|
||||
if (drawable == null || color == NO_COLOR) {
|
||||
return drawable
|
||||
}
|
||||
return drawable.tintDrawableColor(color)
|
||||
}
|
||||
|
||||
/**指示器需要参考的目标控件*/
|
||||
open fun indicatorContentView(childView: View): View? {
|
||||
val lp = childView.layoutParams as DslTabLayout.LayoutParams
|
||||
|
||||
val contentId =
|
||||
if (lp.indicatorContentId != View.NO_ID) lp.indicatorContentId else indicatorContentId
|
||||
|
||||
if (contentId != View.NO_ID) {
|
||||
return childView.findViewById(contentId)
|
||||
}
|
||||
|
||||
//如果child强制指定了index, 就用指定的.
|
||||
val contentIndex =
|
||||
if (lp.indicatorContentIndex >= 0) lp.indicatorContentIndex else indicatorContentIndex
|
||||
|
||||
return if (contentIndex >= 0 && childView is ViewGroup && contentIndex in 0 until childView.childCount) {
|
||||
//有指定
|
||||
val contentChildView = childView.getChildAt(contentIndex)
|
||||
contentChildView
|
||||
} else {
|
||||
//没有指定
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**根据指定[index]索引, 获取目标[View]*/
|
||||
open fun targetChildView(
|
||||
index: Int,
|
||||
onChildView: (childView: View, contentChildView: View?) -> Unit
|
||||
) {
|
||||
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||
onChildView(childView, indicatorContentView(childView))
|
||||
}
|
||||
}
|
||||
|
||||
open fun getChildTargetPaddingLeft(childView: View): Int =
|
||||
if (ignoreChildPadding) childView.paddingLeft else 0
|
||||
|
||||
open fun getChildTargetPaddingRight(childView: View): Int =
|
||||
if (ignoreChildPadding) childView.paddingRight else 0
|
||||
|
||||
open fun getChildTargetPaddingTop(childView: View): Int =
|
||||
if (ignoreChildPadding) childView.paddingTop else 0
|
||||
|
||||
open fun getChildTargetPaddingBottom(childView: View): Int =
|
||||
if (ignoreChildPadding) childView.paddingBottom else 0
|
||||
|
||||
open fun getChildTargetWidth(childView: View): Int =
|
||||
if (ignoreChildPadding) childView.viewDrawWidth else childView.measuredWidth
|
||||
|
||||
open fun getChildTargetHeight(childView: View): Int =
|
||||
if (ignoreChildPadding) childView.viewDrawHeight else childView.measuredHeight
|
||||
|
||||
/**
|
||||
* [childview]对应的中心x坐标
|
||||
* */
|
||||
open fun getChildTargetX(index: Int, gravity: Int = indicatorGravity): Int {
|
||||
|
||||
var result = if (index > 0) tabLayout.maxWidth else 0
|
||||
|
||||
targetChildView(index) { childView, contentChildView ->
|
||||
result = if (contentChildView == null) {
|
||||
when (gravity) {
|
||||
INDICATOR_GRAVITY_START -> childView.left
|
||||
INDICATOR_GRAVITY_END -> childView.right
|
||||
else -> childView.left + getChildTargetPaddingLeft(childView) + getChildTargetWidth(
|
||||
childView
|
||||
) / 2
|
||||
}
|
||||
} else {
|
||||
when (gravity) {
|
||||
INDICATOR_GRAVITY_START -> childView.left + contentChildView.left
|
||||
INDICATOR_GRAVITY_END -> childView.left + contentChildView.right
|
||||
else -> childView.left + contentChildView.left + getChildTargetPaddingLeft(
|
||||
contentChildView
|
||||
) + getChildTargetWidth(
|
||||
contentChildView
|
||||
) / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
open fun getChildTargetY(index: Int, gravity: Int = indicatorGravity): Int {
|
||||
|
||||
var result = if (index > 0) tabLayout.maxHeight else 0
|
||||
|
||||
targetChildView(index) { childView, contentChildView ->
|
||||
result = if (contentChildView == null) {
|
||||
when (gravity) {
|
||||
INDICATOR_GRAVITY_START -> childView.top
|
||||
INDICATOR_GRAVITY_END -> childView.bottom
|
||||
else -> childView.top + getChildTargetPaddingTop(childView) + getChildTargetHeight(
|
||||
childView
|
||||
) / 2
|
||||
}
|
||||
} else {
|
||||
when (gravity) {
|
||||
INDICATOR_GRAVITY_START -> childView.top + contentChildView.top
|
||||
INDICATOR_GRAVITY_END -> childView.top + childView.bottom
|
||||
else -> childView.top + contentChildView.top + getChildTargetPaddingTop(
|
||||
contentChildView
|
||||
) + getChildTargetHeight(
|
||||
contentChildView
|
||||
) / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
open fun getIndicatorDrawWidth(index: Int): Int {
|
||||
var result = indicatorWidth
|
||||
|
||||
when (indicatorWidth) {
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT -> {
|
||||
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||
result = getChildTargetWidth(indicatorContentView(childView) ?: childView)
|
||||
}
|
||||
}
|
||||
ViewGroup.LayoutParams.MATCH_PARENT -> {
|
||||
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||
result = childView.measuredWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result + indicatorWidthOffset
|
||||
}
|
||||
|
||||
open fun getIndicatorDrawHeight(index: Int): Int {
|
||||
var result = indicatorHeight
|
||||
|
||||
when (indicatorHeight) {
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT -> {
|
||||
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||
result = getChildTargetHeight(indicatorContentView(childView) ?: childView)
|
||||
}
|
||||
}
|
||||
ViewGroup.LayoutParams.MATCH_PARENT -> {
|
||||
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||
result = childView.measuredHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result + indicatorHeightOffset
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
//super.draw(canvas)
|
||||
if (!isVisible || _indicatorDrawStyle == INDICATOR_STYLE_NONE || indicatorDrawable == null) {
|
||||
//不绘制
|
||||
return
|
||||
}
|
||||
|
||||
if (tabLayout.isHorizontal()) {
|
||||
drawHorizontal(canvas)
|
||||
} else {
|
||||
drawVertical(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
fun drawHorizontal(canvas: Canvas) {
|
||||
val childSize = tabLayout.dslSelector.visibleViewList.size
|
||||
|
||||
var currentIndex = currentIndex
|
||||
|
||||
if (_targetIndex in 0 until childSize) {
|
||||
currentIndex = max(0, currentIndex)
|
||||
}
|
||||
|
||||
if (currentIndex in 0 until childSize) {
|
||||
|
||||
} else {
|
||||
//无效的index
|
||||
return
|
||||
}
|
||||
|
||||
//"绘制$currentIndex:$currentSelectIndex $positionOffset".logi()
|
||||
|
||||
val drawTargetX = getChildTargetX(currentIndex)
|
||||
val drawWidth = getIndicatorDrawWidth(currentIndex)
|
||||
val drawHeight = getIndicatorDrawHeight(currentIndex)
|
||||
|
||||
val drawLeft = drawTargetX - drawWidth / 2 + indicatorXOffset
|
||||
|
||||
//动画过程中的left
|
||||
var animLeft = drawLeft
|
||||
//width
|
||||
var animWidth = drawWidth
|
||||
//动画执行过程中, 高度额外变大的值
|
||||
var animExHeight = 0
|
||||
|
||||
//end value
|
||||
val nextDrawTargetX = getChildTargetX(_targetIndex)
|
||||
val nextDrawWidth = getIndicatorDrawWidth(_targetIndex)
|
||||
val nextDrawLeft = nextDrawTargetX - nextDrawWidth / 2 + indicatorXOffset
|
||||
|
||||
var animEndWidth = nextDrawWidth
|
||||
var animEndLeft = nextDrawLeft
|
||||
|
||||
if (_targetIndex in 0 until childSize && _targetIndex != currentIndex) {
|
||||
|
||||
//动画过程参数计算变量
|
||||
val animStartLeft = drawLeft
|
||||
val animStartWidth = drawWidth
|
||||
|
||||
val animEndHeight = getIndicatorDrawHeight(_targetIndex)
|
||||
|
||||
if (indicatorEnableFlash) {
|
||||
//闪现效果
|
||||
animWidth = (animWidth * (1 - positionOffset)).toInt()
|
||||
animEndWidth = (animEndWidth * positionOffset).toInt()
|
||||
|
||||
animLeft = drawTargetX - animWidth / 2 + indicatorXOffset
|
||||
animEndLeft = nextDrawLeft
|
||||
} else if (indicatorEnableFlow && (_targetIndex - currentIndex).absoluteValue <= indicatorFlowStep) {
|
||||
//激活了流动效果
|
||||
|
||||
val flowEndWidth: Int
|
||||
if (_targetIndex > currentIndex) {
|
||||
flowEndWidth = animEndLeft - animStartLeft + animEndWidth
|
||||
|
||||
//目标在右边
|
||||
animLeft = if (positionOffset >= 0.5) {
|
||||
(animStartLeft + (animEndLeft - animStartLeft) * (positionOffset - 0.5) / 0.5f).toInt()
|
||||
} else {
|
||||
animStartLeft
|
||||
}
|
||||
} else {
|
||||
flowEndWidth = animStartLeft - animEndLeft + animStartWidth
|
||||
|
||||
//目标在左边
|
||||
animLeft = if (positionOffset >= 0.5) {
|
||||
animEndLeft
|
||||
} else {
|
||||
(animStartLeft - (animStartLeft - animEndLeft) * positionOffset / 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
animWidth = if (positionOffset >= 0.5) {
|
||||
(flowEndWidth - (flowEndWidth - animEndWidth) * (positionOffset - 0.5) / 0.5f).toInt()
|
||||
} else {
|
||||
(animStartWidth + (flowEndWidth - animStartWidth) * positionOffset / 0.5f).toInt()
|
||||
}
|
||||
} else {
|
||||
//默认平移效果
|
||||
if (_targetIndex > currentIndex) {
|
||||
//目标在右边
|
||||
animLeft =
|
||||
(animStartLeft + (animEndLeft - animStartLeft) * positionOffset).toInt()
|
||||
} else {
|
||||
//目标在左边
|
||||
animLeft =
|
||||
(animStartLeft - (animStartLeft - animEndLeft) * positionOffset).toInt()
|
||||
}
|
||||
|
||||
//动画过程中的宽度
|
||||
animWidth =
|
||||
(animStartWidth + (animEndWidth - animStartWidth) * positionOffset).toInt()
|
||||
}
|
||||
|
||||
animExHeight = ((animEndHeight - drawHeight) * positionOffset).toInt()
|
||||
}
|
||||
|
||||
//前景
|
||||
val drawTop = when (_indicatorDrawStyle) {
|
||||
//底部绘制
|
||||
INDICATOR_STYLE_BOTTOM -> viewHeight - drawHeight - indicatorYOffset
|
||||
//顶部绘制
|
||||
INDICATOR_STYLE_TOP -> 0 + indicatorYOffset
|
||||
//居中绘制
|
||||
else -> paddingTop + viewDrawHeight / 2 - drawHeight / 2 + indicatorYOffset -
|
||||
animExHeight +
|
||||
(tabLayout._maxConvexHeight - _childConvexHeight(currentIndex)) / 2
|
||||
}
|
||||
|
||||
indicatorDrawable?.apply {
|
||||
if (indicatorEnableFlash) {
|
||||
//flash
|
||||
if (indicatorEnableFlashClip) {
|
||||
drawIndicatorClipHorizontal(
|
||||
this,
|
||||
canvas,
|
||||
drawLeft,
|
||||
drawTop,
|
||||
drawLeft + drawWidth,
|
||||
drawTop + drawHeight + animExHeight,
|
||||
animWidth,
|
||||
1 - positionOffset
|
||||
)
|
||||
} else {
|
||||
drawIndicator(
|
||||
this, canvas, animLeft,
|
||||
drawTop,
|
||||
animLeft + animWidth,
|
||||
drawTop + drawHeight + animExHeight,
|
||||
1 - positionOffset
|
||||
)
|
||||
}
|
||||
|
||||
if (_targetIndex in 0 until childSize) {
|
||||
if (indicatorEnableFlashClip) {
|
||||
drawIndicatorClipHorizontal(
|
||||
this,
|
||||
canvas,
|
||||
nextDrawLeft,
|
||||
drawTop,
|
||||
nextDrawLeft + nextDrawWidth,
|
||||
drawTop + drawHeight + animExHeight,
|
||||
animEndWidth,
|
||||
positionOffset
|
||||
)
|
||||
} else {
|
||||
drawIndicator(
|
||||
this, canvas, animEndLeft,
|
||||
drawTop,
|
||||
animEndLeft + animEndWidth,
|
||||
drawTop + drawHeight + animExHeight,
|
||||
positionOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//normal
|
||||
drawIndicator(
|
||||
this, canvas, animLeft,
|
||||
drawTop,
|
||||
animLeft + animWidth,
|
||||
drawTop + drawHeight + animExHeight,
|
||||
1 - positionOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun drawIndicator(
|
||||
indicator: Drawable,
|
||||
canvas: Canvas,
|
||||
l: Int,
|
||||
t: Int,
|
||||
r: Int,
|
||||
b: Int,
|
||||
offset: Float
|
||||
) {
|
||||
indicator.apply {
|
||||
if (this is ITabIndicatorDraw) {
|
||||
setBounds(l, t, r, b)
|
||||
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
|
||||
} else {
|
||||
val width = r - l
|
||||
val height = b - t
|
||||
setBounds(0, 0, width, height)
|
||||
canvas.withSave {
|
||||
translate(l.toFloat(), t.toFloat())
|
||||
draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun drawIndicatorClipHorizontal(
|
||||
indicator: Drawable,
|
||||
canvas: Canvas,
|
||||
l: Int,
|
||||
t: Int,
|
||||
r: Int,
|
||||
b: Int,
|
||||
endWidth: Int,
|
||||
offset: Float
|
||||
) {
|
||||
indicator.apply {
|
||||
canvas.save()
|
||||
val dx = (r - l - endWidth) / 2
|
||||
canvas.clipRect(l + dx, t, r - dx, b)
|
||||
setBounds(l, t, r, b)
|
||||
if (this is ITabIndicatorDraw) {
|
||||
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
|
||||
} else {
|
||||
draw(canvas)
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
fun drawIndicatorClipVertical(
|
||||
indicator: Drawable,
|
||||
canvas: Canvas,
|
||||
l: Int,
|
||||
t: Int,
|
||||
r: Int,
|
||||
b: Int,
|
||||
endHeight: Int,
|
||||
offset: Float
|
||||
) {
|
||||
indicator.apply {
|
||||
canvas.save()
|
||||
val dy = (b - t - endHeight) / 2
|
||||
canvas.clipRect(l, t + dy, r, b - dy)
|
||||
setBounds(l, t, r, b)
|
||||
if (this is ITabIndicatorDraw) {
|
||||
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
|
||||
} else {
|
||||
draw(canvas)
|
||||
}
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
fun drawVertical(canvas: Canvas) {
|
||||
val childSize = tabLayout.dslSelector.visibleViewList.size
|
||||
|
||||
var currentIndex = currentIndex
|
||||
|
||||
if (_targetIndex in 0 until childSize) {
|
||||
currentIndex = max(0, currentIndex)
|
||||
}
|
||||
|
||||
if (currentIndex in 0 until childSize) {
|
||||
|
||||
} else {
|
||||
//无效的index
|
||||
return
|
||||
}
|
||||
|
||||
//"绘制$currentIndex:$currentSelectIndex $positionOffset".logi()
|
||||
|
||||
val drawTargetY = getChildTargetY(currentIndex)
|
||||
val drawWidth = getIndicatorDrawWidth(currentIndex)
|
||||
val drawHeight = getIndicatorDrawHeight(currentIndex)
|
||||
|
||||
val drawTop = drawTargetY - drawHeight / 2 + indicatorYOffset
|
||||
|
||||
//动画过程中的top
|
||||
var animTop = drawTop
|
||||
//height
|
||||
var animHeight = drawHeight
|
||||
//动画执行过程中, 宽度额外变大的值
|
||||
var animExWidth = 0
|
||||
|
||||
//end value
|
||||
val nextDrawTargetY = getChildTargetY(_targetIndex)
|
||||
val nextDrawHeight = getIndicatorDrawHeight(_targetIndex)
|
||||
val nextDrawTop = nextDrawTargetY - nextDrawHeight / 2 + indicatorYOffset
|
||||
|
||||
var animEndHeight = nextDrawHeight
|
||||
var animEndTop = nextDrawTop
|
||||
|
||||
if (_targetIndex in 0 until childSize && _targetIndex != currentIndex) {
|
||||
|
||||
//动画过程参数计算变量
|
||||
val animStartTop = drawTop
|
||||
val animStartHeight = drawHeight
|
||||
|
||||
val animEndWidth = getIndicatorDrawWidth(_targetIndex)
|
||||
|
||||
if (indicatorEnableFlash) {
|
||||
//闪现效果
|
||||
animHeight = (animHeight * (1 - positionOffset)).toInt()
|
||||
animEndHeight = (animEndHeight * positionOffset).toInt()
|
||||
|
||||
animTop = drawTargetY - animHeight / 2 + indicatorXOffset
|
||||
animEndTop = nextDrawTargetY - animEndHeight / 2 + indicatorXOffset
|
||||
} else if (indicatorEnableFlow && (_targetIndex - currentIndex).absoluteValue <= indicatorFlowStep) {
|
||||
//激活了流动效果
|
||||
|
||||
val flowEndHeight: Int
|
||||
if (_targetIndex > currentIndex) {
|
||||
flowEndHeight = animEndTop - animStartTop + animEndHeight
|
||||
|
||||
//目标在下边
|
||||
animTop = if (positionOffset >= 0.5) {
|
||||
(animStartTop + (animEndTop - animStartTop) * (positionOffset - 0.5) / 0.5f).toInt()
|
||||
} else {
|
||||
animStartTop
|
||||
}
|
||||
} else {
|
||||
flowEndHeight = animStartTop - animEndTop + animStartHeight
|
||||
|
||||
//目标在上边
|
||||
animTop = if (positionOffset >= 0.5) {
|
||||
animEndTop
|
||||
} else {
|
||||
(animStartTop - (animStartTop - animEndTop) * positionOffset / 0.5f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
animHeight = if (positionOffset >= 0.5) {
|
||||
(flowEndHeight - (flowEndHeight - animEndHeight) * (positionOffset - 0.5) / 0.5f).toInt()
|
||||
} else {
|
||||
(animStartHeight + (flowEndHeight - animStartHeight) * positionOffset / 0.5f).toInt()
|
||||
}
|
||||
} else {
|
||||
if (_targetIndex > currentIndex) {
|
||||
//目标在下边
|
||||
animTop = (animStartTop + (animEndTop - animStartTop) * positionOffset).toInt()
|
||||
} else {
|
||||
//目标在上边
|
||||
animTop = (animStartTop - (animStartTop - animEndTop) * positionOffset).toInt()
|
||||
}
|
||||
|
||||
//动画过程中的宽度
|
||||
animHeight =
|
||||
(animStartHeight + (animEndHeight - animStartHeight) * positionOffset).toInt()
|
||||
}
|
||||
|
||||
animExWidth = ((animEndWidth - drawWidth) * positionOffset).toInt()
|
||||
}
|
||||
|
||||
val drawLeft = when (_indicatorDrawStyle) {
|
||||
INDICATOR_STYLE_BOTTOM -> {
|
||||
//右边/底部绘制
|
||||
viewWidth - drawWidth - indicatorXOffset
|
||||
}
|
||||
INDICATOR_STYLE_TOP -> {
|
||||
//左边/顶部绘制
|
||||
0 + indicatorXOffset
|
||||
}
|
||||
else -> {
|
||||
//居中绘制
|
||||
paddingLeft + indicatorXOffset + (viewDrawWidth / 2 - drawWidth / 2) -
|
||||
(tabLayout._maxConvexHeight - _childConvexHeight(currentIndex)) / 2
|
||||
}
|
||||
}
|
||||
|
||||
indicatorDrawable?.apply {
|
||||
//flash
|
||||
if (indicatorEnableFlash) {
|
||||
if (indicatorEnableFlashClip) {
|
||||
drawIndicatorClipVertical(
|
||||
this, canvas, drawLeft,
|
||||
drawTop,
|
||||
drawLeft + drawWidth + animExWidth,
|
||||
drawTop + drawHeight,
|
||||
animHeight,
|
||||
1 - positionOffset
|
||||
)
|
||||
} else {
|
||||
drawIndicator(
|
||||
this, canvas, drawLeft,
|
||||
animTop,
|
||||
drawLeft + drawWidth + animExWidth,
|
||||
animTop + animHeight,
|
||||
1 - positionOffset
|
||||
)
|
||||
}
|
||||
|
||||
if (_targetIndex in 0 until childSize) {
|
||||
if (indicatorEnableFlashClip) {
|
||||
drawIndicatorClipVertical(
|
||||
this, canvas, drawLeft,
|
||||
nextDrawTop,
|
||||
drawLeft + drawWidth + animExWidth,
|
||||
nextDrawTop + nextDrawHeight,
|
||||
animEndHeight,
|
||||
positionOffset
|
||||
)
|
||||
} else {
|
||||
drawIndicator(
|
||||
this, canvas, drawLeft,
|
||||
animEndTop,
|
||||
drawLeft + drawWidth + animExWidth,
|
||||
animEndTop + animEndHeight,
|
||||
positionOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
drawIndicator(
|
||||
this, canvas, drawLeft,
|
||||
animTop,
|
||||
drawLeft + drawWidth + animExWidth,
|
||||
animTop + animHeight,
|
||||
1 - positionOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun _childConvexHeight(index: Int): Int {
|
||||
if (attachView is ViewGroup) {
|
||||
((attachView as ViewGroup).getChildAt(index).layoutParams as? DslTabLayout.LayoutParams)?.apply {
|
||||
return layoutConvexHeight
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 距离[_targetIndex]的偏移比例.[0->1]的过程
|
||||
* */
|
||||
var positionOffset: Float = 0f
|
||||
set(value) {
|
||||
field = value
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
/**当前绘制的index*/
|
||||
var currentIndex: Int = -1
|
||||
|
||||
/**滚动目标的index*/
|
||||
var _targetIndex = -1
|
||||
}
|
2043
TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayout.kt
Normal file
2043
TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayout.kt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,526 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.IdRes
|
||||
import com.angcyo.tablayout.DslTabIndicator.Companion.NO_COLOR
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/11/26
|
||||
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||
*/
|
||||
open class DslTabLayoutConfig(val tabLayout: DslTabLayout) : DslSelectorConfig() {
|
||||
|
||||
/**是否开启文本颜色*/
|
||||
var tabEnableTextColor = true
|
||||
set(value) {
|
||||
field = value
|
||||
if (field) {
|
||||
tabEnableIcoColor = true
|
||||
}
|
||||
}
|
||||
|
||||
/**是否开启颜色渐变效果*/
|
||||
var tabEnableGradientColor = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (field) {
|
||||
tabEnableIcoGradientColor = true
|
||||
}
|
||||
}
|
||||
|
||||
/**是否激活指示器的颜色渐变效果*/
|
||||
var tabEnableIndicatorGradientColor = false
|
||||
|
||||
/**选中的文本颜色*/
|
||||
var tabSelectColor: Int = Color.WHITE //Color.parseColor("#333333")
|
||||
|
||||
/**未选中的文本颜色*/
|
||||
var tabDeselectColor: Int = Color.parseColor("#999999")
|
||||
|
||||
/**是否开启Bold, 文本加粗*/
|
||||
var tabEnableTextBold = false
|
||||
|
||||
/**是否使用粗体字体的方式设置粗体, 否则使用[Paint.FAKE_BOLD_TEXT_FLAG]
|
||||
* 需要先激活[tabEnableTextBold]*/
|
||||
var tabUseTypefaceBold = false
|
||||
|
||||
/**是否开启图标颜色*/
|
||||
var tabEnableIcoColor = true
|
||||
|
||||
/**是否开启图标颜色渐变效果*/
|
||||
var tabEnableIcoGradientColor = false
|
||||
|
||||
/**选中的图标颜色*/
|
||||
var tabIcoSelectColor: Int = NO_COLOR
|
||||
get() {
|
||||
return if (field == NO_COLOR) tabSelectColor else field
|
||||
}
|
||||
|
||||
/**未选中的图标颜色*/
|
||||
var tabIcoDeselectColor: Int = NO_COLOR
|
||||
get() {
|
||||
return if (field == NO_COLOR) tabDeselectColor else field
|
||||
}
|
||||
|
||||
/**是否开启scale渐变效果*/
|
||||
var tabEnableGradientScale = false
|
||||
|
||||
/**最小缩放的比例*/
|
||||
var tabMinScale = 0.8f
|
||||
|
||||
/**最大缩放的比例*/
|
||||
var tabMaxScale = 1.2f
|
||||
|
||||
/**是否开启字体大小渐变效果*/
|
||||
var tabEnableGradientTextSize = true
|
||||
|
||||
/**tab中文本字体未选中时的字体大小, >0时激活*/
|
||||
var tabTextMinSize = -1f
|
||||
|
||||
/**tab中文本字体选中时的字体大小, >0时激活*/
|
||||
var tabTextMaxSize = -1f
|
||||
|
||||
/**渐变效果实现的回调*/
|
||||
var tabGradientCallback = TabGradientCallback()
|
||||
|
||||
/**指定文本控件的id, 所有文本属性改变, 将会发生在这个控件上.
|
||||
* 如果指定的控件不存在, 控件会降权至[ItemView]*/
|
||||
@IdRes
|
||||
var tabTextViewId: Int = View.NO_ID
|
||||
|
||||
/**指定图标控件的id*/
|
||||
@IdRes
|
||||
var tabIconViewId: Int = View.NO_ID
|
||||
|
||||
/**返回用于配置文本样式的控件*/
|
||||
var onGetTextStyleView: (itemView: View, index: Int) -> TextView? = { itemView, _ ->
|
||||
if (tabTextViewId == View.NO_ID) {
|
||||
var tv: TextView? = if (itemView is TextView) itemView else null
|
||||
|
||||
if (tabLayout.tabIndicator.indicatorContentIndex != -1) {
|
||||
itemView.getChildOrNull(tabLayout.tabIndicator.indicatorContentIndex)?.let {
|
||||
if (it is TextView) {
|
||||
tv = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tabLayout.tabIndicator.indicatorContentId != View.NO_ID) {
|
||||
itemView.findViewById<View>(tabLayout.tabIndicator.indicatorContentId)?.let {
|
||||
if (it is TextView) {
|
||||
tv = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val lp = itemView.layoutParams
|
||||
if (lp is DslTabLayout.LayoutParams) {
|
||||
if (lp.indicatorContentIndex != -1 && itemView is ViewGroup) {
|
||||
itemView.getChildOrNull(lp.indicatorContentIndex)?.let {
|
||||
if (it is TextView) {
|
||||
tv = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lp.indicatorContentId != View.NO_ID) {
|
||||
itemView.findViewById<View>(lp.indicatorContentId)?.let {
|
||||
if (it is TextView) {
|
||||
tv = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lp.contentTextViewIndex != -1 && itemView is ViewGroup) {
|
||||
itemView.getChildOrNull(lp.contentTextViewIndex)?.let {
|
||||
if (it is TextView) {
|
||||
tv = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lp.contentTextViewId != View.NO_ID) {
|
||||
itemView.findViewById<View>(lp.contentTextViewId)?.let {
|
||||
if (it is TextView) {
|
||||
tv = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tv
|
||||
} else {
|
||||
itemView.findViewById(tabTextViewId)
|
||||
}
|
||||
}
|
||||
|
||||
/**返回用于配置ico样式的控件*/
|
||||
var onGetIcoStyleView: (itemView: View, index: Int) -> View? = { itemView, _ ->
|
||||
if (tabIconViewId == View.NO_ID) {
|
||||
var iv: View? = itemView
|
||||
|
||||
if (tabLayout.tabIndicator.indicatorContentIndex != -1) {
|
||||
itemView.getChildOrNull(tabLayout.tabIndicator.indicatorContentIndex)?.let {
|
||||
iv = it
|
||||
}
|
||||
}
|
||||
|
||||
if (tabLayout.tabIndicator.indicatorContentId != View.NO_ID) {
|
||||
itemView.findViewById<View>(tabLayout.tabIndicator.indicatorContentId)?.let {
|
||||
iv = it
|
||||
}
|
||||
}
|
||||
|
||||
val lp = itemView.layoutParams
|
||||
if (lp is DslTabLayout.LayoutParams) {
|
||||
if (lp.indicatorContentIndex != -1 && itemView is ViewGroup) {
|
||||
iv = itemView.getChildOrNull(lp.indicatorContentIndex)
|
||||
}
|
||||
|
||||
if (lp.indicatorContentId != View.NO_ID) {
|
||||
itemView.findViewById<View>(lp.indicatorContentId)?.let {
|
||||
iv = it
|
||||
}
|
||||
}
|
||||
|
||||
if (lp.contentIconViewIndex != -1 && itemView is ViewGroup) {
|
||||
iv = itemView.getChildOrNull(lp.contentIconViewIndex)
|
||||
}
|
||||
|
||||
if (lp.contentIconViewId != View.NO_ID) {
|
||||
itemView.findViewById<View>(lp.contentIconViewId)?.let {
|
||||
iv = it
|
||||
}
|
||||
}
|
||||
}
|
||||
iv
|
||||
} else {
|
||||
itemView.findViewById(tabIconViewId)
|
||||
}
|
||||
}
|
||||
|
||||
/**获取渐变结束时,指示器的颜色.*/
|
||||
var onGetGradientIndicatorColor: (fromIndex: Int, toIndex: Int, positionOffset: Float) -> Int =
|
||||
{ fromIndex, toIndex, positionOffset ->
|
||||
tabLayout.tabIndicator.indicatorColor
|
||||
}
|
||||
|
||||
init {
|
||||
onStyleItemView = { itemView, index, select ->
|
||||
onUpdateItemStyle(itemView, index, select)
|
||||
}
|
||||
onSelectIndexChange = { fromIndex, selectIndexList, reselect, fromUser ->
|
||||
val toIndex = selectIndexList.last()
|
||||
tabLayout._viewPagerDelegate?.onSetCurrentItem(fromIndex, toIndex, reselect, fromUser)
|
||||
}
|
||||
}
|
||||
|
||||
/**xml属性读取*/
|
||||
open fun initAttribute(context: Context, attributeSet: AttributeSet? = null) {
|
||||
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||
|
||||
tabSelectColor =
|
||||
typedArray.getColor(R.styleable.DslTabLayout_tab_select_color, tabSelectColor)
|
||||
tabDeselectColor =
|
||||
typedArray.getColor(
|
||||
R.styleable.DslTabLayout_tab_deselect_color,
|
||||
tabDeselectColor
|
||||
)
|
||||
tabIcoSelectColor =
|
||||
typedArray.getColor(R.styleable.DslTabLayout_tab_ico_select_color, NO_COLOR)
|
||||
tabIcoDeselectColor =
|
||||
typedArray.getColor(R.styleable.DslTabLayout_tab_ico_deselect_color, NO_COLOR)
|
||||
|
||||
tabEnableTextColor = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_enable_text_color,
|
||||
tabEnableTextColor
|
||||
)
|
||||
tabEnableIndicatorGradientColor = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_enable_indicator_gradient_color,
|
||||
tabEnableIndicatorGradientColor
|
||||
)
|
||||
tabEnableGradientColor = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_enable_gradient_color,
|
||||
tabEnableGradientColor
|
||||
)
|
||||
tabEnableIcoColor = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_enable_ico_color,
|
||||
tabEnableIcoColor
|
||||
)
|
||||
tabEnableIcoGradientColor = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_enable_ico_gradient_color,
|
||||
tabEnableIcoGradientColor
|
||||
)
|
||||
|
||||
tabEnableTextBold = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_enable_text_bold,
|
||||
tabEnableTextBold
|
||||
)
|
||||
|
||||
tabUseTypefaceBold = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_use_typeface_bold,
|
||||
tabUseTypefaceBold
|
||||
)
|
||||
|
||||
tabEnableGradientScale = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_enable_gradient_scale,
|
||||
tabEnableGradientScale
|
||||
)
|
||||
tabMinScale = typedArray.getFloat(R.styleable.DslTabLayout_tab_min_scale, tabMinScale)
|
||||
tabMaxScale = typedArray.getFloat(R.styleable.DslTabLayout_tab_max_scale, tabMaxScale)
|
||||
|
||||
tabEnableGradientTextSize = typedArray.getBoolean(
|
||||
R.styleable.DslTabLayout_tab_enable_gradient_text_size,
|
||||
tabEnableGradientTextSize
|
||||
)
|
||||
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_text_min_size)) {
|
||||
tabTextMinSize = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_text_min_size,
|
||||
tabTextMinSize.toInt()
|
||||
).toFloat()
|
||||
}
|
||||
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_text_max_size)) {
|
||||
tabTextMaxSize = typedArray.getDimensionPixelOffset(
|
||||
R.styleable.DslTabLayout_tab_text_max_size,
|
||||
tabTextMaxSize.toInt()
|
||||
).toFloat()
|
||||
}
|
||||
|
||||
tabTextViewId =
|
||||
typedArray.getResourceId(R.styleable.DslTabLayout_tab_text_view_id, tabTextViewId)
|
||||
tabIconViewId =
|
||||
typedArray.getResourceId(R.styleable.DslTabLayout_tab_icon_view_id, tabIconViewId)
|
||||
|
||||
typedArray.recycle()
|
||||
}
|
||||
|
||||
/**更新item的样式*/
|
||||
open fun onUpdateItemStyle(itemView: View, index: Int, select: Boolean) {
|
||||
//"$itemView\n$index\n$select".logw()
|
||||
|
||||
(onGetTextStyleView(itemView, index))?.apply {
|
||||
//文本加粗
|
||||
paint?.apply {
|
||||
if (tabEnableTextBold && select) {
|
||||
//设置粗体
|
||||
if (tabUseTypefaceBold) {
|
||||
typeface = Typeface.defaultFromStyle(Typeface.BOLD)
|
||||
} else {
|
||||
flags = flags or Paint.FAKE_BOLD_TEXT_FLAG
|
||||
isFakeBoldText = true
|
||||
}
|
||||
} else {
|
||||
//取消粗体
|
||||
if (tabUseTypefaceBold) {
|
||||
typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
|
||||
} else {
|
||||
flags = flags and Paint.FAKE_BOLD_TEXT_FLAG.inv()
|
||||
isFakeBoldText = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tabEnableTextColor) {
|
||||
//文本颜色
|
||||
setTextColor(if (select) tabSelectColor else tabDeselectColor)
|
||||
}
|
||||
|
||||
if (tabTextMaxSize > 0 || tabTextMinSize > 0) {
|
||||
//文本字体大小
|
||||
val minTextSize = min(tabTextMinSize, tabTextMaxSize)
|
||||
val maxTextSize = max(tabTextMinSize, tabTextMaxSize)
|
||||
setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
if (select) maxTextSize else minTextSize
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (tabEnableIcoColor) {
|
||||
onGetIcoStyleView(itemView, index)?.apply {
|
||||
_updateIcoColor(this, if (select) tabIcoSelectColor else tabIcoDeselectColor)
|
||||
}
|
||||
}
|
||||
|
||||
if (tabEnableGradientScale) {
|
||||
itemView.scaleX = if (select) tabMaxScale else tabMinScale
|
||||
itemView.scaleY = if (select) tabMaxScale else tabMinScale
|
||||
}
|
||||
|
||||
if (tabLayout.drawBorder) {
|
||||
tabLayout.tabBorder?.updateItemBackground(tabLayout, itemView, index, select)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [DslTabLayout]滚动时回调.
|
||||
* */
|
||||
open fun onPageIndexScrolled(fromIndex: Int, toIndex: Int, positionOffset: Float) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* [onPageIndexScrolled]
|
||||
* */
|
||||
open fun onPageViewScrolled(fromView: View?, toView: View, positionOffset: Float) {
|
||||
//"$fromView\n$toView\n$positionOffset".logi()
|
||||
|
||||
if (fromView != toView) {
|
||||
|
||||
val fromIndex = tabLayout.tabIndicator.currentIndex
|
||||
val toIndex = tabLayout.tabIndicator._targetIndex
|
||||
|
||||
if (tabEnableIndicatorGradientColor) {
|
||||
val startColor = onGetGradientIndicatorColor(fromIndex, fromIndex, 0f)
|
||||
val endColor = onGetGradientIndicatorColor(fromIndex, toIndex, positionOffset)
|
||||
|
||||
tabLayout.tabIndicator.indicatorColor =
|
||||
evaluateColor(positionOffset, startColor, endColor)
|
||||
}
|
||||
|
||||
if (tabEnableGradientColor) {
|
||||
//文本渐变
|
||||
fromView?.apply {
|
||||
_gradientColor(
|
||||
onGetTextStyleView(this, fromIndex),
|
||||
tabSelectColor,
|
||||
tabDeselectColor,
|
||||
positionOffset
|
||||
)
|
||||
}
|
||||
_gradientColor(
|
||||
onGetTextStyleView(toView, toIndex),
|
||||
tabDeselectColor,
|
||||
tabSelectColor,
|
||||
positionOffset
|
||||
)
|
||||
}
|
||||
|
||||
if (tabEnableIcoGradientColor) {
|
||||
//图标渐变
|
||||
fromView?.apply {
|
||||
_gradientIcoColor(
|
||||
onGetIcoStyleView(this, fromIndex),
|
||||
tabIcoSelectColor,
|
||||
tabIcoDeselectColor,
|
||||
positionOffset
|
||||
)
|
||||
}
|
||||
|
||||
_gradientIcoColor(
|
||||
onGetIcoStyleView(toView, toIndex),
|
||||
tabIcoDeselectColor,
|
||||
tabIcoSelectColor,
|
||||
positionOffset
|
||||
)
|
||||
}
|
||||
|
||||
if (tabEnableGradientScale) {
|
||||
//scale渐变
|
||||
_gradientScale(fromView, tabMaxScale, tabMinScale, positionOffset)
|
||||
_gradientScale(toView, tabMinScale, tabMaxScale, positionOffset)
|
||||
}
|
||||
|
||||
if (tabEnableGradientTextSize &&
|
||||
tabTextMaxSize > 0 &&
|
||||
tabTextMinSize > 0 &&
|
||||
tabTextMinSize != tabTextMaxSize
|
||||
) {
|
||||
|
||||
//文本字体大小渐变
|
||||
_gradientTextSize(
|
||||
fromView?.run { onGetTextStyleView(this, fromIndex) },
|
||||
tabTextMaxSize,
|
||||
tabTextMinSize,
|
||||
positionOffset
|
||||
)
|
||||
_gradientTextSize(
|
||||
onGetTextStyleView(toView, toIndex),
|
||||
tabTextMinSize,
|
||||
tabTextMaxSize,
|
||||
positionOffset
|
||||
)
|
||||
|
||||
if (toIndex == tabLayout.dslSelector.visibleViewList.lastIndex || toIndex == 0) {
|
||||
tabLayout._scrollToTarget(toIndex, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun _gradientColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
|
||||
tabGradientCallback.onGradientColor(view, startColor, endColor, percent)
|
||||
}
|
||||
|
||||
open fun _gradientIcoColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
|
||||
tabGradientCallback.onGradientIcoColor(view, startColor, endColor, percent)
|
||||
}
|
||||
|
||||
open fun _gradientScale(view: View?, startScale: Float, endScale: Float, percent: Float) {
|
||||
tabGradientCallback.onGradientScale(view, startScale, endScale, percent)
|
||||
}
|
||||
|
||||
open fun _gradientTextSize(
|
||||
view: TextView?,
|
||||
startTextSize: Float,
|
||||
endTextSize: Float,
|
||||
percent: Float
|
||||
) {
|
||||
tabGradientCallback.onGradientTextSize(view, startTextSize, endTextSize, percent)
|
||||
}
|
||||
|
||||
open fun _updateIcoColor(view: View?, color: Int) {
|
||||
tabGradientCallback.onUpdateIcoColor(view, color)
|
||||
}
|
||||
}
|
||||
|
||||
open class TabGradientCallback {
|
||||
|
||||
open fun onGradientColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
|
||||
(view as? TextView)?.apply {
|
||||
setTextColor(evaluateColor(percent, startColor, endColor))
|
||||
}
|
||||
}
|
||||
|
||||
open fun onGradientIcoColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
|
||||
onUpdateIcoColor(view, evaluateColor(percent, startColor, endColor))
|
||||
}
|
||||
|
||||
open fun onUpdateIcoColor(view: View?, color: Int) {
|
||||
view?.tintDrawableColor(color)
|
||||
}
|
||||
|
||||
open fun onGradientScale(view: View?, startScale: Float, endScale: Float, percent: Float) {
|
||||
view?.apply {
|
||||
(startScale + (endScale - startScale) * percent).let {
|
||||
scaleX = it
|
||||
scaleY = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun onGradientTextSize(
|
||||
view: TextView?,
|
||||
startTextSize: Float,
|
||||
endTextSize: Float,
|
||||
percent: Float
|
||||
) {
|
||||
view?.apply {
|
||||
setTextSize(
|
||||
TypedValue.COMPLEX_UNIT_PX,
|
||||
(startTextSize + (endTextSize - startTextSize) * percent)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.graphics.Canvas
|
||||
|
||||
/**
|
||||
* 用来实现[DslTabIndicator]的自绘
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2022/02/21
|
||||
* Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved.
|
||||
*/
|
||||
interface ITabIndicatorDraw {
|
||||
|
||||
/**绘制指示器
|
||||
* [positionOffset] 页面偏移量*/
|
||||
fun onDrawTabIndicator(
|
||||
tabIndicator: DslTabIndicator,
|
||||
canvas: Canvas,
|
||||
positionOffset: Float
|
||||
)
|
||||
|
||||
}
|
334
TabLayout/src/main/java/com/angcyo/tablayout/LibEx.kt
Normal file
334
TabLayout/src/main/java/com/angcyo/tablayout/LibEx.kt
Normal file
@ -0,0 +1,334 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.math.MathUtils
|
||||
|
||||
/**
|
||||
*
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/11/23
|
||||
*/
|
||||
internal val dpi: Int
|
||||
get() = dp.toInt()
|
||||
|
||||
internal val dp: Float
|
||||
get() = Resources.getSystem().displayMetrics.density
|
||||
|
||||
internal val View.dpi: Int
|
||||
get() = context.resources.displayMetrics.density.toInt()
|
||||
|
||||
internal val View.screenWidth: Int
|
||||
get() = context.resources.displayMetrics.widthPixels
|
||||
|
||||
internal val View.screenHeight: Int
|
||||
get() = context.resources.displayMetrics.heightPixels
|
||||
|
||||
internal val View.viewDrawWidth: Int
|
||||
get() = measuredWidth - paddingLeft - paddingRight
|
||||
|
||||
internal val View.viewDrawHeight: Int
|
||||
get() = measuredHeight - paddingTop - paddingBottom
|
||||
|
||||
/**Match_Parent*/
|
||||
internal fun exactlyMeasure(size: Int): Int =
|
||||
View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY)
|
||||
|
||||
internal fun exactlyMeasure(size: Float): Int = exactlyMeasure(size.toInt())
|
||||
|
||||
/**Wrap_Content*/
|
||||
internal fun atmostMeasure(size: Int): Int =
|
||||
View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.AT_MOST)
|
||||
|
||||
internal fun Int.have(value: Int): Boolean = if (this == 0 || value == 0) {
|
||||
false
|
||||
} else if (this == 0 && value == 0) {
|
||||
true
|
||||
} else {
|
||||
((this > 0 && value > 0) || (this < 0 && value < 0)) && this and value == value
|
||||
}
|
||||
|
||||
internal fun Int.remove(value: Int): Int = this and value.inv()
|
||||
|
||||
internal fun clamp(value: Float, min: Float, max: Float): Float {
|
||||
if (value < min) {
|
||||
return min
|
||||
} else if (value > max) {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
internal fun clamp(value: Int, min: Int, max: Int): Int {
|
||||
if (value < min) {
|
||||
return min
|
||||
} else if (value > max) {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
internal fun Any.logi() {
|
||||
Log.i("DslTabLayout", "$this")
|
||||
}
|
||||
|
||||
internal fun Any.logw() {
|
||||
Log.w("DslTabLayout", "$this")
|
||||
}
|
||||
|
||||
internal fun Any.loge() {
|
||||
Log.e("DslTabLayout", "$this")
|
||||
}
|
||||
|
||||
internal fun View.calcLayoutWidthHeight(
|
||||
rLayoutWidth: String?, rLayoutHeight: String?,
|
||||
parentWidth: Int, parentHeight: Int,
|
||||
rLayoutWidthExclude: Int = 0, rLayoutHeightExclude: Int = 0
|
||||
): IntArray {
|
||||
val size = intArrayOf(-1, -1)
|
||||
if (TextUtils.isEmpty(rLayoutWidth) && TextUtils.isEmpty(rLayoutHeight)) {
|
||||
return size
|
||||
}
|
||||
if (!TextUtils.isEmpty(rLayoutWidth)) {
|
||||
if (rLayoutWidth!!.contains("sw", true)) {
|
||||
val ratio = rLayoutWidth.replace("sw", "", true).toFloatOrNull()
|
||||
ratio?.let {
|
||||
size[0] = (ratio * (screenWidth - rLayoutWidthExclude)).toInt()
|
||||
}
|
||||
} else if (rLayoutWidth!!.contains("pw", true)) {
|
||||
val ratio = rLayoutWidth.replace("pw", "", true).toFloatOrNull()
|
||||
ratio?.let {
|
||||
size[0] = (ratio * (parentWidth - rLayoutWidthExclude)).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!TextUtils.isEmpty(rLayoutHeight)) {
|
||||
if (rLayoutHeight!!.contains("sh", true)) {
|
||||
val ratio = rLayoutHeight.replace("sh", "", true).toFloatOrNull()
|
||||
ratio?.let {
|
||||
size[1] = (ratio * (screenHeight - rLayoutHeightExclude)).toInt()
|
||||
}
|
||||
} else if (rLayoutHeight!!.contains("ph", true)) {
|
||||
val ratio = rLayoutHeight.replace("ph", "", true).toFloatOrNull()
|
||||
ratio?.let {
|
||||
size[1] = (ratio * (parentHeight - rLayoutHeightExclude)).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
internal fun evaluateColor(fraction: Float /*0-1*/, startColor: Int, endColor: Int): Int {
|
||||
val fr = MathUtils.clamp(fraction, 0f, 1f)
|
||||
val startA = startColor shr 24 and 0xff
|
||||
val startR = startColor shr 16 and 0xff
|
||||
val startG = startColor shr 8 and 0xff
|
||||
val startB = startColor and 0xff
|
||||
val endA = endColor shr 24 and 0xff
|
||||
val endR = endColor shr 16 and 0xff
|
||||
val endG = endColor shr 8 and 0xff
|
||||
val endB = endColor and 0xff
|
||||
return startA + (fr * (endA - startA)).toInt() shl 24 or
|
||||
(startR + (fr * (endR - startR)).toInt() shl 16) or
|
||||
(startG + (fr * (endG - startG)).toInt() shl 8) or
|
||||
startB + (fr * (endB - startB)).toInt()
|
||||
}
|
||||
|
||||
internal fun Drawable?.tintDrawableColor(color: Int): Drawable? {
|
||||
|
||||
if (this == null) {
|
||||
return this
|
||||
}
|
||||
|
||||
val wrappedDrawable =
|
||||
DrawableCompat.wrap(this).mutate()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
DrawableCompat.setTint(wrappedDrawable, color)
|
||||
} else {
|
||||
wrappedDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||
}
|
||||
|
||||
return wrappedDrawable
|
||||
}
|
||||
|
||||
internal fun View?.tintDrawableColor(color: Int) {
|
||||
when (this) {
|
||||
is TextView -> {
|
||||
val drawables = arrayOfNulls<Drawable?>(4)
|
||||
compoundDrawables.forEachIndexed { index, drawable ->
|
||||
drawables[index] = drawable?.tintDrawableColor(color)
|
||||
}
|
||||
setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3])
|
||||
}
|
||||
is ImageView -> {
|
||||
setImageDrawable(drawable?.tintDrawableColor(color))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Paint?.textWidth(text: String?): Float {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return 0f
|
||||
}
|
||||
return this?.run {
|
||||
measureText(text)
|
||||
} ?: 0f
|
||||
}
|
||||
|
||||
internal fun Paint?.textHeight(): Float = this?.run { descent() - ascent() } ?: 0f
|
||||
|
||||
internal fun View.getChildOrNull(index: Int): View? {
|
||||
return if (this is ViewGroup) {
|
||||
return if (index in 0 until childCount) {
|
||||
getChildAt(index)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
/**获取[View]在指定[parent]中的矩形坐标*/
|
||||
internal fun View.getLocationInParent(parentView: View? = null, result: Rect = Rect()): Rect {
|
||||
val parent: View? = parentView ?: (parent as? View)
|
||||
|
||||
if (parent == null) {
|
||||
getViewRect(result)
|
||||
} else {
|
||||
result.set(0, 0, 0, 0)
|
||||
if (this != parent) {
|
||||
fun doIt(view: View, parent: View, rect: Rect) {
|
||||
val viewParent = view.parent
|
||||
if (viewParent is View) {
|
||||
rect.left += view.left
|
||||
rect.top += view.top
|
||||
if (viewParent != parent) {
|
||||
doIt(viewParent, parent, rect)
|
||||
}
|
||||
}
|
||||
}
|
||||
doIt(this, parent, result)
|
||||
}
|
||||
result.right = result.left + this.measuredWidth
|
||||
result.bottom = result.top + this.measuredHeight
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取View, 相对于手机屏幕的矩形
|
||||
* */
|
||||
internal fun View.getViewRect(result: Rect = Rect()): Rect {
|
||||
var offsetX = 0
|
||||
var offsetY = 0
|
||||
|
||||
//横屏, 并且显示了虚拟导航栏的时候. 需要左边偏移
|
||||
//只计算一次
|
||||
(context as? Activity)?.let {
|
||||
it.window.decorView.getGlobalVisibleRect(result)
|
||||
if (result.width() > result.height()) {
|
||||
//横屏了
|
||||
offsetX = navBarHeight(it)
|
||||
}
|
||||
}
|
||||
|
||||
return getViewRect(offsetX, offsetY, result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取View, 相对于手机屏幕的矩形, 带皮阿尼一
|
||||
* */
|
||||
internal fun View.getViewRect(offsetX: Int, offsetY: Int, result: Rect = Rect()): Rect {
|
||||
//可见位置的坐标, 超出屏幕的距离会被剃掉
|
||||
//getGlobalVisibleRect(r)
|
||||
val r2 = IntArray(2)
|
||||
//val r3 = IntArray(2)
|
||||
//相对于屏幕的坐标
|
||||
getLocationOnScreen(r2)
|
||||
//相对于窗口的坐标
|
||||
//getLocationInWindow(r3)
|
||||
|
||||
val left = r2[0] + offsetX
|
||||
val top = r2[1] + offsetY
|
||||
|
||||
result.set(left, top, left + measuredWidth, top + measuredHeight)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 导航栏的高度(如果显示了)
|
||||
*/
|
||||
internal fun navBarHeight(context: Context): Int {
|
||||
var result = 0
|
||||
|
||||
if (context is Activity) {
|
||||
val decorRect = Rect()
|
||||
val windowRect = Rect()
|
||||
|
||||
context.window.decorView.getGlobalVisibleRect(decorRect)
|
||||
context.window.findViewById<View>(Window.ID_ANDROID_CONTENT)
|
||||
.getGlobalVisibleRect(windowRect)
|
||||
|
||||
if (decorRect.width() > decorRect.height()) {
|
||||
//横屏
|
||||
result = decorRect.width() - windowRect.width()
|
||||
} else {
|
||||
//竖屏
|
||||
result = decorRect.bottom - windowRect.bottom
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun Collection<*>?.size() = this?.size ?: 0
|
||||
|
||||
/**判断2个列表中的数据是否改变过*/
|
||||
internal fun <T> List<T>?.isChange(other: List<T>?): Boolean {
|
||||
if (this.size() != other.size()) {
|
||||
return true
|
||||
}
|
||||
this?.forEachIndexed { index, t ->
|
||||
if (t != other?.getOrNull(index)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun Int.isHorizontal() = this == LinearLayout.HORIZONTAL
|
||||
|
||||
fun Int.isVertical() = this == LinearLayout.VERTICAL
|
||||
|
||||
internal fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = true): View {
|
||||
if (layoutId == -1) {
|
||||
return this
|
||||
}
|
||||
val rootView = LayoutInflater.from(context).inflate(layoutId, this, false)
|
||||
if (attachToRoot) {
|
||||
addView(rootView)
|
||||
}
|
||||
return rootView
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.angcyo.tablayout
|
||||
|
||||
/**
|
||||
* 不依赖ViewPager和ViewPager2
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/12/14
|
||||
*/
|
||||
interface ViewPagerDelegate {
|
||||
companion object {
|
||||
const val SCROLL_STATE_IDLE = 0
|
||||
const val SCROLL_STATE_DRAGGING = 1
|
||||
const val SCROLL_STATE_SETTLING = 2
|
||||
}
|
||||
|
||||
/**获取当前页面索引*/
|
||||
fun onGetCurrentItem(): Int
|
||||
|
||||
/**设置当前的页面*/
|
||||
fun onSetCurrentItem(fromIndex: Int, toIndex: Int, reselect: Boolean, fromUser: Boolean)
|
||||
}
|
299
TabLayout/src/main/res/values/attr_dsl_tab_layout.xml
Normal file
299
TabLayout/src/main/res/values/attr_dsl_tab_layout.xml
Normal file
@ -0,0 +1,299 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="DslTabLayout">
|
||||
<!--Item是否等宽-->
|
||||
<attr name="tab_item_is_equ_width" format="boolean" />
|
||||
<!--当子Item数量大于等于指定数量时,开启等宽,此属性优先级最高-->
|
||||
<attr name="tab_item_equ_width_count" format="integer" />
|
||||
<!--[~3] 小于等于3个 [3~] 大于等于3个 [3~5] 3<= <=5 -->
|
||||
<attr name="tab_item_equ_width_count_range" format="string" />
|
||||
<!--智能判断Item是否等宽, 如果所有子项, 未撑满tab时, 开启等宽模式.此属性会覆盖[tab_item_is_equ_width]-->
|
||||
<attr name="tab_item_auto_equ_width" format="boolean" />
|
||||
<!--默认选中的索引值-->
|
||||
<attr name="tab_default_index" format="integer" />
|
||||
<!--等宽模式下, 指定item的宽度. 不指定则平分-->
|
||||
<attr name="tab_item_width" format="dimension" />
|
||||
<!--在TabLayout wrap_content时, child match_parent时的高度-->
|
||||
<attr name="tab_item_default_height" format="dimension" />
|
||||
<!--是否绘制边框-->
|
||||
<attr name="tab_draw_border" format="boolean" />
|
||||
<!--是否绘制分割线-->
|
||||
<attr name="tab_draw_divider" format="boolean" />
|
||||
<!--是否绘制指示器-->
|
||||
<attr name="tab_draw_indicator" format="boolean" />
|
||||
<!--高凸模式下的背景drawable-->
|
||||
<attr name="tab_convex_background" format="reference|color" />
|
||||
<!--是否激活滑动选择模式-->
|
||||
<attr name="tab_enable_selector_mode" format="boolean" />
|
||||
<!--方向-->
|
||||
<attr name="tab_orientation" format="enum">
|
||||
<enum name="VERTICAL" value="1" />
|
||||
<enum name="HORIZONTAL" value="0" />
|
||||
</attr>
|
||||
<attr name="tab_layout_scroll_anim" format="boolean" />
|
||||
<attr name="tab_scroll_anim_duration" format="integer" />
|
||||
<!--预览的布局id-->
|
||||
<attr name="tab_preview_item_layout_id" format="reference" />
|
||||
<!--预览的布局数量-->
|
||||
<attr name="tab_preview_item_count" format="integer" />
|
||||
|
||||
<!--indicator 指示器相关属性-->
|
||||
|
||||
<!--强制指定指示器的Drawable-->
|
||||
<attr name="tab_indicator_drawable" format="reference" />
|
||||
<!--强制指定Drawable的颜色-->
|
||||
<attr name="tab_indicator_color" format="color" />
|
||||
<!--指示器的绘制类型, 可以使用[STYLE_TOP|STYLE_FOREGROUND]组合配置-->
|
||||
<attr name="tab_indicator_style" format="flags">
|
||||
<!--不绘制-->
|
||||
<flag name="STYLE_NONE" value="0" />
|
||||
<flag name="STYLE_TOP" value="0x1" />
|
||||
<flag name="STYLE_BOTTOM" value="0x2" />
|
||||
<flag name="STYLE_CENTER" value="0x4" />
|
||||
<!--前景绘制-->
|
||||
<flag name="STYLE_FOREGROUND" value="0x1000" />
|
||||
</attr>
|
||||
<!--指示器的重力-->
|
||||
<attr name="tab_indicator_gravity" format="enum">
|
||||
<!--指示器靠左显示-->
|
||||
<enum name="GRAVITY_START" value="0x1" />
|
||||
<!--指示器靠右显示-->
|
||||
<enum name="GRAVITY_END" value="0x2" />
|
||||
<!--指示器居中显示-->
|
||||
<enum name="GRAVITY_CENTER" value="0x4" />
|
||||
</attr>
|
||||
<!--是否激活流式效果, ViewPager在滚动时, 指示器的宽度由小变大,再由大变小-->
|
||||
<attr name="tab_indicator_enable_flow" format="boolean" />
|
||||
<!--闪现效果-->
|
||||
<attr name="tab_indicator_enable_flash" format="boolean" />
|
||||
<!--闪现效果使用clip处理-->
|
||||
<attr name="tab_indicator_enable_flash_clip" format="boolean" />
|
||||
<!--tab child的索引相差多少值时, 才开启flow效果-->
|
||||
<attr name="tab_indicator_flow_step" format="integer" />
|
||||
<!--指示器的宽度-->
|
||||
<attr name="tab_indicator_width" format="dimension|flags">
|
||||
<flag name="WRAP_CONTENT" value="-2" />
|
||||
<flag name="MATCH_PARENT" value="-1" />
|
||||
</attr>
|
||||
<!--宽度的补偿-->
|
||||
<attr name="tab_indicator_width_offset" format="dimension" />
|
||||
<!--同上-->
|
||||
<attr name="tab_indicator_height" format="dimension|flags">
|
||||
<flag name="WRAP_CONTENT" value="-2" />
|
||||
<flag name="MATCH_PARENT" value="-1" />
|
||||
</attr>
|
||||
<!--同上-->
|
||||
<attr name="tab_indicator_height_offset" format="dimension" />
|
||||
<!--x轴的补偿-->
|
||||
<attr name="tab_indicator_x_offset" format="dimension" />
|
||||
<!--y轴的补偿, 会根据[tab_indicator_style]的类型, 自动取负值.-->
|
||||
<attr name="tab_indicator_y_offset" format="dimension" />
|
||||
<!--指示器child的锚点索引, 用来辅助定位计算指示器的宽度,高度,中点坐标-->
|
||||
<attr name="tab_indicator_content_index" format="integer" />
|
||||
<!--指示器child的锚点控件id, 用来辅助定位计算指示器的宽度,高度,中点坐标-->
|
||||
<attr name="tab_indicator_content_id" format="reference" />
|
||||
<!--切换指示器时, 是否需要动画的支持-->
|
||||
<attr name="tab_indicator_anim" format="boolean" />
|
||||
|
||||
<!--请参考[GradientDrawable](https://developer.android.google.cn/reference/android/graphics/drawable/GradientDrawable)-->
|
||||
<attr name="tab_indicator_shape" format="enum">
|
||||
<enum name="RECTANGLE" value="0" />
|
||||
<enum name="OVAL" value="1" />
|
||||
<enum name="LINE" value="2" />
|
||||
<enum name="RING" value="3" />
|
||||
</attr>
|
||||
<attr name="tab_indicator_solid_color" format="color" />
|
||||
<attr name="tab_indicator_stroke_color" format="color" />
|
||||
<attr name="tab_indicator_stroke_width" format="dimension" />
|
||||
<attr name="tab_indicator_dash_width" format="dimension" />
|
||||
<attr name="tab_indicator_dash_gap" format="dimension" />
|
||||
<attr name="tab_indicator_radius" format="dimension" />
|
||||
<attr name="tab_indicator_radii" format="string" />
|
||||
<attr name="tab_indicator_gradient_colors" format="string" />
|
||||
<attr name="tab_indicator_gradient_start_color" format="color" />
|
||||
<attr name="tab_indicator_gradient_end_color" format="color" />
|
||||
<attr name="tab_indicator_ignore_child_padding" format="boolean" />
|
||||
<!--end...-->
|
||||
|
||||
<!--TabLayoutConfig 相关属性-->
|
||||
<!--item选中时 文本的颜色-->
|
||||
<attr name="tab_select_color" format="color" />
|
||||
<!--item未选中时 文本的颜色-->
|
||||
<attr name="tab_deselect_color" format="color" />
|
||||
<!--选中时 图标(Drawable)的颜色, 默认是文本的颜色-->
|
||||
<attr name="tab_ico_select_color" format="color" />
|
||||
<!--未选中时 图标(Drawable)的颜色, 默认是文本的颜色-->
|
||||
<attr name="tab_ico_deselect_color" format="color" />
|
||||
<!--是否激活自动设置文本的颜色-->
|
||||
<attr name="tab_enable_text_color" format="boolean" />
|
||||
<!--是否激活自动设置图标的颜色-->
|
||||
<attr name="tab_enable_ico_color" format="boolean" />
|
||||
<!--是否激活文本变粗-->
|
||||
<attr name="tab_enable_text_bold" format="boolean" />
|
||||
<!--是否使用字体的方式设置变粗效果, 需要先开启[tab_enable_text_bold]-->
|
||||
<attr name="tab_use_typeface_bold" format="boolean" />
|
||||
<!--是否激活文本颜色渐变-->
|
||||
<attr name="tab_enable_gradient_color" format="boolean" />
|
||||
<!--是否激活指示器的颜色渐变效果-->
|
||||
<attr name="tab_enable_indicator_gradient_color" format="boolean" />
|
||||
<!--是否激活图标颜色渐变-->
|
||||
<attr name="tab_enable_ico_gradient_color" format="boolean" />
|
||||
<!--是否激活缩放渐变-->
|
||||
<attr name="tab_enable_gradient_scale" format="boolean" />
|
||||
<!--缩放渐变的最小值-->
|
||||
<attr name="tab_min_scale" format="float" />
|
||||
<!--缩放渐变的最大值-->
|
||||
<attr name="tab_max_scale" format="float" />
|
||||
<!--是否激活文本大小渐变-->
|
||||
<attr name="tab_enable_gradient_text_size" format="boolean" />
|
||||
<!--文本字体大小最小值-->
|
||||
<attr name="tab_text_min_size" format="dimension" />
|
||||
<!--文本字体大小最大值-->
|
||||
<attr name="tab_text_max_size" format="dimension" />
|
||||
<!--指定文本控件的id, 所有文本属性改变, 将会发生在这个控件上, 如果指定的控件不存在, 控件会降权至[ItemView]-->
|
||||
<attr name="tab_text_view_id" format="reference" />
|
||||
<!--指定图标控件的id, 同上-->
|
||||
<attr name="tab_icon_view_id" format="reference" />
|
||||
<!--end...-->
|
||||
|
||||
<!--Divider 分割线相关属性-->
|
||||
<!--强制指定分割线的Drawable-->
|
||||
<attr name="tab_divider_drawable" format="reference" />
|
||||
<!--参考[GradientDrawable](https://developer.android.google.cn/reference/android/graphics/drawable/GradientDrawable)-->
|
||||
<attr name="tab_divider_stroke_color" format="color" />
|
||||
<attr name="tab_divider_solid_color" format="color" />
|
||||
<attr name="tab_divider_stroke_width" format="dimension" />
|
||||
<attr name="tab_divider_radius_size" format="dimension" />
|
||||
<!--分割线margin距离-->
|
||||
<attr name="tab_divider_margin_left" format="dimension" />
|
||||
<attr name="tab_divider_margin_right" format="dimension" />
|
||||
<attr name="tab_divider_margin_top" format="dimension" />
|
||||
<attr name="tab_divider_margin_bottom" format="dimension" />
|
||||
<!--分割线的宽度-->
|
||||
<attr name="tab_divider_width" format="dimension" />
|
||||
<attr name="tab_divider_height" format="dimension" />
|
||||
<!--分割线显示的位置-->
|
||||
<attr name="tab_divider_show_mode" format="flags">
|
||||
<flag name="SHOW_DIVIDER_BEGINNING" value="1" />
|
||||
<flag name="SHOW_DIVIDER_MIDDLE" value="2" />
|
||||
<flag name="SHOW_DIVIDER_END" value="4" />
|
||||
</attr>
|
||||
<!--end...-->
|
||||
|
||||
<!--Border 边框相关属性-->
|
||||
<!--强制指定边框的Drawable-->
|
||||
<attr name="tab_border_drawable" format="reference" />
|
||||
<!--参考[GradientDrawable](https://developer.android.google.cn/reference/android/graphics/drawable/GradientDrawable)-->
|
||||
<attr name="tab_border_stroke_color" format="color" />
|
||||
<attr name="tab_border_solid_color" format="color" />
|
||||
<attr name="tab_border_stroke_width" format="dimension" />
|
||||
<attr name="tab_border_radius_size" format="dimension" />
|
||||
<!--边框是否要负责绘制item的背景, 可以自动根据边框的圆角自动配置给item-->
|
||||
<attr name="tab_border_draw_item_background" format="boolean" />
|
||||
<!--高度补偿-->
|
||||
<attr name="tab_border_item_background_height_offset" format="dimension" />
|
||||
<!--宽度补偿-->
|
||||
<attr name="tab_border_item_background_width_offset" format="dimension" />
|
||||
<attr name="tab_border_keep_item_radius" format="boolean" />
|
||||
<attr name="tab_border_item_background_solid_color" format="color" />
|
||||
<attr name="tab_border_item_background_solid_disable_color" format="color" />
|
||||
<attr name="tab_border_item_background_gradient_start_color" format="color" />
|
||||
<attr name="tab_border_item_background_gradient_end_color" format="color" />
|
||||
<!--end...-->
|
||||
|
||||
<!--Badge 角标相关属性-->
|
||||
<!--是否绘制角标-->
|
||||
<attr name="tab_draw_badge" format="boolean" />
|
||||
<!--角标的背景填充颜色-->
|
||||
<attr name="tab_badge_solid_color" format="color" />
|
||||
<!--角标文本的颜色-->
|
||||
<attr name="tab_badge_text_color" format="color" />
|
||||
<!--圆点状态时的半径大小-->
|
||||
<attr name="tab_badge_circle_radius" format="dimension" />
|
||||
<!--角标的圆角半径-->
|
||||
<attr name="tab_badge_radius" format="dimension" />
|
||||
<!--角标重力-->
|
||||
<attr name="tab_badge_gravity" format="flags">
|
||||
<flag name="top" value="0x30" />
|
||||
<flag name="bottom" value="0x50" />
|
||||
<flag name="left" value="0x03" />
|
||||
<flag name="right" value="0x05" />
|
||||
<flag name="center_vertical" value="0x10" />
|
||||
<flag name="center_horizontal" value="0x01" />
|
||||
<flag name="center" value="0x11" />
|
||||
</attr>
|
||||
<!--x轴方向的偏移量, 会根据[Gravity]自动取负值-->
|
||||
<attr name="tab_badge_offset_x" format="dimension" />
|
||||
<!--同上-->
|
||||
<attr name="tab_badge_offset_y" format="dimension" />
|
||||
<!--参考[View]的padding属性-->
|
||||
<attr name="tab_badge_padding_left" format="dimension" />
|
||||
<attr name="tab_badge_padding_right" format="dimension" />
|
||||
<attr name="tab_badge_padding_top" format="dimension" />
|
||||
<attr name="tab_badge_padding_bottom" format="dimension" />
|
||||
<!--角标的文本内容, 多用于xml预览-->
|
||||
<attr name="tab_badge_text" format="string" />
|
||||
<!--角标的文本字体大小-->
|
||||
<attr name="tab_badge_text_size" format="dimension" />
|
||||
|
||||
<!--角标[tab_badge_gravity]定位锚点-->
|
||||
<attr name="tab_badge_anchor_child_index" format="integer" />
|
||||
<!--是否要忽略锚点view的padding-->
|
||||
<attr name="tab_badge_ignore_child_padding" format="boolean" />
|
||||
<!--角标圆形状态下的单独配置的偏移-->
|
||||
<attr name="tab_badge_circle_offset_x" format="dimension" />
|
||||
<!--角标圆形状态下的单独配置的偏移-->
|
||||
<attr name="tab_badge_circle_offset_y" format="dimension" />
|
||||
|
||||
<attr name="tab_badge_stroke_color" format="color" />
|
||||
<attr name="tab_badge_stroke_width" format="dimension" />
|
||||
|
||||
<attr name="tab_badge_min_width" format="dimension|flags">
|
||||
<flag name="WRAP_HEIGHT" value="-1" />
|
||||
<flag name="NONE" value="-2" />
|
||||
</attr>
|
||||
<attr name="tab_badge_min_height" format="dimension">
|
||||
<flag name="NONE" value="-2" />
|
||||
</attr>
|
||||
|
||||
<!--Highlight 突出相关属性-->
|
||||
<attr name="tab_draw_highlight" format="boolean" />
|
||||
<attr name="tab_highlight_drawable" format="reference" />
|
||||
<attr name="tab_highlight_width_offset" format="dimension" />
|
||||
<attr name="tab_highlight_height_offset" format="dimension" />
|
||||
<!--突出的宽度-->
|
||||
<attr name="tab_highlight_width" format="dimension|flags">
|
||||
<flag name="WRAP_CONTENT" value="-2" />
|
||||
<flag name="MATCH_PARENT" value="-1" />
|
||||
</attr>
|
||||
<attr name="tab_highlight_height" format="dimension|flags">
|
||||
<flag name="WRAP_CONTENT" value="-2" />
|
||||
<flag name="MATCH_PARENT" value="-1" />
|
||||
</attr>
|
||||
<!--end...-->
|
||||
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="DslTabLayout_Layout">
|
||||
<!--支持按比例设置宽度, sw屏幕宽度 pw父宽度, 0.5sw:屏幕宽度的0.5倍, 0.3pw:父宽度的0.3倍-->
|
||||
<attr name="layout_tab_width" format="string" />
|
||||
<!--同上 sh, ph-->
|
||||
<attr name="layout_tab_height" format="string" />
|
||||
<!--高凸模式, 需要高凸的高度-->
|
||||
<attr name="layout_tab_convex_height" format="dimension" />
|
||||
<!--单独指定child的锚点索引-->
|
||||
<attr name="layout_tab_indicator_content_index" format="integer" />
|
||||
<!--单独指定child的锚点控件的id-->
|
||||
<attr name="layout_tab_indicator_content_id" format="reference" />
|
||||
<!--android.widget.LinearLayout.LayoutParams.weight 剩余空间所占比例-->
|
||||
<attr name="layout_tab_weight" format="float" />
|
||||
<!--单独配置的drawable, 可以覆盖tab中的配置-->
|
||||
<attr name="layout_highlight_drawable" format="reference" />
|
||||
|
||||
<attr name="layout_tab_text_view_index" format="integer" />
|
||||
<attr name="layout_tab_icon_view_index" format="integer" />
|
||||
<attr name="layout_tab_text_view_id" format="reference" />
|
||||
<attr name="layout_tab_icon_view_id" format="reference" />
|
||||
</declare-styleable>
|
||||
</resources>
|
1
ViewPager2Delegate/.gitignore
vendored
Normal file
1
ViewPager2Delegate/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
33
ViewPager2Delegate/build.gradle
Normal file
33
ViewPager2Delegate/build.gradle
Normal file
@ -0,0 +1,33 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "30.0.2"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 28
|
||||
targetSdkVersion 33
|
||||
versionCode 1
|
||||
versionName "1.2"
|
||||
|
||||
consumerProguardFiles 'consumer-rules.pro'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//https://github.com/angcyo/DslTabLayout
|
||||
implementation project(':TabLayout')
|
||||
//https://mvnrepository.com/artifact/androidx.viewpager2/viewpager2
|
||||
implementation 'androidx.viewpager2:viewpager2:1.0.0'
|
||||
}
|
||||
|
||||
//apply from: "$gradleHost/master/publish.gradle"
|
0
ViewPager2Delegate/consumer-rules.pro
Normal file
0
ViewPager2Delegate/consumer-rules.pro
Normal file
21
ViewPager2Delegate/proguard-rules.pro
vendored
Normal file
21
ViewPager2Delegate/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
ViewPager2Delegate/src/main/AndroidManifest.xml
Normal file
2
ViewPager2Delegate/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.angcyo.tablayout.delegate2" />
|
@ -0,0 +1,66 @@
|
||||
package com.angcyo.tablayout.delegate2
|
||||
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.angcyo.tablayout.DslTabLayout
|
||||
import com.angcyo.tablayout.ViewPagerDelegate
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/**
|
||||
* 兼容[ViewPager2]
|
||||
* Email:angcyo@126.com
|
||||
* @author angcyo
|
||||
* @date 2019/12/14
|
||||
*/
|
||||
open class ViewPager2Delegate(
|
||||
val viewPager: ViewPager2,
|
||||
val dslTabLayout: DslTabLayout?,
|
||||
val forceSmoothScroll: Boolean? = null
|
||||
) : ViewPager2.OnPageChangeCallback(), ViewPagerDelegate {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* [forceSmoothScroll] 为 null, 只有切换左右page时, 才有VP的动画, 否则没有.
|
||||
* */
|
||||
fun install(
|
||||
viewPager: ViewPager2,
|
||||
dslTabLayout: DslTabLayout?,
|
||||
forceSmoothScroll: Boolean? = null
|
||||
): ViewPager2Delegate {
|
||||
return ViewPager2Delegate(viewPager, dslTabLayout, forceSmoothScroll)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
viewPager.registerOnPageChangeCallback(this)
|
||||
dslTabLayout?.setupViewPager(this)
|
||||
}
|
||||
|
||||
override fun onGetCurrentItem(): Int {
|
||||
return viewPager.currentItem
|
||||
}
|
||||
|
||||
override fun onSetCurrentItem(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
reselect: Boolean,
|
||||
fromUser: Boolean
|
||||
) {
|
||||
if (fromUser) {
|
||||
val smoothScroll = forceSmoothScroll ?: ((toIndex - fromIndex).absoluteValue <= 1)
|
||||
viewPager.setCurrentItem(toIndex, smoothScroll)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
dslTabLayout?.onPageScrollStateChanged(state)
|
||||
}
|
||||
|
||||
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
|
||||
dslTabLayout?.onPageScrolled(position, positionOffset, positionOffsetPixels)
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
dslTabLayout?.onPageSelected(position)
|
||||
}
|
||||
}
|
@ -5,25 +5,25 @@ plugins {
|
||||
android {
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file('D:\\AndroidKeys\\yutou.jks')
|
||||
storeFile file('C:\\Users\\58381\\Documents\\AndroidSign\\yutou.jks')
|
||||
storePassword '34864394'
|
||||
keyAlias 'yutou'
|
||||
keyPassword '34864394'
|
||||
}
|
||||
yutou {
|
||||
storeFile file('D:\\AndroidKeys\\yutou.jks')
|
||||
storeFile file('C:\\Users\\58381\\Documents\\AndroidSign\\yutou.jks')
|
||||
storePassword '34864394'
|
||||
keyAlias 'yutou'
|
||||
keyPassword '34864394'
|
||||
}
|
||||
}
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "30.0.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.yutou.passmanage"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
minSdkVersion 28
|
||||
targetSdkVersion 33
|
||||
versionCode 1
|
||||
versionName "1.2"
|
||||
|
||||
@ -59,5 +59,6 @@ dependencies {
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
|
||||
api project(path:':TabLayout')
|
||||
api project(path:':ViewPager2Delegate')
|
||||
}
|
@ -11,7 +11,8 @@
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.MyPassworldManage">
|
||||
<activity android:name=".MainActivity">
|
||||
<activity android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
package com.yutou.passmanage.Interfaces;
|
||||
|
||||
public interface NetworkInterface {
|
||||
public abstract class NetworkInterface<T> {
|
||||
/**
|
||||
* 请求成功
|
||||
* @param data 请求参数
|
||||
* @param state http状态
|
||||
*/
|
||||
void httpGetData(Object data, int state);
|
||||
public void httpGetData(T data, int state){}
|
||||
|
||||
/**
|
||||
* 请求异常
|
||||
* @param e 异常
|
||||
*/
|
||||
void httpError(Exception e);
|
||||
public void httpError(Exception e){}
|
||||
}
|
||||
|
@ -5,9 +5,11 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
@ -16,6 +18,7 @@ import android.widget.Toast;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.angcyo.tablayout.DslTabLayout;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.yutou.passmanage.Adapters.PassWordListAdapter;
|
||||
import com.yutou.passmanage.Datas.AppData;
|
||||
@ -25,6 +28,8 @@ import com.yutou.passmanage.Interfaces.NetworkInterface;
|
||||
import com.yutou.passmanage.Tools.NetworkTool;
|
||||
import com.yutou.passmanage.Tools.RoomDatabaseManager;
|
||||
import com.yutou.passmanage.Tools.Tools;
|
||||
import com.yutou.passmanage.bean.PasswordBean;
|
||||
import com.yutou.passmanage.bean.PasswordTypeBean;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -49,6 +54,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
FloatingActionButton floatButton;
|
||||
PassWordListAdapter adapter;
|
||||
TextView authError;
|
||||
DslTabLayout tabLayout;
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
@ -63,6 +69,12 @@ public class MainActivity extends AppCompatActivity {
|
||||
BiometricPrompt prompt;
|
||||
|
||||
void auth() {
|
||||
if(true){
|
||||
init();
|
||||
authError.setVisibility(View.GONE);
|
||||
passList.setVisibility(View.VISIBLE);
|
||||
return;
|
||||
}
|
||||
BiometricManager manager = BiometricManager.from(this);
|
||||
if (manager.canAuthenticate(BIOMETRIC_WEAK | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||
|
||||
@ -122,6 +134,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
clearSearch = findViewById(R.id.clearSearch);
|
||||
passList = findViewById(R.id.passList);
|
||||
floatButton = findViewById(R.id.floatButton);
|
||||
tabLayout = findViewById(R.id.tabLayout);
|
||||
floatButton.setOnClickListener(v -> showAddPasswordDialog());
|
||||
passList.setLayoutManager(new LinearLayoutManager(this));
|
||||
passList.setAdapter(adapter);
|
||||
@ -241,7 +254,29 @@ public class MainActivity extends AppCompatActivity {
|
||||
ToolsPasswordDao dao;
|
||||
|
||||
void initData() {
|
||||
|
||||
NetworkTool.httpGet(NetworkTool.NetworkAPI.PASSWORD_TYPE_ALL, new JSONObject(), new NetworkInterface<JSONObject>() {
|
||||
@Override
|
||||
public void httpGetData(JSONObject json, int state) {
|
||||
List<PasswordTypeBean> data = JSONArray.parseArray(json.getJSONArray("data").toJSONString(), PasswordTypeBean.class);
|
||||
for (PasswordTypeBean bean : data) {
|
||||
Button tab = new Button(MainActivity.this);
|
||||
tab.setOnClickListener(view -> {
|
||||
|
||||
});
|
||||
tab.setText(bean.getTitle());
|
||||
tab.setTag(bean);
|
||||
tabLayout.addView(tab);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void httpError(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
new Thread(() -> {
|
||||
|
||||
List<ToolsPassword> list = dao.getAllAndRemove();
|
||||
if (list.size() == 0) {
|
||||
authError.setText("列表为空,点击右下角+新增配置");
|
||||
@ -303,11 +338,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
NetworkTool.httpGet(NetworkTool.NetworkAPI.PASSWORD_ALL, new JSONObject(), new NetworkInterface() {
|
||||
NetworkTool.httpGet(NetworkTool.NetworkAPI.PASSWORD_ALL, new JSONObject(), new NetworkInterface<JSONObject>() {
|
||||
@Override
|
||||
public void httpGetData(Object data, int state) {
|
||||
data = ((String) data).replace("desc", "info");
|
||||
JSONObject json = JSONObject.parseObject((String) data);
|
||||
public void httpGetData(JSONObject json, int state) {
|
||||
// data = ((String) data).replace("desc", "info");
|
||||
// JSONObject json = JSONObject.parseObject((String) data);
|
||||
if (json.getInteger("code") == 0) {
|
||||
List<ToolsPassword> list = JSONArray.parseArray(json.getJSONArray("data").toJSONString(), ToolsPassword.class);
|
||||
new Thread(() -> {
|
||||
|
@ -3,6 +3,8 @@ package com.yutou.passmanage.Tools;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONException;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.yutou.passmanage.Datas.AppData;
|
||||
@ -13,9 +15,12 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
@ -36,7 +41,7 @@ public class NetworkTool {
|
||||
}
|
||||
|
||||
|
||||
public static void httpGet(String url, JSONObject body, NetworkInterface networkInterface) {
|
||||
public static <T> void httpGet(String url, JSONObject body, NetworkInterface<T> networkInterface) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -60,15 +65,26 @@ public class NetworkTool {
|
||||
public void run() {
|
||||
if (networkInterface != null) {
|
||||
try {
|
||||
networkInterface.httpGetData(str.toString(), connection.getResponseCode());
|
||||
} catch (IOException e) {
|
||||
JSONObject json=JSONObject.parseObject(str.toString());
|
||||
Type[] types = ((ParameterizedType) networkInterface.getClass().getGenericSuperclass()).getActualTypeArguments();
|
||||
if(types.length==0){
|
||||
networkInterface.httpGetData((T) JSONObject.parseObject(str.toString()),connection.getResponseCode());
|
||||
}else {
|
||||
Type type=types[0];
|
||||
if (type.getTypeName().contains("JSON")) {
|
||||
networkInterface.httpGetData((T) JSONObject.parseObject(str.toString()), connection.getResponseCode());
|
||||
}else {
|
||||
networkInterface.httpGetData(JSONObject.parseObject(json.getJSONObject("data").toJSONString(), type), connection.getResponseCode());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
//Log.i(TAG + "[" + url + "]", "body:" + str + " (" + connection.getResponseCode() + ")");
|
||||
Log.i(TAG + "[" + url + "]", "body:" + str + " (" + connection.getResponseCode() + ")");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
AppData.handler.post(new Runnable() {
|
||||
@ -129,7 +145,7 @@ public class NetworkTool {
|
||||
connection.setReadTimeout(10 * 1000);
|
||||
//connection.addRequestProperty("Connection", "keep-alive");
|
||||
//connection.addRequestProperty("User-Agent", getExtUa());
|
||||
// connection.addRequestProperty("content-type", "application/json");
|
||||
// connection.addRequestProperty("content-type", "application/json");
|
||||
connection.addRequestProperty("charset", "UTF-8");
|
||||
OutputStream outputStream = connection.getOutputStream();
|
||||
|
||||
@ -142,7 +158,7 @@ public class NetworkTool {
|
||||
}
|
||||
final String finalStr = str.toString();
|
||||
|
||||
// Log.i(TAG + "[" + url + "?" + toGetSplice(body) + "]", "body:" + str + " (" + connection.getResponseCode() + ")");
|
||||
Log.i(TAG + "[" + url + "?" + toGetSplice(body) + "]", "body:" + str + " (" + connection.getResponseCode() + ")");
|
||||
AppData.handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -0,0 +1,6 @@
|
||||
package com.yutou.passmanage.bean;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class BaseBean implements Serializable {
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package com.yutou.passmanage.bean;
|
||||
|
||||
public class PasswordBean extends BaseBean{
|
||||
private int id;
|
||||
private String title;
|
||||
private String titlePinyin;
|
||||
private String username;
|
||||
private String password;
|
||||
private String url;
|
||||
private String info;
|
||||
private int type;
|
||||
private int uid;
|
||||
|
||||
public PasswordBean() {
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getTitlePinyin() {
|
||||
return titlePinyin;
|
||||
}
|
||||
|
||||
public void setTitlePinyin(String titlePinyin) {
|
||||
this.titlePinyin = titlePinyin;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public void setInfo(String info) {
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(int uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.yutou.passmanage.bean;
|
||||
|
||||
public class PasswordTypeBean extends BaseBean{
|
||||
private int id;
|
||||
private String title;
|
||||
private int uid;
|
||||
|
||||
public PasswordTypeBean() {
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public int getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(int uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PasswordTypeBean{" +
|
||||
"id=" + id +
|
||||
", title='" + title + '\'' +
|
||||
", uid=" + uid +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -29,16 +29,24 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.angcyo.tablayout.DslTabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
</com.angcyo.tablayout.DslTabLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/passList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tabLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/search" >
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
app:layout_constraintTop_toBottomOf="@+id/search"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/goneText"
|
||||
|
@ -18,7 +18,7 @@
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/account" />
|
||||
android:text="@string/title" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/title"
|
||||
@ -27,7 +27,7 @@
|
||||
android:layout_weight="1"
|
||||
android:ems="10"
|
||||
android:inputType="textPersonName"
|
||||
android:hint="@string/account" />
|
||||
android:hint="@string/title" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
<string name="app_name">密码管理器</string>
|
||||
<string name="password">密码</string>
|
||||
<string name="account">账号</string>
|
||||
<string name="title">标题</string>
|
||||
<string name="info">备注</string>
|
||||
<string name="name">名字</string>
|
||||
<string name="toast_copy_password">已复制密码</string>
|
||||
|
@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.0'
|
||||
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
|
@ -16,4 +16,9 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
android.enableJetifier=true
|
||||
|
||||
systemProp.http.proxyHost=127.0.0.1
|
||||
systemProp.https.proxyHost=127.0.0.1
|
||||
systemProp.https.proxyPort=7980
|
||||
systemProp.http.proxyPort=7890
|
||||
|
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
@ -1,2 +1,4 @@
|
||||
include ':app'
|
||||
include ':ViewPager2Delegate'
|
||||
include ':TabLayout'
|
||||
rootProject.name = "MyPassworldManage"
|
Loading…
Reference in New Issue
Block a user