This commit is contained in:
zlzw 2024-08-02 10:34:16 +08:00
parent d305c7809c
commit 9e889a2f14
42 changed files with 6797 additions and 27 deletions

1
TabLayout/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

29
TabLayout/build.gradle Normal file
View 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"

View File

21
TabLayout/proguard-rules.pro vendored Normal file
View 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

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.angcyo.tablayout" />

View 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="基类方法">
}

View 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)
}
}

View File

@ -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()!!
}
}

View 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
}

View 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)
}
}

View 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
)

View 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)
}
}
}

View 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
}
}

View 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()
}
}
}
}

View 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
}

File diff suppressed because it is too large Load Diff

View File

@ -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)
)
}
}
}

View File

@ -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
)
}

View 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
}

View File

@ -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)
}

View 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
View File

@ -0,0 +1 @@
/build

View 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"

View File

21
ViewPager2Delegate/proguard-rules.pro vendored Normal file
View 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

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.angcyo.tablayout.delegate2" />

View File

@ -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)
}
}

View File

@ -5,25 +5,25 @@ plugins {
android { android {
signingConfigs { signingConfigs {
debug { debug {
storeFile file('D:\\AndroidKeys\\yutou.jks') storeFile file('C:\\Users\\58381\\Documents\\AndroidSign\\yutou.jks')
storePassword '34864394' storePassword '34864394'
keyAlias 'yutou' keyAlias 'yutou'
keyPassword '34864394' keyPassword '34864394'
} }
yutou { yutou {
storeFile file('D:\\AndroidKeys\\yutou.jks') storeFile file('C:\\Users\\58381\\Documents\\AndroidSign\\yutou.jks')
storePassword '34864394' storePassword '34864394'
keyAlias 'yutou' keyAlias 'yutou'
keyPassword '34864394' keyPassword '34864394'
} }
} }
compileSdkVersion 30 compileSdkVersion 33
buildToolsVersion "30.0.2" buildToolsVersion "30.0.2"
defaultConfig { defaultConfig {
applicationId "com.yutou.passmanage" applicationId "com.yutou.passmanage"
minSdkVersion 23 minSdkVersion 28
targetSdkVersion 30 targetSdkVersion 33
versionCode 1 versionCode 1
versionName "1.2" versionName "1.2"
@ -59,5 +59,6 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.biometric:biometric:1.1.0" implementation "androidx.biometric:biometric:1.1.0"
api project(path:':TabLayout')
api project(path:':ViewPager2Delegate')
} }

View File

@ -11,7 +11,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:theme="@style/Theme.MyPassworldManage"> android:theme="@style/Theme.MyPassworldManage">
<activity android:name=".MainActivity"> <activity android:name=".MainActivity"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -1,16 +1,16 @@
package com.yutou.passmanage.Interfaces; package com.yutou.passmanage.Interfaces;
public interface NetworkInterface { public abstract class NetworkInterface<T> {
/** /**
* 请求成功 * 请求成功
* @param data 请求参数 * @param data 请求参数
* @param state http状态 * @param state http状态
*/ */
void httpGetData(Object data, int state); public void httpGetData(T data, int state){}
/** /**
* 请求异常 * 请求异常
* @param e 异常 * @param e 异常
*/ */
void httpError(Exception e); public void httpError(Exception e){}
} }

View File

@ -5,9 +5,11 @@ import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.ImageView; import android.widget.ImageView;
@ -16,6 +18,7 @@ import android.widget.Toast;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.angcyo.tablayout.DslTabLayout;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.yutou.passmanage.Adapters.PassWordListAdapter; import com.yutou.passmanage.Adapters.PassWordListAdapter;
import com.yutou.passmanage.Datas.AppData; 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.NetworkTool;
import com.yutou.passmanage.Tools.RoomDatabaseManager; import com.yutou.passmanage.Tools.RoomDatabaseManager;
import com.yutou.passmanage.Tools.Tools; 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.ArrayList;
import java.util.List; import java.util.List;
@ -49,6 +54,7 @@ public class MainActivity extends AppCompatActivity {
FloatingActionButton floatButton; FloatingActionButton floatButton;
PassWordListAdapter adapter; PassWordListAdapter adapter;
TextView authError; TextView authError;
DslTabLayout tabLayout;
Handler handler = new Handler(Looper.getMainLooper()); Handler handler = new Handler(Looper.getMainLooper());
@Override @Override
@ -63,6 +69,12 @@ public class MainActivity extends AppCompatActivity {
BiometricPrompt prompt; BiometricPrompt prompt;
void auth() { void auth() {
if(true){
init();
authError.setVisibility(View.GONE);
passList.setVisibility(View.VISIBLE);
return;
}
BiometricManager manager = BiometricManager.from(this); BiometricManager manager = BiometricManager.from(this);
if (manager.canAuthenticate(BIOMETRIC_WEAK | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) { if (manager.canAuthenticate(BIOMETRIC_WEAK | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) {
@ -122,6 +134,7 @@ public class MainActivity extends AppCompatActivity {
clearSearch = findViewById(R.id.clearSearch); clearSearch = findViewById(R.id.clearSearch);
passList = findViewById(R.id.passList); passList = findViewById(R.id.passList);
floatButton = findViewById(R.id.floatButton); floatButton = findViewById(R.id.floatButton);
tabLayout = findViewById(R.id.tabLayout);
floatButton.setOnClickListener(v -> showAddPasswordDialog()); floatButton.setOnClickListener(v -> showAddPasswordDialog());
passList.setLayoutManager(new LinearLayoutManager(this)); passList.setLayoutManager(new LinearLayoutManager(this));
passList.setAdapter(adapter); passList.setAdapter(adapter);
@ -241,7 +254,29 @@ public class MainActivity extends AppCompatActivity {
ToolsPasswordDao dao; ToolsPasswordDao dao;
void initData() { 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(() -> { new Thread(() -> {
List<ToolsPassword> list = dao.getAllAndRemove(); List<ToolsPassword> list = dao.getAllAndRemove();
if (list.size() == 0) { if (list.size() == 0) {
authError.setText("列表为空,点击右下角+新增配置"); 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 @Override
public void httpGetData(Object data, int state) { public void httpGetData(JSONObject json, int state) {
data = ((String) data).replace("desc", "info"); // data = ((String) data).replace("desc", "info");
JSONObject json = JSONObject.parseObject((String) data); // JSONObject json = JSONObject.parseObject((String) data);
if (json.getInteger("code") == 0) { if (json.getInteger("code") == 0) {
List<ToolsPassword> list = JSONArray.parseArray(json.getJSONArray("data").toJSONString(), ToolsPassword.class); List<ToolsPassword> list = JSONArray.parseArray(json.getJSONArray("data").toJSONString(), ToolsPassword.class);
new Thread(() -> { new Thread(() -> {

View File

@ -3,6 +3,8 @@ package com.yutou.passmanage.Tools;
import android.util.Log; import android.util.Log;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.yutou.passmanage.Datas.AppData; import com.yutou.passmanage.Datas.AppData;
@ -13,9 +15,12 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.OutputStream; import java.io.OutputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.List;
import java.util.Set; 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() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -60,15 +65,26 @@ public class NetworkTool {
public void run() { public void run() {
if (networkInterface != null) { if (networkInterface != null) {
try { try {
networkInterface.httpGetData(str.toString(), connection.getResponseCode()); JSONObject json=JSONObject.parseObject(str.toString());
} catch (IOException e) { 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(); e.printStackTrace();
} }
} }
} }
}); });
//Log.i(TAG + "[" + url + "]", "body:" + str + " (" + connection.getResponseCode() + ")"); Log.i(TAG + "[" + url + "]", "body:" + str + " (" + connection.getResponseCode() + ")");
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
AppData.handler.post(new Runnable() { AppData.handler.post(new Runnable() {
@ -142,7 +158,7 @@ public class NetworkTool {
} }
final String finalStr = str.toString(); 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() { AppData.handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {

View File

@ -0,0 +1,6 @@
package com.yutou.passmanage.bean;
import java.io.Serializable;
public class BaseBean implements Serializable {
}

View File

@ -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;
}
}

View File

@ -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 +
'}';
}
}

View File

@ -29,16 +29,24 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/passList" android:id="@+id/passList"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@+id/tabLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search" > app:layout_constraintTop_toBottomOf="@+id/search"/>
</androidx.recyclerview.widget.RecyclerView>
<TextView <TextView
android:id="@+id/goneText" android:id="@+id/goneText"

View File

@ -18,7 +18,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginStart="20dp" android:layout_marginStart="20dp"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/account" /> android:text="@string/title" />
<EditText <EditText
android:id="@+id/title" android:id="@+id/title"
@ -27,7 +27,7 @@
android:layout_weight="1" android:layout_weight="1"
android:ems="10" android:ems="10"
android:inputType="textPersonName" android:inputType="textPersonName"
android:hint="@string/account" /> android:hint="@string/title" />
</LinearLayout> </LinearLayout>

View File

@ -2,6 +2,7 @@
<string name="app_name">密码管理器</string> <string name="app_name">密码管理器</string>
<string name="password">密码</string> <string name="password">密码</string>
<string name="account">账号</string> <string name="account">账号</string>
<string name="title">标题</string>
<string name="info">备注</string> <string name="info">备注</string>
<string name="name">名字</string> <string name="name">名字</string>
<string name="toast_copy_password">已复制密码</string> <string name="toast_copy_password">已复制密码</string>

View File

@ -6,7 +6,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.0' 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 // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }

View File

@ -17,3 +17,8 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # 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

View 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

View File

@ -1,2 +1,4 @@
include ':app' include ':app'
include ':ViewPager2Delegate'
include ':TabLayout'
rootProject.name = "MyPassworldManage" rootProject.name = "MyPassworldManage"