Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
a0982360ec | |||
de339d31f7 | |||
9e889a2f14 | |||
d305c7809c | |||
ac900e5647 | |||
e12cd888b5 | |||
|
9057f8b765 | ||
|
0c5bebf60a | ||
|
0886fe1feb |
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/caches
|
||||||
|
/.idea/libraries
|
||||||
|
/.idea/modules.xml
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/navEditor.xml
|
||||||
|
/.idea/assetWizardSettings.xml
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx
|
||||||
|
local.properties
|
1
TabLayout/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
27
TabLayout/build.gradle
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 28
|
||||||
|
targetSdkVersion 33
|
||||||
|
|
||||||
|
consumerProguardFiles 'consumer-rules.pro'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namespace 'com.angcyo.tablayout'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
//apply from: "$gradleHost/master/publish.gradle"
|
0
TabLayout/consumer-rules.pro
Normal file
21
TabLayout/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
1
TabLayout/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
200
TabLayout/src/main/java/com/angcyo/tablayout/AbsDslDrawable.kt
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.*
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础自绘Drawable
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/11/25
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abstract class AbsDslDrawable : Drawable() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**不绘制*/
|
||||||
|
const val DRAW_TYPE_DRAW_NONE = 0x00
|
||||||
|
|
||||||
|
/**[android.view.View.draw]*/
|
||||||
|
const val DRAW_TYPE_DRAW_AFTER = 0x01
|
||||||
|
const val DRAW_TYPE_DRAW_BEFORE = 0x02
|
||||||
|
|
||||||
|
/**[android.view.View.onDraw]*/
|
||||||
|
const val DRAW_TYPE_ON_DRAW_AFTER = 0x04
|
||||||
|
const val DRAW_TYPE_ON_DRAW_BEFORE = 0x08
|
||||||
|
}
|
||||||
|
|
||||||
|
/**画笔*/
|
||||||
|
val textPaint: TextPaint by lazy {
|
||||||
|
TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
isFilterBitmap = true
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
textSize = 12 * dp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val drawRect = Rect()
|
||||||
|
val drawRectF = RectF()
|
||||||
|
|
||||||
|
/**需要在那个方法中触发绘制*/
|
||||||
|
var drawType = DRAW_TYPE_ON_DRAW_AFTER
|
||||||
|
|
||||||
|
/**xml属性读取*/
|
||||||
|
open fun initAttribute(
|
||||||
|
context: Context,
|
||||||
|
attributeSet: AttributeSet? = null
|
||||||
|
) {
|
||||||
|
//val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.xxx)
|
||||||
|
//typedArray.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
//<editor-fold desc="View相关方法">
|
||||||
|
|
||||||
|
/**附着的[View]*/
|
||||||
|
val attachView: View?
|
||||||
|
get() = if (callback is View) callback as? View else null
|
||||||
|
|
||||||
|
val isInEditMode: Boolean
|
||||||
|
get() = attachView?.isInEditMode ?: false
|
||||||
|
val paddingLeft: Int
|
||||||
|
get() = attachView?.paddingLeft ?: 0
|
||||||
|
val paddingRight: Int
|
||||||
|
get() = attachView?.paddingRight ?: 0
|
||||||
|
val paddingTop: Int
|
||||||
|
get() = attachView?.paddingTop ?: 0
|
||||||
|
val paddingBottom: Int
|
||||||
|
get() = attachView?.paddingBottom ?: 0
|
||||||
|
val viewHeight: Int
|
||||||
|
get() = attachView?.measuredHeight ?: 0
|
||||||
|
val viewWidth: Int
|
||||||
|
get() = attachView?.measuredWidth ?: 0
|
||||||
|
val viewDrawHeight: Int
|
||||||
|
get() = viewHeight - paddingTop - paddingBottom
|
||||||
|
val viewDrawWidth: Int
|
||||||
|
get() = viewWidth - paddingLeft - paddingRight
|
||||||
|
|
||||||
|
val isViewRtl: Boolean
|
||||||
|
get() = attachView != null && ViewCompat.getLayoutDirection(attachView!!) == ViewCompat.LAYOUT_DIRECTION_RTL
|
||||||
|
|
||||||
|
//</editor-fold desc="View相关方法">
|
||||||
|
|
||||||
|
//<editor-fold desc="基类方法">
|
||||||
|
|
||||||
|
/**核心方法, 绘制*/
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntrinsicWidth(): Int {
|
||||||
|
return super.getIntrinsicWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMinimumWidth(): Int {
|
||||||
|
return super.getMinimumWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntrinsicHeight(): Int {
|
||||||
|
return super.getIntrinsicHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMinimumHeight(): Int {
|
||||||
|
return super.getMinimumHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) {
|
||||||
|
if (textPaint.alpha != alpha) {
|
||||||
|
textPaint.alpha = alpha
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAlpha(): Int {
|
||||||
|
return textPaint.alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
super.setBounds(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setBounds(bounds: Rect) {
|
||||||
|
super.setBounds(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
//不透明度
|
||||||
|
override fun getOpacity(): Int {
|
||||||
|
return if (alpha < 255) PixelFormat.TRANSLUCENT else PixelFormat.OPAQUE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getColorFilter(): ColorFilter? {
|
||||||
|
return textPaint.colorFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||||
|
textPaint.colorFilter = colorFilter
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mutate(): Drawable {
|
||||||
|
return super.mutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDither(dither: Boolean) {
|
||||||
|
textPaint.isDither = dither
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setFilterBitmap(filter: Boolean) {
|
||||||
|
textPaint.isFilterBitmap = filter
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isFilterBitmap(): Boolean {
|
||||||
|
return textPaint.isFilterBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBoundsChange(bounds: Rect) {
|
||||||
|
super.onBoundsChange(bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLevelChange(level: Int): Boolean {
|
||||||
|
return super.onLevelChange(level)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChange(state: IntArray): Boolean {
|
||||||
|
return super.onStateChange(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTintList(tint: ColorStateList?) {
|
||||||
|
super.setTintList(tint)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTintMode(tintMode: PorterDuff.Mode?) {
|
||||||
|
super.setTintMode(tintMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTintBlendMode(blendMode: BlendMode?) {
|
||||||
|
super.setTintBlendMode(blendMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHotspot(x: Float, y: Float) {
|
||||||
|
super.setHotspot(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setHotspotBounds(left: Int, top: Int, right: Int, bottom: Int) {
|
||||||
|
super.setHotspotBounds(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getHotspotBounds(outRect: Rect) {
|
||||||
|
super.getHotspotBounds(outRect)
|
||||||
|
}
|
||||||
|
|
||||||
|
//</editor-fold desc="基类方法">
|
||||||
|
}
|
275
TabLayout/src/main/java/com/angcyo/tablayout/DslBadgeDrawable.kt
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未读数, 未读小红点, 角标绘制Drawable
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/12/13
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
open class DslBadgeDrawable : DslGradientDrawable() {
|
||||||
|
|
||||||
|
val dslGravity = DslGravity()
|
||||||
|
|
||||||
|
/**重力*/
|
||||||
|
var badgeGravity: Int = Gravity.CENTER //Gravity.TOP or Gravity.RIGHT
|
||||||
|
|
||||||
|
/**角标文本颜色*/
|
||||||
|
var badgeTextColor = Color.WHITE
|
||||||
|
|
||||||
|
/**角标的文本(默认居中绘制文本,暂不支持修改), 空字符串会绘制成小圆点
|
||||||
|
* null 不绘制角标
|
||||||
|
* "" 空字符绘制圆点
|
||||||
|
* 其他 正常绘制
|
||||||
|
* */
|
||||||
|
var badgeText: String? = null
|
||||||
|
|
||||||
|
/**角标的文本大小*/
|
||||||
|
var badgeTextSize: Float = 12 * dp
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
textPaint.textSize = field
|
||||||
|
}
|
||||||
|
|
||||||
|
/**当[badgeText]只有1个字符时, 使用圆形背景*/
|
||||||
|
var badgeAutoCircle: Boolean = true
|
||||||
|
|
||||||
|
/**圆点状态时的半径大小*/
|
||||||
|
var badgeCircleRadius = 4 * dpi
|
||||||
|
|
||||||
|
/**原点状态下, 单独配置的偏移*/
|
||||||
|
var badgeCircleOffsetX: Int = 0
|
||||||
|
var badgeCircleOffsetY: Int = 0
|
||||||
|
|
||||||
|
/**额外偏移距离, 会根据[Gravity]自动取负值*/
|
||||||
|
var badgeOffsetX: Int = 0
|
||||||
|
var badgeOffsetY: Int = 0
|
||||||
|
|
||||||
|
/**文本偏移*/
|
||||||
|
var badgeTextOffsetX: Int = 0
|
||||||
|
var badgeTextOffsetY: Int = 0
|
||||||
|
|
||||||
|
/**圆点状态时无效*/
|
||||||
|
var badgePaddingLeft = 0
|
||||||
|
var badgePaddingRight = 0
|
||||||
|
var badgePaddingTop = 0
|
||||||
|
var badgePaddingBottom = 0
|
||||||
|
|
||||||
|
/**最小的高度大小, px. 大于0生效; 圆点时属性无效*/
|
||||||
|
var badgeMinHeight = -2
|
||||||
|
|
||||||
|
/**最小的宽度大小, px. 大于0生效; 圆点时属性无效;
|
||||||
|
* -1 表示使用使用计算出来的高度值*/
|
||||||
|
var badgeMinWidth = -2
|
||||||
|
|
||||||
|
//计算属性
|
||||||
|
val textWidth: Float
|
||||||
|
get() = textPaint.textWidth(badgeText)
|
||||||
|
|
||||||
|
//最大的宽度
|
||||||
|
val maxWidth: Int
|
||||||
|
get() = max(
|
||||||
|
textWidth.toInt(),
|
||||||
|
originDrawable?.minimumWidth ?: 0
|
||||||
|
) + badgePaddingLeft + badgePaddingRight
|
||||||
|
|
||||||
|
//最大的高度
|
||||||
|
val maxHeight: Int
|
||||||
|
get() = max(
|
||||||
|
textHeight.toInt(),
|
||||||
|
originDrawable?.minimumHeight ?: 0
|
||||||
|
) + badgePaddingTop + badgePaddingBottom
|
||||||
|
|
||||||
|
val textHeight: Float
|
||||||
|
get() = textPaint.textHeight()
|
||||||
|
|
||||||
|
//原型状态
|
||||||
|
val isCircle: Boolean
|
||||||
|
get() = TextUtils.isEmpty(badgeText)
|
||||||
|
|
||||||
|
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||||
|
super.initAttribute(context, attributeSet)
|
||||||
|
updateOriginDrawable()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
//super.draw(canvas)
|
||||||
|
|
||||||
|
if (badgeText == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
with(dslGravity) {
|
||||||
|
gravity = if (isViewRtl) {
|
||||||
|
when (badgeGravity) {
|
||||||
|
Gravity.RIGHT -> {
|
||||||
|
Gravity.LEFT
|
||||||
|
}
|
||||||
|
Gravity.LEFT -> {
|
||||||
|
Gravity.RIGHT
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
badgeGravity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
badgeGravity
|
||||||
|
}
|
||||||
|
|
||||||
|
setGravityBounds(bounds)
|
||||||
|
|
||||||
|
if (isCircle) {
|
||||||
|
gravityOffsetX = badgeCircleOffsetX
|
||||||
|
gravityOffsetY = badgeCircleOffsetY
|
||||||
|
} else {
|
||||||
|
gravityOffsetX = badgeOffsetX
|
||||||
|
gravityOffsetY = badgeOffsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
val textWidth = textPaint.textWidth(badgeText)
|
||||||
|
val textHeight = textPaint.textHeight()
|
||||||
|
|
||||||
|
val drawHeight = if (isCircle) {
|
||||||
|
badgeCircleRadius.toFloat()
|
||||||
|
} else {
|
||||||
|
val height = textHeight + badgePaddingTop + badgePaddingBottom
|
||||||
|
if (badgeMinHeight > 0) {
|
||||||
|
max(height, badgeMinHeight.toFloat())
|
||||||
|
} else {
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val drawWidth = if (isCircle) {
|
||||||
|
badgeCircleRadius.toFloat()
|
||||||
|
} else {
|
||||||
|
val width = textWidth + badgePaddingLeft + badgePaddingRight
|
||||||
|
if (badgeMinWidth == -1) {
|
||||||
|
max(width, drawHeight)
|
||||||
|
} else if (badgeMinWidth > 0) {
|
||||||
|
max(width, badgeMinWidth.toFloat())
|
||||||
|
} else {
|
||||||
|
width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyGravity(drawWidth, drawHeight) { centerX, centerY ->
|
||||||
|
|
||||||
|
if (isCircle) {
|
||||||
|
textPaint.color = gradientSolidColor
|
||||||
|
|
||||||
|
//圆心计算
|
||||||
|
val cx: Float
|
||||||
|
val cy: Float
|
||||||
|
if (gravity.isCenter()) {
|
||||||
|
cx = centerX.toFloat()
|
||||||
|
cy = centerY.toFloat()
|
||||||
|
} else {
|
||||||
|
cx = centerX.toFloat() + _gravityOffsetX
|
||||||
|
cy = centerY.toFloat() + _gravityOffsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
//绘制圆
|
||||||
|
textPaint.color = gradientSolidColor
|
||||||
|
canvas.drawCircle(
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
badgeCircleRadius.toFloat(),
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
//圆的描边
|
||||||
|
if (gradientStrokeWidth > 0 && gradientStrokeColor != Color.TRANSPARENT) {
|
||||||
|
val oldWidth = textPaint.strokeWidth
|
||||||
|
val oldStyle = textPaint.style
|
||||||
|
|
||||||
|
textPaint.color = gradientStrokeColor
|
||||||
|
textPaint.strokeWidth = gradientStrokeWidth.toFloat()
|
||||||
|
textPaint.style = Paint.Style.STROKE
|
||||||
|
|
||||||
|
canvas.drawCircle(
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
badgeCircleRadius.toFloat(),
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
textPaint.strokeWidth = oldWidth
|
||||||
|
textPaint.style = oldStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
textPaint.color = badgeTextColor
|
||||||
|
|
||||||
|
val textDrawX: Float = centerX - textWidth / 2
|
||||||
|
val textDrawY: Float = centerY + textHeight / 2
|
||||||
|
|
||||||
|
val bgLeft = _gravityLeft
|
||||||
|
val bgTop = _gravityTop
|
||||||
|
|
||||||
|
//绘制背景
|
||||||
|
if (badgeAutoCircle && badgeText?.length == 1) {
|
||||||
|
if (gradientSolidColor != Color.TRANSPARENT) {
|
||||||
|
textPaint.color = gradientSolidColor
|
||||||
|
canvas.drawCircle(
|
||||||
|
centerX.toFloat(),
|
||||||
|
centerY.toFloat(),
|
||||||
|
max(maxWidth, maxHeight).toFloat() / 2,
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
originDrawable?.apply {
|
||||||
|
setBounds(
|
||||||
|
bgLeft, bgTop,
|
||||||
|
(bgLeft + drawWidth).toInt(),
|
||||||
|
(bgTop + drawHeight).toInt()
|
||||||
|
)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//绘制文本
|
||||||
|
textPaint.color = badgeTextColor
|
||||||
|
canvas.drawText(
|
||||||
|
badgeText!!,
|
||||||
|
textDrawX + badgeTextOffsetX,
|
||||||
|
textDrawY - textPaint.descent() + badgeTextOffsetY,
|
||||||
|
textPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntrinsicWidth(): Int {
|
||||||
|
val width = if (isCircle) {
|
||||||
|
badgeCircleRadius * 2
|
||||||
|
} else if (badgeAutoCircle && badgeText?.length == 1) {
|
||||||
|
max(maxWidth, maxHeight)
|
||||||
|
} else {
|
||||||
|
maxWidth
|
||||||
|
}
|
||||||
|
return max(badgeMinWidth, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIntrinsicHeight(): Int {
|
||||||
|
val height = if (isCircle) {
|
||||||
|
badgeCircleRadius * 2
|
||||||
|
} else if (badgeAutoCircle && badgeText?.length == 1) {
|
||||||
|
max(maxWidth, maxHeight)
|
||||||
|
} else {
|
||||||
|
maxHeight
|
||||||
|
}
|
||||||
|
return max(badgeMinHeight, height)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,306 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.ColorFilter
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.IntDef
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用来构建GradientDrawable
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/11/27
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
open class DslGradientDrawable : AbsDslDrawable() {
|
||||||
|
|
||||||
|
/**形状*/
|
||||||
|
@Shape
|
||||||
|
var gradientShape = GradientDrawable.RECTANGLE
|
||||||
|
|
||||||
|
/**填充的颜色*/
|
||||||
|
var gradientSolidColor = Color.TRANSPARENT
|
||||||
|
|
||||||
|
/**边框的颜色*/
|
||||||
|
var gradientStrokeColor = Color.TRANSPARENT
|
||||||
|
|
||||||
|
/**边框的宽度*/
|
||||||
|
var gradientStrokeWidth = 0
|
||||||
|
|
||||||
|
/**蚂蚁线的宽度*/
|
||||||
|
var gradientDashWidth = 0f
|
||||||
|
|
||||||
|
/**蚂蚁线之间的间距*/
|
||||||
|
var gradientDashGap = 0f
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 四个角, 8个设置点的圆角信息
|
||||||
|
* 从 左上y轴->左上x轴->右上x轴->右上y轴..., 开始设置.
|
||||||
|
*/
|
||||||
|
var gradientRadii = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
|
||||||
|
|
||||||
|
/**颜色渐变*/
|
||||||
|
var gradientColors: IntArray? = null
|
||||||
|
var gradientColorsOffsets: FloatArray? = null
|
||||||
|
|
||||||
|
/**渐变中心点坐标*/
|
||||||
|
var gradientCenterX = 0.5f
|
||||||
|
var gradientCenterY = 0.5f
|
||||||
|
|
||||||
|
/**渐变半径, 非比例值, 是px值. [GradientDrawable.RADIAL_GRADIENT]类型才有效*/
|
||||||
|
var gradientRadius = 0.5f
|
||||||
|
|
||||||
|
/** 渐变方向, 默认从左到右 */
|
||||||
|
var gradientOrientation = GradientDrawable.Orientation.LEFT_RIGHT
|
||||||
|
|
||||||
|
/** 渐变类型 */
|
||||||
|
@GradientType
|
||||||
|
var gradientType = GradientDrawable.LINEAR_GRADIENT
|
||||||
|
|
||||||
|
/**真正绘制的[Drawable]*/
|
||||||
|
var originDrawable: Drawable? = null
|
||||||
|
|
||||||
|
/**宽度补偿*/
|
||||||
|
var gradientWidthOffset: Int = 0
|
||||||
|
|
||||||
|
/**高度补偿*/
|
||||||
|
var gradientHeightOffset: Int = 0
|
||||||
|
|
||||||
|
/**当前的配置, 是否能生成有效的[GradientDrawable]*/
|
||||||
|
open fun isValidConfig(): Boolean {
|
||||||
|
return gradientSolidColor != Color.TRANSPARENT ||
|
||||||
|
gradientStrokeColor != Color.TRANSPARENT ||
|
||||||
|
gradientColors != null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun _fillRadii(array: FloatArray, radii: String?) {
|
||||||
|
if (radii.isNullOrEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val split = radii.split(",")
|
||||||
|
if (split.size != 8) {
|
||||||
|
throw IllegalArgumentException("radii 需要8个值.")
|
||||||
|
} else {
|
||||||
|
val dp = Resources.getSystem().displayMetrics.density
|
||||||
|
for (i in split.indices) {
|
||||||
|
array[i] = split[i].toFloat() * dp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fillRadii(radius: Float) {
|
||||||
|
Arrays.fill(gradientRadii, radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fillRadii(radius: Int) {
|
||||||
|
_fillRadii(gradientRadii, radius.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun _fillRadii(array: FloatArray, radius: Float) {
|
||||||
|
Arrays.fill(array, radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun _fillRadii(array: FloatArray, radius: Int) {
|
||||||
|
_fillRadii(array, radius.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun _fillColor(colors: String?): IntArray? {
|
||||||
|
if (colors.isNullOrEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val split = colors.split(",")
|
||||||
|
|
||||||
|
return IntArray(split.size) {
|
||||||
|
val str = split[it]
|
||||||
|
if (str.startsWith("#")) {
|
||||||
|
Color.parseColor(str)
|
||||||
|
} else {
|
||||||
|
str.toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**构建或者更新[originDrawable]*/
|
||||||
|
open fun updateOriginDrawable(): GradientDrawable? {
|
||||||
|
val drawable: GradientDrawable? = when (originDrawable) {
|
||||||
|
null -> GradientDrawable()
|
||||||
|
is GradientDrawable -> originDrawable as GradientDrawable
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawable?.apply {
|
||||||
|
bounds = this@DslGradientDrawable.bounds
|
||||||
|
|
||||||
|
shape = gradientShape
|
||||||
|
setStroke(
|
||||||
|
gradientStrokeWidth,
|
||||||
|
gradientStrokeColor,
|
||||||
|
gradientDashWidth,
|
||||||
|
gradientDashGap
|
||||||
|
)
|
||||||
|
setColor(gradientSolidColor)
|
||||||
|
cornerRadii = gradientRadii
|
||||||
|
|
||||||
|
if (gradientColors != null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
setGradientCenter(
|
||||||
|
this@DslGradientDrawable.gradientCenterX,
|
||||||
|
this@DslGradientDrawable.gradientCenterY
|
||||||
|
)
|
||||||
|
}
|
||||||
|
gradientRadius = this@DslGradientDrawable.gradientRadius
|
||||||
|
gradientType = this@DslGradientDrawable.gradientType
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
orientation = gradientOrientation
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
setColors(gradientColors, gradientColorsOffsets)
|
||||||
|
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
colors = gradientColors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
originDrawable = this
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
return drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun configDrawable(config: DslGradientDrawable.() -> Unit): DslGradientDrawable {
|
||||||
|
this.config()
|
||||||
|
updateOriginDrawable()
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
super.draw(canvas)
|
||||||
|
originDrawable?.apply {
|
||||||
|
setBounds(
|
||||||
|
this@DslGradientDrawable.bounds.left - gradientWidthOffset / 2,
|
||||||
|
this@DslGradientDrawable.bounds.top - gradientHeightOffset / 2,
|
||||||
|
this@DslGradientDrawable.bounds.right + gradientWidthOffset / 2,
|
||||||
|
this@DslGradientDrawable.bounds.bottom + gradientHeightOffset / 2
|
||||||
|
)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//<editor-fold desc="圆角相关配置">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4个角, 8个点 圆角配置
|
||||||
|
*/
|
||||||
|
fun cornerRadii(radii: FloatArray) {
|
||||||
|
gradientRadii = radii
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cornerRadius(radii: Float) {
|
||||||
|
Arrays.fill(gradientRadii, radii)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cornerRadius(
|
||||||
|
leftTop: Float = 0f,
|
||||||
|
rightTop: Float = 0f,
|
||||||
|
rightBottom: Float = 0f,
|
||||||
|
leftBottom: Float = 0f
|
||||||
|
) {
|
||||||
|
gradientRadii[0] = leftTop
|
||||||
|
gradientRadii[1] = leftTop
|
||||||
|
gradientRadii[2] = rightTop
|
||||||
|
gradientRadii[3] = rightTop
|
||||||
|
gradientRadii[4] = rightBottom
|
||||||
|
gradientRadii[5] = rightBottom
|
||||||
|
gradientRadii[6] = leftBottom
|
||||||
|
gradientRadii[7] = leftBottom
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只配置左边的圆角
|
||||||
|
*/
|
||||||
|
fun cornerRadiiLeft(radii: Float) {
|
||||||
|
gradientRadii[0] = radii
|
||||||
|
gradientRadii[1] = radii
|
||||||
|
gradientRadii[6] = radii
|
||||||
|
gradientRadii[7] = radii
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cornerRadiiRight(radii: Float) {
|
||||||
|
gradientRadii[2] = radii
|
||||||
|
gradientRadii[3] = radii
|
||||||
|
gradientRadii[4] = radii
|
||||||
|
gradientRadii[5] = radii
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cornerRadiiTop(radii: Float) {
|
||||||
|
gradientRadii[0] = radii
|
||||||
|
gradientRadii[1] = radii
|
||||||
|
gradientRadii[2] = radii
|
||||||
|
gradientRadii[3] = radii
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cornerRadiiBottom(radii: Float) {
|
||||||
|
gradientRadii[4] = radii
|
||||||
|
gradientRadii[5] = radii
|
||||||
|
gradientRadii[6] = radii
|
||||||
|
gradientRadii[7] = radii
|
||||||
|
}
|
||||||
|
|
||||||
|
//</editor-fold desc="圆角相关配置">
|
||||||
|
|
||||||
|
//<editor-fold desc="传递属性">
|
||||||
|
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||||
|
super.setColorFilter(colorFilter)
|
||||||
|
originDrawable?.colorFilter = colorFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setTintList(tint: ColorStateList?) {
|
||||||
|
super.setTintList(tint)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
originDrawable?.setTintList(tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setState(stateSet: IntArray): Boolean {
|
||||||
|
return originDrawable?.setState(stateSet) ?: super.setState(stateSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getState(): IntArray {
|
||||||
|
return originDrawable?.state ?: super.getState()
|
||||||
|
}
|
||||||
|
|
||||||
|
//</editor-fold desc="传递属性">
|
||||||
|
}
|
||||||
|
|
||||||
|
@IntDef(
|
||||||
|
GradientDrawable.RECTANGLE,
|
||||||
|
GradientDrawable.OVAL,
|
||||||
|
GradientDrawable.LINE,
|
||||||
|
GradientDrawable.RING
|
||||||
|
)
|
||||||
|
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class Shape
|
||||||
|
|
||||||
|
@IntDef(
|
||||||
|
GradientDrawable.LINEAR_GRADIENT,
|
||||||
|
GradientDrawable.RADIAL_GRADIENT,
|
||||||
|
GradientDrawable.SWEEP_GRADIENT
|
||||||
|
)
|
||||||
|
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class GradientType
|
||||||
|
|
||||||
|
/**快速创建[GradientDrawable]*/
|
||||||
|
fun dslGradientDrawable(action: DslGradientDrawable.() -> Unit): GradientDrawable {
|
||||||
|
return DslGradientDrawable().run {
|
||||||
|
action()
|
||||||
|
updateOriginDrawable()!!
|
||||||
|
}
|
||||||
|
}
|
215
TabLayout/src/main/java/com/angcyo/tablayout/DslGravity.kt
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.view.Gravity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [android.view.Gravity] 辅助计算类
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/12/13
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
class DslGravity {
|
||||||
|
|
||||||
|
/**束缚范围*/
|
||||||
|
val gravityBounds: RectF = RectF()
|
||||||
|
|
||||||
|
/**束缚重力*/
|
||||||
|
var gravity: Int = Gravity.LEFT or Gravity.TOP
|
||||||
|
|
||||||
|
/**使用中心坐标作为定位参考, 否则就是四条边*/
|
||||||
|
var gravityRelativeCenter: Boolean = true
|
||||||
|
|
||||||
|
/**额外偏移距离, 会根据[Gravity]自动取负值*/
|
||||||
|
var gravityOffsetX: Int = 0
|
||||||
|
var gravityOffsetY: Int = 0
|
||||||
|
|
||||||
|
fun setGravityBounds(rectF: RectF) {
|
||||||
|
gravityBounds.set(rectF)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGravityBounds(rect: Rect) {
|
||||||
|
gravityBounds.set(rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGravityBounds(
|
||||||
|
left: Int,
|
||||||
|
top: Int,
|
||||||
|
right: Int,
|
||||||
|
bottom: Int
|
||||||
|
) {
|
||||||
|
gravityBounds.set(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGravityBounds(
|
||||||
|
left: Float,
|
||||||
|
top: Float,
|
||||||
|
right: Float,
|
||||||
|
bottom: Float
|
||||||
|
) {
|
||||||
|
gravityBounds.set(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
//计算后的属性
|
||||||
|
var _horizontalGravity: Int = Gravity.LEFT
|
||||||
|
var _verticalGravity: Int = Gravity.TOP
|
||||||
|
var _isCenterGravity: Boolean = false
|
||||||
|
var _targetWidth = 0f
|
||||||
|
var _targetHeight = 0f
|
||||||
|
var _gravityLeft = 0
|
||||||
|
var _gravityTop = 0
|
||||||
|
var _gravityRight = 0
|
||||||
|
var _gravityBottom = 0
|
||||||
|
|
||||||
|
//根据gravity调整后的offset
|
||||||
|
var _gravityOffsetX = 0
|
||||||
|
var _gravityOffsetY = 0
|
||||||
|
|
||||||
|
/**根据[gravity]返回在[gravityBounds]中的[left] [top]位置*/
|
||||||
|
fun applyGravity(
|
||||||
|
width: Float = _targetWidth,
|
||||||
|
height: Float = _targetHeight,
|
||||||
|
callback: (centerX: Int, centerY: Int) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
_targetWidth = width
|
||||||
|
_targetHeight = height
|
||||||
|
|
||||||
|
val layoutDirection = 0
|
||||||
|
val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)
|
||||||
|
val verticalGravity = gravity and Gravity.VERTICAL_GRAVITY_MASK
|
||||||
|
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||||
|
|
||||||
|
//调整offset
|
||||||
|
_gravityOffsetX = when (horizontalGravity) {
|
||||||
|
Gravity.RIGHT -> -gravityOffsetX
|
||||||
|
Gravity.END -> -gravityOffsetX
|
||||||
|
else -> gravityOffsetX
|
||||||
|
}
|
||||||
|
_gravityOffsetY = when (verticalGravity) {
|
||||||
|
Gravity.BOTTOM -> -gravityOffsetY
|
||||||
|
else -> gravityOffsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
//计算居中的坐标
|
||||||
|
val centerX = when (horizontalGravity) {
|
||||||
|
Gravity.CENTER_HORIZONTAL -> (gravityBounds.left + gravityBounds.width() / 2 + _gravityOffsetX).toInt()
|
||||||
|
Gravity.RIGHT -> (gravityBounds.right + _gravityOffsetX - if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
|
||||||
|
Gravity.END -> (gravityBounds.right + _gravityOffsetX - if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
|
||||||
|
else -> (gravityBounds.left + _gravityOffsetX + if (gravityRelativeCenter) 0f else _targetWidth / 2).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val centerY = when (verticalGravity) {
|
||||||
|
Gravity.CENTER_VERTICAL -> (gravityBounds.top + gravityBounds.height() / 2 + _gravityOffsetY).toInt()
|
||||||
|
Gravity.BOTTOM -> (gravityBounds.bottom + _gravityOffsetY - if (gravityRelativeCenter) 0f else _targetHeight / 2).toInt()
|
||||||
|
else -> (gravityBounds.top + _gravityOffsetY + if (gravityRelativeCenter) 0f else _targetHeight / 2).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
//缓存
|
||||||
|
_horizontalGravity = horizontalGravity
|
||||||
|
_verticalGravity = verticalGravity
|
||||||
|
_isCenterGravity = horizontalGravity == Gravity.CENTER_HORIZONTAL &&
|
||||||
|
verticalGravity == Gravity.CENTER_VERTICAL
|
||||||
|
|
||||||
|
_gravityLeft = (centerX - _targetWidth / 2).toInt()
|
||||||
|
_gravityRight = (centerX + _targetWidth / 2).toInt()
|
||||||
|
_gravityTop = (centerY - _targetHeight / 2).toInt()
|
||||||
|
_gravityBottom = (centerY + _targetHeight / 2).toInt()
|
||||||
|
|
||||||
|
//回调
|
||||||
|
callback(centerX, centerY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认计算出的是目标中心点坐标参考距离
|
||||||
|
* [com.angcyo.drawable.DslGravity.getGravityRelativeCenter]
|
||||||
|
* */
|
||||||
|
fun dslGravity(
|
||||||
|
rect: RectF, //计算的矩形
|
||||||
|
gravity: Int, //重力
|
||||||
|
width: Float, //放置目标的宽度
|
||||||
|
height: Float, //放置目标的高度
|
||||||
|
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
|
||||||
|
offsetY: Int = 0,
|
||||||
|
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
|
||||||
|
): DslGravity {
|
||||||
|
val _dslGravity = DslGravity()
|
||||||
|
_dslGravity.setGravityBounds(rect)
|
||||||
|
_config(_dslGravity, gravity, width, height, offsetX, offsetY, callback)
|
||||||
|
return _dslGravity
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认计算出的是目标中心点坐标参考距离
|
||||||
|
* [com.angcyo.drawable.DslGravity.getGravityRelativeCenter]
|
||||||
|
* */
|
||||||
|
fun dslGravity(
|
||||||
|
rect: Rect, //计算的矩形
|
||||||
|
gravity: Int, //重力
|
||||||
|
width: Float, //放置目标的宽度
|
||||||
|
height: Float, //放置目标的高度
|
||||||
|
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
|
||||||
|
offsetY: Int = 0,
|
||||||
|
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
|
||||||
|
): DslGravity {
|
||||||
|
val _dslGravity = DslGravity()
|
||||||
|
_dslGravity.setGravityBounds(rect)
|
||||||
|
_config(_dslGravity, gravity, width, height, offsetX, offsetY, callback)
|
||||||
|
return _dslGravity
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun _config(
|
||||||
|
_dslGravity: DslGravity,
|
||||||
|
gravity: Int, //重力
|
||||||
|
width: Float, //放置目标的宽度
|
||||||
|
height: Float, //放置目标的高度
|
||||||
|
offsetX: Int = 0, //额外的偏移,会根据[gravity]进行左右|上下取反
|
||||||
|
offsetY: Int = 0,
|
||||||
|
callback: (dslGravity: DslGravity, centerX: Int, centerY: Int) -> Unit
|
||||||
|
) {
|
||||||
|
_dslGravity.gravity = gravity
|
||||||
|
_dslGravity.gravityOffsetX = offsetX
|
||||||
|
_dslGravity.gravityOffsetY = offsetY
|
||||||
|
_dslGravity.applyGravity(width, height) { centerX, centerY ->
|
||||||
|
callback(_dslGravity, centerX, centerY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**居中*/
|
||||||
|
fun Int.isCenter(): Boolean {
|
||||||
|
val layoutDirection = 0
|
||||||
|
val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection)
|
||||||
|
val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK
|
||||||
|
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||||
|
|
||||||
|
return verticalGravity == Gravity.CENTER_VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.isLeft(): Boolean {
|
||||||
|
val layoutDirection = 0
|
||||||
|
val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection)
|
||||||
|
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||||
|
|
||||||
|
return horizontalGravity == Gravity.LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.isRight(): Boolean {
|
||||||
|
val layoutDirection = 0
|
||||||
|
val absoluteGravity = Gravity.getAbsoluteGravity(this, layoutDirection)
|
||||||
|
val horizontalGravity = absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK
|
||||||
|
|
||||||
|
return horizontalGravity == Gravity.RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.isTop(): Boolean {
|
||||||
|
val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK
|
||||||
|
return verticalGravity == Gravity.TOP
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.isBottom(): Boolean {
|
||||||
|
val verticalGravity = this and Gravity.VERTICAL_GRAVITY_MASK
|
||||||
|
return verticalGravity == Gravity.BOTTOM
|
||||||
|
}
|
438
TabLayout/src/main/java/com/angcyo/tablayout/DslSelector.kt
Normal file
@ -0,0 +1,438 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用来操作[ViewGroup]中的[child], 支持单选, 多选, 拦截.
|
||||||
|
* 操作的都是可见性为[VISIBLE]的[View]
|
||||||
|
*
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/11/24
|
||||||
|
*/
|
||||||
|
|
||||||
|
open class DslSelector {
|
||||||
|
|
||||||
|
var parent: ViewGroup? = null
|
||||||
|
var dslSelectorConfig: DslSelectorConfig = DslSelectorConfig()
|
||||||
|
|
||||||
|
//可见view列表
|
||||||
|
val visibleViewList: MutableList<View> = mutableListOf()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中的索引列表
|
||||||
|
* */
|
||||||
|
val selectorIndexList: MutableList<Int> = mutableListOf()
|
||||||
|
get() {
|
||||||
|
field.clear()
|
||||||
|
visibleViewList.forEachIndexed { index, view ->
|
||||||
|
if (view.isSe()) {
|
||||||
|
field.add(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中的View列表
|
||||||
|
* */
|
||||||
|
val selectorViewList: MutableList<View> = mutableListOf()
|
||||||
|
get() {
|
||||||
|
field.clear()
|
||||||
|
visibleViewList.forEachIndexed { index, view ->
|
||||||
|
if (view.isSe() || index == dslSelectIndex) {
|
||||||
|
field.add(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
//child 点击事件处理
|
||||||
|
val _onChildClickListener = View.OnClickListener {
|
||||||
|
val index = visibleViewList.indexOf(it)
|
||||||
|
val select =
|
||||||
|
if (dslSelectorConfig.dslMultiMode || dslSelectorConfig.dslMinSelectLimit < 1) {
|
||||||
|
!it.isSe()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!interceptSelector(index, select, true)) {
|
||||||
|
selector(
|
||||||
|
visibleViewList.indexOf(it),
|
||||||
|
select,
|
||||||
|
notify = true,
|
||||||
|
fromUser = true,
|
||||||
|
forceNotify = it is CompoundButton && dslSelectorConfig.dslMultiMode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**兼容[CompoundButton]*/
|
||||||
|
val _onCheckedChangeListener = CompoundButton.OnCheckedChangeListener { buttonView, isChecked ->
|
||||||
|
buttonView.isChecked = buttonView.isSelected //恢复状态 不做任何处理, 在[OnClickListener]中处理
|
||||||
|
/*val index = visibleViewList.indexOf(buttonView)
|
||||||
|
|
||||||
|
if (interceptSelector(index, isChecked, false)) {
|
||||||
|
//拦截了此操作
|
||||||
|
buttonView.isChecked = !isChecked //恢复状态
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectorViewList = selectorViewList
|
||||||
|
val sum = selectorViewList.size
|
||||||
|
//Limit 过滤
|
||||||
|
if (buttonView.isChecked) {
|
||||||
|
if (sum > dslSelectorConfig.dslMaxSelectLimit) {
|
||||||
|
//不允许选择
|
||||||
|
buttonView.isChecked = false //恢复状态
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//取消选择, 检查是否达到了 limit
|
||||||
|
if (sum < dslSelectorConfig.dslMinSelectLimit) {
|
||||||
|
//不允许取消选择
|
||||||
|
buttonView.isChecked = true //恢复状态
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
//已经选中了控件
|
||||||
|
} else {
|
||||||
|
//已经取消了控件
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/**当前选中的索引*/
|
||||||
|
var dslSelectIndex = -1
|
||||||
|
|
||||||
|
/**安装*/
|
||||||
|
fun install(viewGroup: ViewGroup, config: DslSelectorConfig.() -> Unit = {}): DslSelector {
|
||||||
|
dslSelectIndex = -1
|
||||||
|
parent = viewGroup
|
||||||
|
updateVisibleList()
|
||||||
|
dslSelectorConfig.config()
|
||||||
|
|
||||||
|
updateStyle()
|
||||||
|
updateClickListener()
|
||||||
|
|
||||||
|
if (dslSelectIndex in 0 until visibleViewList.size) {
|
||||||
|
selector(dslSelectIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**更新样式*/
|
||||||
|
fun updateStyle() {
|
||||||
|
visibleViewList.forEachIndexed { index, view ->
|
||||||
|
val selector = dslSelectIndex == index || view.isSe()
|
||||||
|
dslSelectorConfig.onStyleItemView(view, index, selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**更新child的点击事件*/
|
||||||
|
fun updateClickListener() {
|
||||||
|
parent?.apply {
|
||||||
|
for (i in 0 until childCount) {
|
||||||
|
getChildAt(i)?.let {
|
||||||
|
it.setOnClickListener(_onChildClickListener)
|
||||||
|
if (it is CompoundButton) {
|
||||||
|
it.setOnCheckedChangeListener(_onCheckedChangeListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**更新可见视图列表*/
|
||||||
|
fun updateVisibleList(): List<View> {
|
||||||
|
visibleViewList.clear()
|
||||||
|
parent?.apply {
|
||||||
|
for (i in 0 until childCount) {
|
||||||
|
getChildAt(i)?.let {
|
||||||
|
if (it.visibility == View.VISIBLE) {
|
||||||
|
visibleViewList.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dslSelectIndex in visibleViewList.indices) {
|
||||||
|
if (!visibleViewList[dslSelectIndex].isSe()) {
|
||||||
|
visibleViewList[dslSelectIndex].setSe(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//如果当前选中的索引, 不在可见视图列表中
|
||||||
|
dslSelectIndex = -1
|
||||||
|
}
|
||||||
|
return visibleViewList
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作单个
|
||||||
|
* @param index 操作目标的索引值
|
||||||
|
* @param select 选中 or 取消选中
|
||||||
|
* @param notify 是否需要通知事件
|
||||||
|
* @param forceNotify 是否强制通知事件.child使用[CompoundButton]时, 推荐使用
|
||||||
|
* */
|
||||||
|
fun selector(
|
||||||
|
index: Int,
|
||||||
|
select: Boolean = true,
|
||||||
|
notify: Boolean = true,
|
||||||
|
fromUser: Boolean = false,
|
||||||
|
forceNotify: Boolean = false
|
||||||
|
) {
|
||||||
|
val oldSelectorList = selectorIndexList.toList()
|
||||||
|
val lastSelectorIndex: Int? = oldSelectorList.lastOrNull()
|
||||||
|
val reselect = !dslSelectorConfig.dslMultiMode &&
|
||||||
|
oldSelectorList.isNotEmpty() &&
|
||||||
|
oldSelectorList.contains(index)
|
||||||
|
|
||||||
|
var needNotify = _selector(index, select, fromUser) || forceNotify
|
||||||
|
|
||||||
|
if (!oldSelectorList.isChange(selectorIndexList)) {
|
||||||
|
//选中项, 未改变时不通知
|
||||||
|
needNotify = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needNotify || reselect) {
|
||||||
|
dslSelectIndex = selectorIndexList.lastOrNull() ?: -1
|
||||||
|
if (notify) {
|
||||||
|
notifySelectChange(lastSelectorIndex ?: -1, reselect, fromUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**选择所有
|
||||||
|
* [select] true:选择所有, false:取消所有*/
|
||||||
|
fun selectorAll(
|
||||||
|
select: Boolean = true,
|
||||||
|
notify: Boolean = true,
|
||||||
|
fromUser: Boolean = true
|
||||||
|
) {
|
||||||
|
val indexList = visibleViewList.mapIndexedTo(mutableListOf()) { index, _ ->
|
||||||
|
index
|
||||||
|
}
|
||||||
|
selector(indexList, select, notify, fromUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作多个
|
||||||
|
* @param select 选中 or 取消选中
|
||||||
|
* [selector]
|
||||||
|
* */
|
||||||
|
fun selector(
|
||||||
|
indexList: MutableList<Int>,
|
||||||
|
select: Boolean = true,
|
||||||
|
notify: Boolean = true,
|
||||||
|
fromUser: Boolean = false
|
||||||
|
) {
|
||||||
|
val oldSelectorIndexList = selectorIndexList
|
||||||
|
val lastSelectorIndex: Int? = oldSelectorIndexList.lastOrNull()
|
||||||
|
|
||||||
|
var result = false
|
||||||
|
|
||||||
|
indexList.forEach {
|
||||||
|
result = _selector(it, select, fromUser) || result
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
dslSelectIndex = selectorIndexList.lastOrNull() ?: -1
|
||||||
|
if (notify) {
|
||||||
|
notifySelectChange(lastSelectorIndex ?: -1, false, fromUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**通知事件*/
|
||||||
|
fun notifySelectChange(lastSelectorIndex: Int, reselect: Boolean, fromUser: Boolean) {
|
||||||
|
val indexSelectorList = selectorIndexList
|
||||||
|
dslSelectorConfig.onSelectViewChange(
|
||||||
|
visibleViewList.getOrNull(lastSelectorIndex),
|
||||||
|
selectorViewList,
|
||||||
|
reselect,
|
||||||
|
fromUser
|
||||||
|
)
|
||||||
|
dslSelectorConfig.onSelectIndexChange(
|
||||||
|
lastSelectorIndex,
|
||||||
|
indexSelectorList,
|
||||||
|
reselect,
|
||||||
|
fromUser
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**当前的操作是否被拦截*/
|
||||||
|
fun interceptSelector(index: Int, select: Boolean, fromUser: Boolean): Boolean {
|
||||||
|
val visibleViewList = visibleViewList
|
||||||
|
if (index !in 0 until visibleViewList.size) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return dslSelectorConfig.onSelectItemView(visibleViewList[index], index, select, fromUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**@return 是否发生过改变*/
|
||||||
|
fun _selector(index: Int, select: Boolean, fromUser: Boolean): Boolean {
|
||||||
|
val visibleViewList = visibleViewList
|
||||||
|
//超范围过滤
|
||||||
|
if (index !in 0 until visibleViewList.size) {
|
||||||
|
"index out of list.".logi()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectorIndexList = selectorIndexList
|
||||||
|
val selectorViewList = selectorViewList
|
||||||
|
|
||||||
|
if (selectorIndexList.isNotEmpty()) {
|
||||||
|
if (select) {
|
||||||
|
//需要选中某项
|
||||||
|
|
||||||
|
if (dslSelectorConfig.dslMultiMode) {
|
||||||
|
//多选模式
|
||||||
|
if (selectorIndexList.contains(index)) {
|
||||||
|
//已经选中
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//单选模式
|
||||||
|
|
||||||
|
//取消之前选中
|
||||||
|
selectorIndexList.forEach {
|
||||||
|
if (it != index) {
|
||||||
|
visibleViewList[it].setSe(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectorIndexList.contains(index)) {
|
||||||
|
//已经选中
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//需要取消选中
|
||||||
|
if (!selectorIndexList.contains(index)) {
|
||||||
|
//目标已经是未选中
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Limit 过滤
|
||||||
|
if (select) {
|
||||||
|
val sum = selectorViewList.size + 1
|
||||||
|
if (sum > dslSelectorConfig.dslMaxSelectLimit) {
|
||||||
|
//不允许选择
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//取消选择, 检查是否达到了 limit
|
||||||
|
val sum = selectorViewList.size - 1
|
||||||
|
if (sum < dslSelectorConfig.dslMinSelectLimit) {
|
||||||
|
//不允许取消选择
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectorView = visibleViewList[index]
|
||||||
|
|
||||||
|
//更新选中样式
|
||||||
|
selectorView.setSe(select)
|
||||||
|
|
||||||
|
if (dslSelectorConfig.dslMultiMode) {
|
||||||
|
//多选
|
||||||
|
} else {
|
||||||
|
//单选
|
||||||
|
|
||||||
|
//取消之前
|
||||||
|
selectorViewList.forEach { view ->
|
||||||
|
//更新样式
|
||||||
|
val indexOf = visibleViewList.indexOf(view)
|
||||||
|
if (indexOf != index &&
|
||||||
|
!dslSelectorConfig.onSelectItemView(view, indexOf, false, fromUser)
|
||||||
|
) {
|
||||||
|
view.setSe(false)
|
||||||
|
dslSelectorConfig.onStyleItemView(view, indexOf, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dslSelectorConfig.onStyleItemView(selectorView, index, select)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**是否选中状态*/
|
||||||
|
fun View.isSe(): Boolean {
|
||||||
|
return isSelected || if (this is CompoundButton) isChecked else false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setSe(se: Boolean) {
|
||||||
|
isSelected = se
|
||||||
|
if (this is CompoundButton) isChecked = se
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dsl配置项
|
||||||
|
* */
|
||||||
|
open class DslSelectorConfig {
|
||||||
|
|
||||||
|
/**取消选择时, 最小需要保持多个选中. 可以决定单选时, 是否允许取消所有选中*/
|
||||||
|
var dslMinSelectLimit = 1
|
||||||
|
|
||||||
|
/**多选时, 最大允许多个选中*/
|
||||||
|
var dslMaxSelectLimit = Int.MAX_VALUE
|
||||||
|
|
||||||
|
/**是否是多选模式*/
|
||||||
|
var dslMultiMode: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用来初始化[itemView]的样式
|
||||||
|
* [onSelectItemView]
|
||||||
|
* */
|
||||||
|
var onStyleItemView: (itemView: View, index: Int, select: Boolean) -> Unit =
|
||||||
|
{ _, _, _ ->
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中[View]改变回调, 优先于[onSelectIndexChange]触发, 区别在于参数类型不一样
|
||||||
|
* @param fromView 单选模式下有效, 表示之前选中的[View]
|
||||||
|
* @param reselect 是否是重复选择, 只在单选模式下有效
|
||||||
|
* @param fromUser 是否是用户产生的回调, 而非代码设置
|
||||||
|
* */
|
||||||
|
var onSelectViewChange: (fromView: View?, selectViewList: List<View>, reselect: Boolean, fromUser: Boolean) -> Unit =
|
||||||
|
{ _, _, _, _ ->
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中改变回调
|
||||||
|
* [onSelectViewChange]
|
||||||
|
* @param fromIndex 单选模式下有效, 表示之前选中的[View], 在可见性[child]列表中的索引
|
||||||
|
* */
|
||||||
|
var onSelectIndexChange: (fromIndex: Int, selectIndexList: List<Int>, reselect: Boolean, fromUser: Boolean) -> Unit =
|
||||||
|
{ fromIndex, selectList, reselect, fromUser ->
|
||||||
|
"选择:[$fromIndex]->${selectList} reselect:$reselect fromUser:$fromUser".logi()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当需要选中[itemView]时回调, 返回[true]表示拦截默认处理
|
||||||
|
* @param itemView 操作的[View]
|
||||||
|
* @param index [itemView]在可见性view列表中的索引. 非ViewGroup中的索引
|
||||||
|
* @param select 选中 or 取消选中
|
||||||
|
* @return true 表示拦截默认处理
|
||||||
|
* */
|
||||||
|
var onSelectItemView: (itemView: View, index: Int, select: Boolean, fromUser: Boolean) -> Boolean =
|
||||||
|
{ _, _, _, _ ->
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**[DslSelector]组件*/
|
||||||
|
fun dslSelector(viewGroup: ViewGroup, config: DslSelectorConfig.() -> Unit = {}): DslSelector {
|
||||||
|
return DslSelector().apply {
|
||||||
|
install(viewGroup, config)
|
||||||
|
}
|
||||||
|
}
|
222
TabLayout/src/main/java/com/angcyo/tablayout/DslTabBadge.kt
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
|
import androidx.annotation.Px
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 角标
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/12/13
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
open class DslTabBadge : DslBadgeDrawable() {
|
||||||
|
|
||||||
|
/**角标默认配置项*/
|
||||||
|
val defaultBadgeConfig = TabBadgeConfig()
|
||||||
|
|
||||||
|
/**预览的角标属性*/
|
||||||
|
var xmlBadgeText: String? = null
|
||||||
|
|
||||||
|
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||||
|
val typedArray =
|
||||||
|
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||||
|
gradientSolidColor =
|
||||||
|
typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_solid_color,
|
||||||
|
defaultBadgeConfig.badgeSolidColor
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeSolidColor = gradientSolidColor
|
||||||
|
|
||||||
|
badgeTextColor =
|
||||||
|
typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_text_color,
|
||||||
|
defaultBadgeConfig.badgeTextColor
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeTextColor = badgeTextColor
|
||||||
|
|
||||||
|
gradientStrokeColor =
|
||||||
|
typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_stroke_color,
|
||||||
|
defaultBadgeConfig.badgeStrokeColor
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeStrokeColor = gradientStrokeColor
|
||||||
|
|
||||||
|
gradientStrokeWidth =
|
||||||
|
typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_stroke_width,
|
||||||
|
defaultBadgeConfig.badgeStrokeWidth
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeStrokeWidth = gradientStrokeWidth
|
||||||
|
|
||||||
|
badgeGravity = typedArray.getInt(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_gravity,
|
||||||
|
defaultBadgeConfig.badgeGravity
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeGravity = badgeGravity
|
||||||
|
|
||||||
|
badgeOffsetX = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_offset_x,
|
||||||
|
defaultBadgeConfig.badgeOffsetX
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeOffsetX = badgeOffsetX
|
||||||
|
badgeOffsetY = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_offset_y,
|
||||||
|
defaultBadgeConfig.badgeOffsetY
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeOffsetY = badgeOffsetY
|
||||||
|
|
||||||
|
badgeCircleOffsetX = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_circle_offset_x,
|
||||||
|
defaultBadgeConfig.badgeOffsetX
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeCircleOffsetX = badgeCircleOffsetX
|
||||||
|
badgeCircleOffsetY = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_circle_offset_y,
|
||||||
|
defaultBadgeConfig.badgeOffsetY
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeCircleOffsetY = badgeCircleOffsetY
|
||||||
|
|
||||||
|
badgeCircleRadius = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_circle_radius,
|
||||||
|
defaultBadgeConfig.badgeCircleRadius
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeCircleRadius = badgeCircleRadius
|
||||||
|
|
||||||
|
val badgeRadius = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_radius,
|
||||||
|
defaultBadgeConfig.badgeRadius
|
||||||
|
)
|
||||||
|
cornerRadius(badgeRadius.toFloat())
|
||||||
|
defaultBadgeConfig.badgeRadius = badgeRadius
|
||||||
|
|
||||||
|
badgePaddingLeft = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_padding_left,
|
||||||
|
defaultBadgeConfig.badgePaddingLeft
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgePaddingLeft = badgePaddingLeft
|
||||||
|
|
||||||
|
badgePaddingRight = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_padding_right,
|
||||||
|
defaultBadgeConfig.badgePaddingRight
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgePaddingRight = badgePaddingRight
|
||||||
|
|
||||||
|
badgePaddingTop = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_padding_top,
|
||||||
|
defaultBadgeConfig.badgePaddingTop
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgePaddingTop = badgePaddingTop
|
||||||
|
|
||||||
|
badgePaddingBottom = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_padding_bottom,
|
||||||
|
defaultBadgeConfig.badgePaddingBottom
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgePaddingBottom = badgePaddingBottom
|
||||||
|
|
||||||
|
xmlBadgeText = typedArray.getString(R.styleable.DslTabLayout_tab_badge_text)
|
||||||
|
|
||||||
|
badgeTextSize = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_text_size,
|
||||||
|
defaultBadgeConfig.badgeTextSize.toInt()
|
||||||
|
).toFloat()
|
||||||
|
defaultBadgeConfig.badgeTextSize = badgeTextSize
|
||||||
|
|
||||||
|
defaultBadgeConfig.badgeAnchorChildIndex =
|
||||||
|
typedArray.getInteger(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_anchor_child_index,
|
||||||
|
defaultBadgeConfig.badgeAnchorChildIndex
|
||||||
|
)
|
||||||
|
defaultBadgeConfig.badgeIgnoreChildPadding = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_ignore_child_padding,
|
||||||
|
defaultBadgeConfig.badgeIgnoreChildPadding
|
||||||
|
)
|
||||||
|
|
||||||
|
defaultBadgeConfig.badgeMinWidth = typedArray.getLayoutDimension(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_min_width,
|
||||||
|
defaultBadgeConfig.badgeMinWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
defaultBadgeConfig.badgeMinHeight = typedArray.getLayoutDimension(
|
||||||
|
R.styleable.DslTabLayout_tab_badge_min_height,
|
||||||
|
defaultBadgeConfig.badgeMinHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
typedArray.recycle()
|
||||||
|
super.initAttribute(context, attributeSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**使用指定配置, 更新[DslBadgeDrawable]*/
|
||||||
|
fun updateBadgeConfig(badgeConfig: TabBadgeConfig) {
|
||||||
|
gradientSolidColor = badgeConfig.badgeSolidColor
|
||||||
|
gradientStrokeColor = badgeConfig.badgeStrokeColor
|
||||||
|
gradientStrokeWidth = badgeConfig.badgeStrokeWidth
|
||||||
|
badgeTextColor = badgeConfig.badgeTextColor
|
||||||
|
badgeGravity = badgeConfig.badgeGravity
|
||||||
|
badgeOffsetX = badgeConfig.badgeOffsetX
|
||||||
|
badgeOffsetY = badgeConfig.badgeOffsetY
|
||||||
|
badgeCircleOffsetX = badgeConfig.badgeCircleOffsetX
|
||||||
|
badgeCircleOffsetY = badgeConfig.badgeCircleOffsetY
|
||||||
|
badgeCircleRadius = badgeConfig.badgeCircleRadius
|
||||||
|
badgePaddingLeft = badgeConfig.badgePaddingLeft
|
||||||
|
badgePaddingRight = badgeConfig.badgePaddingRight
|
||||||
|
badgePaddingTop = badgeConfig.badgePaddingTop
|
||||||
|
badgePaddingBottom = badgeConfig.badgePaddingBottom
|
||||||
|
badgeTextSize = badgeConfig.badgeTextSize
|
||||||
|
cornerRadius(badgeConfig.badgeRadius.toFloat())
|
||||||
|
badgeMinHeight = badgeConfig.badgeMinHeight
|
||||||
|
badgeMinWidth = badgeConfig.badgeMinWidth
|
||||||
|
badgeText = badgeConfig.badgeText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**角标绘制参数配置*/
|
||||||
|
data class TabBadgeConfig(
|
||||||
|
/**角标的文本(默认居中绘制文本,暂不支持修改), 空字符串会绘制成小圆点
|
||||||
|
* null 不绘制角标
|
||||||
|
* "" 空字符绘制圆点
|
||||||
|
* 其他 正常绘制
|
||||||
|
* */
|
||||||
|
var badgeText: String? = null,
|
||||||
|
/**重力*/
|
||||||
|
var badgeGravity: Int = Gravity.CENTER,
|
||||||
|
/**角标背景颜色*/
|
||||||
|
var badgeSolidColor: Int = Color.RED,
|
||||||
|
/**角标边框颜色*/
|
||||||
|
var badgeStrokeColor: Int = Color.TRANSPARENT,
|
||||||
|
/**角标边框宽度*/
|
||||||
|
var badgeStrokeWidth: Int = 0,
|
||||||
|
|
||||||
|
/**角标文本颜色*/
|
||||||
|
var badgeTextColor: Int = Color.WHITE,
|
||||||
|
/**角标文本字体大小*/
|
||||||
|
@Px
|
||||||
|
var badgeTextSize: Float = 12 * dp,
|
||||||
|
/**圆点状态时的半径大小*/
|
||||||
|
var badgeCircleRadius: Int = 4 * dpi,
|
||||||
|
/**圆角大小*/
|
||||||
|
var badgeRadius: Int = 10 * dpi,
|
||||||
|
/**额外偏移距离, 会根据[Gravity]自动取负值*/
|
||||||
|
var badgeOffsetX: Int = 0,
|
||||||
|
var badgeOffsetY: Int = 0,
|
||||||
|
var badgeCircleOffsetX: Int = 0,
|
||||||
|
var badgeCircleOffsetY: Int = 0,
|
||||||
|
/**圆点状态时无效*/
|
||||||
|
var badgePaddingLeft: Int = 4 * dpi,
|
||||||
|
var badgePaddingRight: Int = 4 * dpi,
|
||||||
|
var badgePaddingTop: Int = 0,
|
||||||
|
var badgePaddingBottom: Int = 0,
|
||||||
|
|
||||||
|
var badgeAnchorChildIndex: Int = -1,
|
||||||
|
var badgeIgnoreChildPadding: Boolean = true,
|
||||||
|
|
||||||
|
/**最小的高度大小, px. 大于0生效; 圆点时属性无效*/
|
||||||
|
var badgeMinHeight: Int = -2,
|
||||||
|
|
||||||
|
/**最小的宽度大小, px. 大于0生效; 圆点时属性无效;
|
||||||
|
* -1 表示使用使用计算出来的高度值*/
|
||||||
|
var badgeMinWidth: Int = -1
|
||||||
|
)
|
279
TabLayout/src/main/java/com/angcyo/tablayout/DslTabBorder.kt
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边框绘制, 支持首尾圆角中间不圆角的样式
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/11/27
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
open class DslTabBorder : DslGradientDrawable() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否要接管[itemView]背景的绘制
|
||||||
|
* [updateItemBackground]
|
||||||
|
* */
|
||||||
|
var borderDrawItemBackground: Boolean = true
|
||||||
|
|
||||||
|
/**是否保持每个[itemView]的圆角都一样, 否则首尾有圆角, 中间没有圆角*/
|
||||||
|
var borderKeepItemRadius: Boolean = false
|
||||||
|
|
||||||
|
var borderBackgroundDrawable: Drawable? = null
|
||||||
|
|
||||||
|
/**宽度补偿*/
|
||||||
|
var borderBackgroundWidthOffset: Int = 0
|
||||||
|
|
||||||
|
/**高度补偿*/
|
||||||
|
var borderBackgroundHeightOffset: Int = 0
|
||||||
|
|
||||||
|
/**强制指定选中item的背景颜色*/
|
||||||
|
var borderItemBackgroundSolidColor: Int? = null
|
||||||
|
|
||||||
|
/**当item不可选中时的背景绘制颜色
|
||||||
|
* [com.angcyo.tablayout.DslTabLayout.itemEnableSelector]
|
||||||
|
* [borderItemBackgroundSolidColor]*/
|
||||||
|
var borderItemBackgroundSolidDisableColor: Int? = null
|
||||||
|
|
||||||
|
/**强制指定选中item的背景渐变颜色*/
|
||||||
|
var borderItemBackgroundGradientColors: IntArray? = null
|
||||||
|
|
||||||
|
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||||
|
val typedArray =
|
||||||
|
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||||
|
|
||||||
|
val borderBackgroundColor =
|
||||||
|
typedArray.getColor(R.styleable.DslTabLayout_tab_border_solid_color, gradientSolidColor)
|
||||||
|
|
||||||
|
gradientStrokeColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_border_stroke_color,
|
||||||
|
gradientStrokeColor
|
||||||
|
)
|
||||||
|
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_border_stroke_width,
|
||||||
|
2 * dpi
|
||||||
|
)
|
||||||
|
val radiusSize =
|
||||||
|
typedArray.getDimensionPixelOffset(R.styleable.DslTabLayout_tab_border_radius_size, 0)
|
||||||
|
|
||||||
|
cornerRadius(radiusSize.toFloat())
|
||||||
|
|
||||||
|
originDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_border_drawable)
|
||||||
|
|
||||||
|
borderDrawItemBackground = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_border_draw_item_background,
|
||||||
|
borderDrawItemBackground
|
||||||
|
)
|
||||||
|
|
||||||
|
borderKeepItemRadius = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_border_keep_item_radius,
|
||||||
|
borderKeepItemRadius
|
||||||
|
)
|
||||||
|
|
||||||
|
borderBackgroundWidthOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_border_item_background_width_offset,
|
||||||
|
borderBackgroundWidthOffset
|
||||||
|
)
|
||||||
|
|
||||||
|
borderBackgroundHeightOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_border_item_background_height_offset,
|
||||||
|
borderBackgroundHeightOffset
|
||||||
|
)
|
||||||
|
|
||||||
|
//
|
||||||
|
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_solid_color)) {
|
||||||
|
borderItemBackgroundSolidColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_border_item_background_solid_color,
|
||||||
|
gradientStrokeColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_solid_disable_color)) {
|
||||||
|
borderItemBackgroundSolidDisableColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_border_item_background_solid_disable_color,
|
||||||
|
borderItemBackgroundSolidColor ?: gradientStrokeColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_gradient_start_color) ||
|
||||||
|
typedArray.hasValue(R.styleable.DslTabLayout_tab_border_item_background_gradient_end_color)
|
||||||
|
) {
|
||||||
|
val startColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_border_item_background_gradient_start_color,
|
||||||
|
gradientStrokeColor
|
||||||
|
)
|
||||||
|
val endColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_border_item_background_gradient_end_color,
|
||||||
|
gradientStrokeColor
|
||||||
|
)
|
||||||
|
borderItemBackgroundGradientColors = intArrayOf(startColor, endColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
typedArray.recycle()
|
||||||
|
|
||||||
|
if (originDrawable == null) {
|
||||||
|
//无自定义的drawable, 那么自绘.
|
||||||
|
borderBackgroundDrawable = DslGradientDrawable().configDrawable {
|
||||||
|
gradientSolidColor = borderBackgroundColor
|
||||||
|
gradientRadii = this@DslTabBorder.gradientRadii
|
||||||
|
}.originDrawable
|
||||||
|
|
||||||
|
updateOriginDrawable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
super.draw(canvas)
|
||||||
|
|
||||||
|
originDrawable?.apply {
|
||||||
|
setBounds(
|
||||||
|
paddingLeft,
|
||||||
|
paddingBottom,
|
||||||
|
viewWidth - paddingRight,
|
||||||
|
viewHeight - paddingBottom
|
||||||
|
)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawBorderBackground(canvas: Canvas) {
|
||||||
|
super.draw(canvas)
|
||||||
|
|
||||||
|
borderBackgroundDrawable?.apply {
|
||||||
|
setBounds(
|
||||||
|
paddingLeft,
|
||||||
|
paddingBottom,
|
||||||
|
viewWidth - paddingRight,
|
||||||
|
viewHeight - paddingBottom
|
||||||
|
)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemSelectBgDrawable: Drawable? = null
|
||||||
|
var itemDeselectBgDrawable: Drawable? = null
|
||||||
|
|
||||||
|
/**开启边框绘制后, [itemView]的背景也需要负责设置*/
|
||||||
|
open fun updateItemBackground(
|
||||||
|
tabLayout: DslTabLayout,
|
||||||
|
itemView: View,
|
||||||
|
index: Int,
|
||||||
|
select: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
if (!borderDrawItemBackground) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (select) {
|
||||||
|
|
||||||
|
val isFirst = index == 0
|
||||||
|
val isLast = index == tabLayout.dslSelector.visibleViewList.size - 1
|
||||||
|
|
||||||
|
val drawable = DslGradientDrawable().configDrawable {
|
||||||
|
gradientWidthOffset = borderBackgroundWidthOffset
|
||||||
|
gradientHeightOffset = borderBackgroundHeightOffset
|
||||||
|
|
||||||
|
gradientSolidColor =
|
||||||
|
borderItemBackgroundSolidColor ?: this@DslTabBorder.gradientStrokeColor
|
||||||
|
if (!tabLayout.itemEnableSelector) {
|
||||||
|
if (borderItemBackgroundSolidDisableColor != null) {
|
||||||
|
gradientSolidColor = borderItemBackgroundSolidDisableColor!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gradientColors = borderItemBackgroundGradientColors
|
||||||
|
|
||||||
|
if ((isFirst && isLast) || borderKeepItemRadius) {
|
||||||
|
//只有一个child
|
||||||
|
gradientRadii = this@DslTabBorder.gradientRadii
|
||||||
|
} else if (isFirst) {
|
||||||
|
if (tabLayout.isHorizontal()) {
|
||||||
|
if (tabLayout.isLayoutRtl) {
|
||||||
|
gradientRadii = floatArrayOf(
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
this@DslTabBorder.gradientRadii[2],
|
||||||
|
this@DslTabBorder.gradientRadii[3],
|
||||||
|
this@DslTabBorder.gradientRadii[4],
|
||||||
|
this@DslTabBorder.gradientRadii[5],
|
||||||
|
0f,
|
||||||
|
0f
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
gradientRadii = floatArrayOf(
|
||||||
|
this@DslTabBorder.gradientRadii[0],
|
||||||
|
this@DslTabBorder.gradientRadii[1],
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
this@DslTabBorder.gradientRadii[6],
|
||||||
|
this@DslTabBorder.gradientRadii[7]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gradientRadii = floatArrayOf(
|
||||||
|
this@DslTabBorder.gradientRadii[0],
|
||||||
|
this@DslTabBorder.gradientRadii[1],
|
||||||
|
this@DslTabBorder.gradientRadii[2],
|
||||||
|
this@DslTabBorder.gradientRadii[3],
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (isLast) {
|
||||||
|
if (tabLayout.isHorizontal()) {
|
||||||
|
if (tabLayout.isLayoutRtl) {
|
||||||
|
gradientRadii = floatArrayOf(
|
||||||
|
this@DslTabBorder.gradientRadii[0],
|
||||||
|
this@DslTabBorder.gradientRadii[1],
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
this@DslTabBorder.gradientRadii[6],
|
||||||
|
this@DslTabBorder.gradientRadii[7]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
gradientRadii = floatArrayOf(
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
this@DslTabBorder.gradientRadii[2],
|
||||||
|
this@DslTabBorder.gradientRadii[3],
|
||||||
|
this@DslTabBorder.gradientRadii[4],
|
||||||
|
this@DslTabBorder.gradientRadii[5],
|
||||||
|
0f,
|
||||||
|
0f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gradientRadii = floatArrayOf(
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
0f,
|
||||||
|
this@DslTabBorder.gradientRadii[4],
|
||||||
|
this@DslTabBorder.gradientRadii[5],
|
||||||
|
this@DslTabBorder.gradientRadii[6],
|
||||||
|
this@DslTabBorder.gradientRadii[7]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemSelectBgDrawable = drawable
|
||||||
|
|
||||||
|
ViewCompat.setBackground(itemView, itemSelectBgDrawable)
|
||||||
|
} else {
|
||||||
|
ViewCompat.setBackground(itemView, itemDeselectBgDrawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
153
TabLayout/src/main/java/com/angcyo/tablayout/DslTabDivider.kt
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 垂直分割线
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/11/27
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
open class DslTabDivider : DslGradientDrawable() {
|
||||||
|
|
||||||
|
var dividerWidth = 2 * dpi
|
||||||
|
var dividerHeight = 2 * dpi
|
||||||
|
var dividerMarginLeft = 0
|
||||||
|
var dividerMarginRight = 0
|
||||||
|
var dividerMarginTop = 0
|
||||||
|
var dividerMarginBottom = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [LinearLayout.SHOW_DIVIDER_BEGINNING]
|
||||||
|
* [LinearLayout.SHOW_DIVIDER_MIDDLE]
|
||||||
|
* [LinearLayout.SHOW_DIVIDER_END]
|
||||||
|
* */
|
||||||
|
var dividerShowMode = LinearLayout.SHOW_DIVIDER_MIDDLE
|
||||||
|
|
||||||
|
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||||
|
super.initAttribute(context, attributeSet)
|
||||||
|
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||||
|
|
||||||
|
dividerWidth = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_width,
|
||||||
|
dividerWidth
|
||||||
|
)
|
||||||
|
dividerHeight = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_height,
|
||||||
|
dividerHeight
|
||||||
|
)
|
||||||
|
dividerMarginLeft = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_margin_left,
|
||||||
|
dividerMarginLeft
|
||||||
|
)
|
||||||
|
dividerMarginRight = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_margin_right,
|
||||||
|
dividerMarginRight
|
||||||
|
)
|
||||||
|
dividerMarginTop = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_margin_top,
|
||||||
|
dividerMarginTop
|
||||||
|
)
|
||||||
|
dividerMarginBottom = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_margin_bottom,
|
||||||
|
dividerMarginBottom
|
||||||
|
)
|
||||||
|
|
||||||
|
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_divider_solid_color)) {
|
||||||
|
gradientSolidColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_solid_color,
|
||||||
|
gradientSolidColor
|
||||||
|
)
|
||||||
|
} else if (typedArray.hasValue(R.styleable.DslTabLayout_tab_border_stroke_color)) {
|
||||||
|
gradientSolidColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_border_stroke_color,
|
||||||
|
gradientSolidColor
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
gradientSolidColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_deselect_color,
|
||||||
|
gradientSolidColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
gradientStrokeColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_stroke_color,
|
||||||
|
gradientStrokeColor
|
||||||
|
)
|
||||||
|
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_stroke_width,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
val radiusSize =
|
||||||
|
typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_divider_radius_size,
|
||||||
|
2 * dpi
|
||||||
|
)
|
||||||
|
|
||||||
|
cornerRadius(radiusSize.toFloat())
|
||||||
|
|
||||||
|
originDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_divider_drawable)
|
||||||
|
|
||||||
|
dividerShowMode =
|
||||||
|
typedArray.getInt(R.styleable.DslTabLayout_tab_divider_show_mode, dividerShowMode)
|
||||||
|
|
||||||
|
typedArray.recycle()
|
||||||
|
|
||||||
|
if (originDrawable == null) {
|
||||||
|
//无自定义的drawable, 那么自绘.
|
||||||
|
|
||||||
|
updateOriginDrawable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
super.draw(canvas)
|
||||||
|
|
||||||
|
originDrawable?.apply {
|
||||||
|
bounds = this@DslTabDivider.bounds
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val _tabLayout: DslTabLayout?
|
||||||
|
get() = if (callback is DslTabLayout) callback as DslTabLayout else null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [childIndex]位置前面是否需要分割线
|
||||||
|
* */
|
||||||
|
open fun haveBeforeDivider(childIndex: Int, childCount: Int): Boolean {
|
||||||
|
val tabLayout = _tabLayout
|
||||||
|
if (tabLayout != null && tabLayout.isHorizontal() && tabLayout.isLayoutRtl) {
|
||||||
|
if (childIndex == 0) {
|
||||||
|
return dividerShowMode and LinearLayout.SHOW_DIVIDER_END != 0
|
||||||
|
}
|
||||||
|
return dividerShowMode and LinearLayout.SHOW_DIVIDER_MIDDLE != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childIndex == 0) {
|
||||||
|
return dividerShowMode and LinearLayout.SHOW_DIVIDER_BEGINNING != 0
|
||||||
|
}
|
||||||
|
return dividerShowMode and LinearLayout.SHOW_DIVIDER_MIDDLE != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [childIndex]位置后面是否需要分割线
|
||||||
|
* */
|
||||||
|
open fun haveAfterDivider(childIndex: Int, childCount: Int): Boolean {
|
||||||
|
val tabLayout = _tabLayout
|
||||||
|
if (tabLayout != null && tabLayout.isHorizontal() && tabLayout.isLayoutRtl) {
|
||||||
|
if (childIndex == childCount - 1) {
|
||||||
|
return dividerShowMode and LinearLayout.SHOW_DIVIDER_BEGINNING != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childIndex == childCount - 1) {
|
||||||
|
return dividerShowMode and LinearLayout.SHOW_DIVIDER_END != 0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
118
TabLayout/src/main/java/com/angcyo/tablayout/DslTabHighlight.kt
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.ViewGroup
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2021/05/19
|
||||||
|
* Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
open class DslTabHighlight(val tabLayout: DslTabLayout) : DslGradientDrawable() {
|
||||||
|
|
||||||
|
/**需要绘制的Drawable*/
|
||||||
|
var highlightDrawable: Drawable? = null
|
||||||
|
|
||||||
|
/**宽度测量模式*/
|
||||||
|
var highlightWidth = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
|
||||||
|
/**高度测量模式*/
|
||||||
|
var highlightHeight = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
|
||||||
|
/**宽度补偿*/
|
||||||
|
var highlightWidthOffset = 0
|
||||||
|
|
||||||
|
/**高度补偿*/
|
||||||
|
var highlightHeightOffset = 0
|
||||||
|
|
||||||
|
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||||
|
//super.initAttribute(context, attributeSet)
|
||||||
|
|
||||||
|
val typedArray =
|
||||||
|
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||||
|
highlightDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_highlight_drawable)
|
||||||
|
|
||||||
|
highlightWidth = typedArray.getLayoutDimension(
|
||||||
|
R.styleable.DslTabLayout_tab_highlight_width,
|
||||||
|
highlightWidth
|
||||||
|
)
|
||||||
|
highlightHeight = typedArray.getLayoutDimension(
|
||||||
|
R.styleable.DslTabLayout_tab_highlight_height,
|
||||||
|
highlightHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
highlightWidthOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_highlight_width_offset,
|
||||||
|
highlightWidthOffset
|
||||||
|
)
|
||||||
|
highlightHeightOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_highlight_height_offset,
|
||||||
|
highlightHeightOffset
|
||||||
|
)
|
||||||
|
|
||||||
|
typedArray.recycle()
|
||||||
|
|
||||||
|
if (highlightDrawable == null && isValidConfig()) {
|
||||||
|
updateOriginDrawable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateOriginDrawable(): GradientDrawable? {
|
||||||
|
val drawable = super.updateOriginDrawable()
|
||||||
|
highlightDrawable = originDrawable
|
||||||
|
return drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
//super.draw(canvas)
|
||||||
|
val itemView = tabLayout.currentItemView
|
||||||
|
if (itemView != null) {
|
||||||
|
val lp = itemView.layoutParams
|
||||||
|
|
||||||
|
if (lp is DslTabLayout.LayoutParams) {
|
||||||
|
lp.highlightDrawable ?: highlightDrawable
|
||||||
|
} else {
|
||||||
|
highlightDrawable
|
||||||
|
}?.apply {
|
||||||
|
|
||||||
|
val drawWidth: Int = when (highlightWidth) {
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT -> itemView.measuredWidth
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT -> intrinsicWidth
|
||||||
|
else -> highlightWidth
|
||||||
|
} + highlightWidthOffset
|
||||||
|
|
||||||
|
val drawHeight: Int = when (highlightHeight) {
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT -> itemView.measuredHeight
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT -> intrinsicHeight
|
||||||
|
else -> highlightHeight
|
||||||
|
} + highlightHeightOffset
|
||||||
|
|
||||||
|
val centerX: Int = itemView.left + (itemView.right - itemView.left) / 2
|
||||||
|
val centerY: Int = itemView.top + (itemView.bottom - itemView.top) / 2
|
||||||
|
|
||||||
|
setBounds(
|
||||||
|
centerX - drawWidth / 2,
|
||||||
|
centerY - drawHeight / 2,
|
||||||
|
centerX + drawWidth / 2,
|
||||||
|
centerY + drawHeight / 2
|
||||||
|
)
|
||||||
|
|
||||||
|
draw(canvas)
|
||||||
|
canvas.save()
|
||||||
|
if (tabLayout.isHorizontal()) {
|
||||||
|
canvas.translate(itemView.left.toFloat(), 0f)
|
||||||
|
} else {
|
||||||
|
canvas.translate(0f, itemView.top.toFloat())
|
||||||
|
}
|
||||||
|
itemView.draw(canvas)
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
931
TabLayout/src/main/java/com/angcyo/tablayout/DslTabIndicator.kt
Normal file
@ -0,0 +1,931 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.GradientDrawable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.graphics.withSave
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指示器
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/11/25
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
open class DslTabIndicator(val tabLayout: DslTabLayout) : DslGradientDrawable() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**非颜色值*/
|
||||||
|
const val NO_COLOR = -2
|
||||||
|
|
||||||
|
//---style---
|
||||||
|
|
||||||
|
/**不绘制指示器*/
|
||||||
|
const val INDICATOR_STYLE_NONE = 0
|
||||||
|
|
||||||
|
/**指示器绘制在[itemView]的顶部*/
|
||||||
|
const val INDICATOR_STYLE_TOP = 0x1
|
||||||
|
|
||||||
|
/**指示器绘制在[itemView]的底部*/
|
||||||
|
const val INDICATOR_STYLE_BOTTOM = 0x2
|
||||||
|
|
||||||
|
/**默认样式,指示器绘制在[itemView]的中心*/
|
||||||
|
const val INDICATOR_STYLE_CENTER = 0x4
|
||||||
|
|
||||||
|
/**前景绘制,
|
||||||
|
* 默认是背景绘制, 指示器绘制[itemView]的背部, [itemView] 请不要设置background, 否则可能看不见*/
|
||||||
|
const val INDICATOR_STYLE_FOREGROUND = 0x1000
|
||||||
|
|
||||||
|
//---gravity---
|
||||||
|
|
||||||
|
/**指示器重力在开始的位置(横向左边, 纵向上边)*/
|
||||||
|
const val INDICATOR_GRAVITY_START = 0x1
|
||||||
|
|
||||||
|
/**指示器重力在结束的位置(横向右边, 纵向下边)*/
|
||||||
|
const val INDICATOR_GRAVITY_END = 0x2
|
||||||
|
|
||||||
|
/**指示器重力在中间*/
|
||||||
|
const val INDICATOR_GRAVITY_CENTER = 0x4
|
||||||
|
}
|
||||||
|
|
||||||
|
/**指示器绘制的样式*/
|
||||||
|
var indicatorStyle = INDICATOR_STYLE_NONE //初始化
|
||||||
|
|
||||||
|
/**[indicatorStyle]*/
|
||||||
|
val _indicatorDrawStyle: Int
|
||||||
|
get() = indicatorStyle.remove(INDICATOR_STYLE_FOREGROUND)
|
||||||
|
|
||||||
|
/**优先将指示器显示在[DslTabLayout]的什么位置
|
||||||
|
* [INDICATOR_GRAVITY_START] 开始的位置
|
||||||
|
* [INDICATOR_GRAVITY_END] 结束的位置
|
||||||
|
* [INDICATOR_GRAVITY_CENTER] 中间的位置*/
|
||||||
|
var indicatorGravity = INDICATOR_GRAVITY_CENTER
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指示器在流向下一个位置时, 是否采用[Flow]流线的方式改变宽度
|
||||||
|
* */
|
||||||
|
var indicatorEnableFlow: Boolean = false
|
||||||
|
|
||||||
|
/**指示器闪现效果, 从当前位置直接跨越到目标位置*/
|
||||||
|
var indicatorEnableFlash: Boolean = false
|
||||||
|
|
||||||
|
/**使用clip的方式绘制闪现效果*/
|
||||||
|
var indicatorEnableFlashClip: Boolean = true
|
||||||
|
|
||||||
|
/**当目标和当前的索引差值<=此值时, [Flow]效果才有效*/
|
||||||
|
var indicatorFlowStep: Int = 1
|
||||||
|
|
||||||
|
/**指示器绘制实体*/
|
||||||
|
var indicatorDrawable: Drawable? = null
|
||||||
|
set(value) {
|
||||||
|
field = tintDrawableColor(value, indicatorColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**过滤[indicatorDrawable]的颜色*/
|
||||||
|
var indicatorColor: Int = NO_COLOR
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
indicatorDrawable = indicatorDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指示器的宽度
|
||||||
|
* WRAP_CONTENT: [childView]内容的宽度,
|
||||||
|
* MATCH_PARENT: [childView]的宽度
|
||||||
|
* 40dp: 固定值
|
||||||
|
* */
|
||||||
|
var indicatorWidth = 0 //初始化
|
||||||
|
|
||||||
|
/**宽度补偿*/
|
||||||
|
var indicatorWidthOffset = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指示器的高度
|
||||||
|
* WRAP_CONTENT: [childView]内容的高度,
|
||||||
|
* MATCH_PARENT: [childView]的高度
|
||||||
|
* 40dp: 固定值
|
||||||
|
* */
|
||||||
|
var indicatorHeight = 0 //初始化
|
||||||
|
|
||||||
|
/**高度补偿*/
|
||||||
|
var indicatorHeightOffset = 0
|
||||||
|
|
||||||
|
/**XY轴方向补偿*/
|
||||||
|
var indicatorXOffset = 0
|
||||||
|
|
||||||
|
/**会根据[indicatorStyle]自动取负值*/
|
||||||
|
var indicatorYOffset = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宽高[WRAP_CONTENT]时, 内容view的定位索引
|
||||||
|
* */
|
||||||
|
var indicatorContentIndex = -1
|
||||||
|
var indicatorContentId = View.NO_ID
|
||||||
|
|
||||||
|
/**切换时是否需要动画的支持*/
|
||||||
|
var indicatorAnim = true
|
||||||
|
|
||||||
|
/**在获取锚点view的宽高时, 是否需要忽略对应的padding属性*/
|
||||||
|
var ignoreChildPadding: Boolean = true
|
||||||
|
|
||||||
|
init {
|
||||||
|
callback = tabLayout
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initAttribute(context: Context, attributeSet: AttributeSet?) {
|
||||||
|
val typedArray =
|
||||||
|
context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||||
|
|
||||||
|
indicatorDrawable = typedArray.getDrawable(R.styleable.DslTabLayout_tab_indicator_drawable)
|
||||||
|
indicatorColor =
|
||||||
|
typedArray.getColor(R.styleable.DslTabLayout_tab_indicator_color, indicatorColor)
|
||||||
|
indicatorStyle = typedArray.getInt(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_style,
|
||||||
|
if (tabLayout.isHorizontal()) INDICATOR_STYLE_BOTTOM else INDICATOR_STYLE_TOP
|
||||||
|
)
|
||||||
|
indicatorGravity = typedArray.getInt(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_gravity,
|
||||||
|
indicatorGravity
|
||||||
|
)
|
||||||
|
|
||||||
|
//初始化指示器的高度和宽度
|
||||||
|
if (indicatorStyle.have(INDICATOR_STYLE_FOREGROUND)) {
|
||||||
|
//前景绘制
|
||||||
|
indicatorWidth = typedArray.getLayoutDimension(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_width,
|
||||||
|
if (tabLayout.isHorizontal()) ViewGroup.LayoutParams.MATCH_PARENT else 3 * dpi
|
||||||
|
)
|
||||||
|
indicatorHeight = typedArray.getLayoutDimension(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_height,
|
||||||
|
if (tabLayout.isHorizontal()) 3 * dpi else ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
indicatorXOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_x_offset,
|
||||||
|
if (tabLayout.isHorizontal()) 0 else 2 * dpi
|
||||||
|
)
|
||||||
|
indicatorYOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_y_offset,
|
||||||
|
if (tabLayout.isHorizontal()) 2 * dpi else 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
//背景绘制样式
|
||||||
|
if (tabLayout.isHorizontal()) {
|
||||||
|
indicatorWidth = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
indicatorHeight = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
} else {
|
||||||
|
indicatorHeight = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
indicatorWidth = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
}
|
||||||
|
indicatorWidth = typedArray.getLayoutDimension(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_width,
|
||||||
|
indicatorWidth
|
||||||
|
)
|
||||||
|
indicatorHeight = typedArray.getLayoutDimension(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_height,
|
||||||
|
indicatorHeight
|
||||||
|
)
|
||||||
|
indicatorXOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_x_offset,
|
||||||
|
indicatorXOffset
|
||||||
|
)
|
||||||
|
indicatorYOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_y_offset,
|
||||||
|
indicatorYOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreChildPadding = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_ignore_child_padding,
|
||||||
|
!indicatorStyle.have(INDICATOR_STYLE_CENTER)
|
||||||
|
)
|
||||||
|
|
||||||
|
indicatorFlowStep =
|
||||||
|
typedArray.getInt(R.styleable.DslTabLayout_tab_indicator_flow_step, indicatorFlowStep)
|
||||||
|
indicatorEnableFlow = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_enable_flow,
|
||||||
|
indicatorEnableFlow
|
||||||
|
)
|
||||||
|
indicatorEnableFlash = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_enable_flash,
|
||||||
|
indicatorEnableFlash
|
||||||
|
)
|
||||||
|
indicatorEnableFlashClip = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_enable_flash_clip,
|
||||||
|
indicatorEnableFlashClip
|
||||||
|
)
|
||||||
|
|
||||||
|
indicatorWidthOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_width_offset,
|
||||||
|
indicatorWidthOffset
|
||||||
|
)
|
||||||
|
indicatorHeightOffset = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_height_offset,
|
||||||
|
indicatorHeightOffset
|
||||||
|
)
|
||||||
|
indicatorContentIndex = typedArray.getInt(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_content_index,
|
||||||
|
indicatorContentIndex
|
||||||
|
)
|
||||||
|
indicatorContentId = typedArray.getResourceId(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_content_id,
|
||||||
|
indicatorContentId
|
||||||
|
)
|
||||||
|
indicatorAnim = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_anim,
|
||||||
|
indicatorAnim
|
||||||
|
)
|
||||||
|
|
||||||
|
//代码构建Drawable
|
||||||
|
gradientShape =
|
||||||
|
typedArray.getInt(R.styleable.DslTabLayout_tab_indicator_shape, gradientShape)
|
||||||
|
gradientSolidColor =
|
||||||
|
typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_solid_color,
|
||||||
|
gradientSolidColor
|
||||||
|
)
|
||||||
|
gradientStrokeColor =
|
||||||
|
typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_stroke_color,
|
||||||
|
gradientStrokeColor
|
||||||
|
)
|
||||||
|
gradientStrokeWidth = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_stroke_width,
|
||||||
|
gradientStrokeWidth
|
||||||
|
)
|
||||||
|
gradientDashWidth = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_dash_width,
|
||||||
|
gradientDashWidth.toInt()
|
||||||
|
).toFloat()
|
||||||
|
gradientDashGap = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_dash_gap,
|
||||||
|
gradientDashGap.toInt()
|
||||||
|
).toFloat()
|
||||||
|
|
||||||
|
val gradientRadius =
|
||||||
|
typedArray.getDimensionPixelOffset(R.styleable.DslTabLayout_tab_indicator_radius, 0)
|
||||||
|
if (gradientRadius > 0) {
|
||||||
|
Arrays.fill(gradientRadii, gradientRadius.toFloat())
|
||||||
|
} else {
|
||||||
|
typedArray.getString(R.styleable.DslTabLayout_tab_indicator_radii)?.let {
|
||||||
|
_fillRadii(gradientRadii, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val gradientColors =
|
||||||
|
typedArray.getString(R.styleable.DslTabLayout_tab_indicator_gradient_colors)
|
||||||
|
|
||||||
|
this.gradientColors = if (gradientColors.isNullOrEmpty()) {
|
||||||
|
val startColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_gradient_start_color,
|
||||||
|
Color.TRANSPARENT
|
||||||
|
)
|
||||||
|
val endColor = typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_indicator_gradient_end_color,
|
||||||
|
Color.TRANSPARENT
|
||||||
|
)
|
||||||
|
if (startColor != endColor) {
|
||||||
|
intArrayOf(startColor, endColor)
|
||||||
|
} else {
|
||||||
|
this.gradientColors
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_fillColor(gradientColors) ?: this.gradientColors
|
||||||
|
}
|
||||||
|
//...end
|
||||||
|
|
||||||
|
typedArray.recycle()
|
||||||
|
|
||||||
|
if (indicatorDrawable == null && isValidConfig()) {
|
||||||
|
updateOriginDrawable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateOriginDrawable(): GradientDrawable? {
|
||||||
|
val drawable = super.updateOriginDrawable()
|
||||||
|
indicatorDrawable = originDrawable
|
||||||
|
return drawable
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun tintDrawableColor(drawable: Drawable?, color: Int): Drawable? {
|
||||||
|
if (drawable == null || color == NO_COLOR) {
|
||||||
|
return drawable
|
||||||
|
}
|
||||||
|
return drawable.tintDrawableColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**指示器需要参考的目标控件*/
|
||||||
|
open fun indicatorContentView(childView: View): View? {
|
||||||
|
val lp = childView.layoutParams as DslTabLayout.LayoutParams
|
||||||
|
|
||||||
|
val contentId =
|
||||||
|
if (lp.indicatorContentId != View.NO_ID) lp.indicatorContentId else indicatorContentId
|
||||||
|
|
||||||
|
if (contentId != View.NO_ID) {
|
||||||
|
return childView.findViewById(contentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
//如果child强制指定了index, 就用指定的.
|
||||||
|
val contentIndex =
|
||||||
|
if (lp.indicatorContentIndex >= 0) lp.indicatorContentIndex else indicatorContentIndex
|
||||||
|
|
||||||
|
return if (contentIndex >= 0 && childView is ViewGroup && contentIndex in 0 until childView.childCount) {
|
||||||
|
//有指定
|
||||||
|
val contentChildView = childView.getChildAt(contentIndex)
|
||||||
|
contentChildView
|
||||||
|
} else {
|
||||||
|
//没有指定
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**根据指定[index]索引, 获取目标[View]*/
|
||||||
|
open fun targetChildView(
|
||||||
|
index: Int,
|
||||||
|
onChildView: (childView: View, contentChildView: View?) -> Unit
|
||||||
|
) {
|
||||||
|
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||||
|
onChildView(childView, indicatorContentView(childView))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getChildTargetPaddingLeft(childView: View): Int =
|
||||||
|
if (ignoreChildPadding) childView.paddingLeft else 0
|
||||||
|
|
||||||
|
open fun getChildTargetPaddingRight(childView: View): Int =
|
||||||
|
if (ignoreChildPadding) childView.paddingRight else 0
|
||||||
|
|
||||||
|
open fun getChildTargetPaddingTop(childView: View): Int =
|
||||||
|
if (ignoreChildPadding) childView.paddingTop else 0
|
||||||
|
|
||||||
|
open fun getChildTargetPaddingBottom(childView: View): Int =
|
||||||
|
if (ignoreChildPadding) childView.paddingBottom else 0
|
||||||
|
|
||||||
|
open fun getChildTargetWidth(childView: View): Int =
|
||||||
|
if (ignoreChildPadding) childView.viewDrawWidth else childView.measuredWidth
|
||||||
|
|
||||||
|
open fun getChildTargetHeight(childView: View): Int =
|
||||||
|
if (ignoreChildPadding) childView.viewDrawHeight else childView.measuredHeight
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [childview]对应的中心x坐标
|
||||||
|
* */
|
||||||
|
open fun getChildTargetX(index: Int, gravity: Int = indicatorGravity): Int {
|
||||||
|
|
||||||
|
var result = if (index > 0) tabLayout.maxWidth else 0
|
||||||
|
|
||||||
|
targetChildView(index) { childView, contentChildView ->
|
||||||
|
result = if (contentChildView == null) {
|
||||||
|
when (gravity) {
|
||||||
|
INDICATOR_GRAVITY_START -> childView.left
|
||||||
|
INDICATOR_GRAVITY_END -> childView.right
|
||||||
|
else -> childView.left + getChildTargetPaddingLeft(childView) + getChildTargetWidth(
|
||||||
|
childView
|
||||||
|
) / 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (gravity) {
|
||||||
|
INDICATOR_GRAVITY_START -> childView.left + contentChildView.left
|
||||||
|
INDICATOR_GRAVITY_END -> childView.left + contentChildView.right
|
||||||
|
else -> childView.left + contentChildView.left + getChildTargetPaddingLeft(
|
||||||
|
contentChildView
|
||||||
|
) + getChildTargetWidth(
|
||||||
|
contentChildView
|
||||||
|
) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getChildTargetY(index: Int, gravity: Int = indicatorGravity): Int {
|
||||||
|
|
||||||
|
var result = if (index > 0) tabLayout.maxHeight else 0
|
||||||
|
|
||||||
|
targetChildView(index) { childView, contentChildView ->
|
||||||
|
result = if (contentChildView == null) {
|
||||||
|
when (gravity) {
|
||||||
|
INDICATOR_GRAVITY_START -> childView.top
|
||||||
|
INDICATOR_GRAVITY_END -> childView.bottom
|
||||||
|
else -> childView.top + getChildTargetPaddingTop(childView) + getChildTargetHeight(
|
||||||
|
childView
|
||||||
|
) / 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (gravity) {
|
||||||
|
INDICATOR_GRAVITY_START -> childView.top + contentChildView.top
|
||||||
|
INDICATOR_GRAVITY_END -> childView.top + childView.bottom
|
||||||
|
else -> childView.top + contentChildView.top + getChildTargetPaddingTop(
|
||||||
|
contentChildView
|
||||||
|
) + getChildTargetHeight(
|
||||||
|
contentChildView
|
||||||
|
) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getIndicatorDrawWidth(index: Int): Int {
|
||||||
|
var result = indicatorWidth
|
||||||
|
|
||||||
|
when (indicatorWidth) {
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT -> {
|
||||||
|
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||||
|
result = getChildTargetWidth(indicatorContentView(childView) ?: childView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT -> {
|
||||||
|
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||||
|
result = childView.measuredWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result + indicatorWidthOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getIndicatorDrawHeight(index: Int): Int {
|
||||||
|
var result = indicatorHeight
|
||||||
|
|
||||||
|
when (indicatorHeight) {
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT -> {
|
||||||
|
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||||
|
result = getChildTargetHeight(indicatorContentView(childView) ?: childView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT -> {
|
||||||
|
tabLayout.dslSelector.visibleViewList.getOrNull(index)?.also { childView ->
|
||||||
|
result = childView.measuredHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result + indicatorHeightOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
//super.draw(canvas)
|
||||||
|
if (!isVisible || _indicatorDrawStyle == INDICATOR_STYLE_NONE || indicatorDrawable == null) {
|
||||||
|
//不绘制
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabLayout.isHorizontal()) {
|
||||||
|
drawHorizontal(canvas)
|
||||||
|
} else {
|
||||||
|
drawVertical(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawHorizontal(canvas: Canvas) {
|
||||||
|
val childSize = tabLayout.dslSelector.visibleViewList.size
|
||||||
|
|
||||||
|
var currentIndex = currentIndex
|
||||||
|
|
||||||
|
if (_targetIndex in 0 until childSize) {
|
||||||
|
currentIndex = max(0, currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex in 0 until childSize) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//无效的index
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//"绘制$currentIndex:$currentSelectIndex $positionOffset".logi()
|
||||||
|
|
||||||
|
val drawTargetX = getChildTargetX(currentIndex)
|
||||||
|
val drawWidth = getIndicatorDrawWidth(currentIndex)
|
||||||
|
val drawHeight = getIndicatorDrawHeight(currentIndex)
|
||||||
|
|
||||||
|
val drawLeft = drawTargetX - drawWidth / 2 + indicatorXOffset
|
||||||
|
|
||||||
|
//动画过程中的left
|
||||||
|
var animLeft = drawLeft
|
||||||
|
//width
|
||||||
|
var animWidth = drawWidth
|
||||||
|
//动画执行过程中, 高度额外变大的值
|
||||||
|
var animExHeight = 0
|
||||||
|
|
||||||
|
//end value
|
||||||
|
val nextDrawTargetX = getChildTargetX(_targetIndex)
|
||||||
|
val nextDrawWidth = getIndicatorDrawWidth(_targetIndex)
|
||||||
|
val nextDrawLeft = nextDrawTargetX - nextDrawWidth / 2 + indicatorXOffset
|
||||||
|
|
||||||
|
var animEndWidth = nextDrawWidth
|
||||||
|
var animEndLeft = nextDrawLeft
|
||||||
|
|
||||||
|
if (_targetIndex in 0 until childSize && _targetIndex != currentIndex) {
|
||||||
|
|
||||||
|
//动画过程参数计算变量
|
||||||
|
val animStartLeft = drawLeft
|
||||||
|
val animStartWidth = drawWidth
|
||||||
|
|
||||||
|
val animEndHeight = getIndicatorDrawHeight(_targetIndex)
|
||||||
|
|
||||||
|
if (indicatorEnableFlash) {
|
||||||
|
//闪现效果
|
||||||
|
animWidth = (animWidth * (1 - positionOffset)).toInt()
|
||||||
|
animEndWidth = (animEndWidth * positionOffset).toInt()
|
||||||
|
|
||||||
|
animLeft = drawTargetX - animWidth / 2 + indicatorXOffset
|
||||||
|
animEndLeft = nextDrawLeft
|
||||||
|
} else if (indicatorEnableFlow && (_targetIndex - currentIndex).absoluteValue <= indicatorFlowStep) {
|
||||||
|
//激活了流动效果
|
||||||
|
|
||||||
|
val flowEndWidth: Int
|
||||||
|
if (_targetIndex > currentIndex) {
|
||||||
|
flowEndWidth = animEndLeft - animStartLeft + animEndWidth
|
||||||
|
|
||||||
|
//目标在右边
|
||||||
|
animLeft = if (positionOffset >= 0.5) {
|
||||||
|
(animStartLeft + (animEndLeft - animStartLeft) * (positionOffset - 0.5) / 0.5f).toInt()
|
||||||
|
} else {
|
||||||
|
animStartLeft
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flowEndWidth = animStartLeft - animEndLeft + animStartWidth
|
||||||
|
|
||||||
|
//目标在左边
|
||||||
|
animLeft = if (positionOffset >= 0.5) {
|
||||||
|
animEndLeft
|
||||||
|
} else {
|
||||||
|
(animStartLeft - (animStartLeft - animEndLeft) * positionOffset / 0.5f).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animWidth = if (positionOffset >= 0.5) {
|
||||||
|
(flowEndWidth - (flowEndWidth - animEndWidth) * (positionOffset - 0.5) / 0.5f).toInt()
|
||||||
|
} else {
|
||||||
|
(animStartWidth + (flowEndWidth - animStartWidth) * positionOffset / 0.5f).toInt()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//默认平移效果
|
||||||
|
if (_targetIndex > currentIndex) {
|
||||||
|
//目标在右边
|
||||||
|
animLeft =
|
||||||
|
(animStartLeft + (animEndLeft - animStartLeft) * positionOffset).toInt()
|
||||||
|
} else {
|
||||||
|
//目标在左边
|
||||||
|
animLeft =
|
||||||
|
(animStartLeft - (animStartLeft - animEndLeft) * positionOffset).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
//动画过程中的宽度
|
||||||
|
animWidth =
|
||||||
|
(animStartWidth + (animEndWidth - animStartWidth) * positionOffset).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
animExHeight = ((animEndHeight - drawHeight) * positionOffset).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
//前景
|
||||||
|
val drawTop = when (_indicatorDrawStyle) {
|
||||||
|
//底部绘制
|
||||||
|
INDICATOR_STYLE_BOTTOM -> viewHeight - drawHeight - indicatorYOffset
|
||||||
|
//顶部绘制
|
||||||
|
INDICATOR_STYLE_TOP -> 0 + indicatorYOffset
|
||||||
|
//居中绘制
|
||||||
|
else -> paddingTop + viewDrawHeight / 2 - drawHeight / 2 + indicatorYOffset -
|
||||||
|
animExHeight +
|
||||||
|
(tabLayout._maxConvexHeight - _childConvexHeight(currentIndex)) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
indicatorDrawable?.apply {
|
||||||
|
if (indicatorEnableFlash) {
|
||||||
|
//flash
|
||||||
|
if (indicatorEnableFlashClip) {
|
||||||
|
drawIndicatorClipHorizontal(
|
||||||
|
this,
|
||||||
|
canvas,
|
||||||
|
drawLeft,
|
||||||
|
drawTop,
|
||||||
|
drawLeft + drawWidth,
|
||||||
|
drawTop + drawHeight + animExHeight,
|
||||||
|
animWidth,
|
||||||
|
1 - positionOffset
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawIndicator(
|
||||||
|
this, canvas, animLeft,
|
||||||
|
drawTop,
|
||||||
|
animLeft + animWidth,
|
||||||
|
drawTop + drawHeight + animExHeight,
|
||||||
|
1 - positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_targetIndex in 0 until childSize) {
|
||||||
|
if (indicatorEnableFlashClip) {
|
||||||
|
drawIndicatorClipHorizontal(
|
||||||
|
this,
|
||||||
|
canvas,
|
||||||
|
nextDrawLeft,
|
||||||
|
drawTop,
|
||||||
|
nextDrawLeft + nextDrawWidth,
|
||||||
|
drawTop + drawHeight + animExHeight,
|
||||||
|
animEndWidth,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawIndicator(
|
||||||
|
this, canvas, animEndLeft,
|
||||||
|
drawTop,
|
||||||
|
animEndLeft + animEndWidth,
|
||||||
|
drawTop + drawHeight + animExHeight,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//normal
|
||||||
|
drawIndicator(
|
||||||
|
this, canvas, animLeft,
|
||||||
|
drawTop,
|
||||||
|
animLeft + animWidth,
|
||||||
|
drawTop + drawHeight + animExHeight,
|
||||||
|
1 - positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawIndicator(
|
||||||
|
indicator: Drawable,
|
||||||
|
canvas: Canvas,
|
||||||
|
l: Int,
|
||||||
|
t: Int,
|
||||||
|
r: Int,
|
||||||
|
b: Int,
|
||||||
|
offset: Float
|
||||||
|
) {
|
||||||
|
indicator.apply {
|
||||||
|
if (this is ITabIndicatorDraw) {
|
||||||
|
setBounds(l, t, r, b)
|
||||||
|
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
|
||||||
|
} else {
|
||||||
|
val width = r - l
|
||||||
|
val height = b - t
|
||||||
|
setBounds(0, 0, width, height)
|
||||||
|
canvas.withSave {
|
||||||
|
translate(l.toFloat(), t.toFloat())
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawIndicatorClipHorizontal(
|
||||||
|
indicator: Drawable,
|
||||||
|
canvas: Canvas,
|
||||||
|
l: Int,
|
||||||
|
t: Int,
|
||||||
|
r: Int,
|
||||||
|
b: Int,
|
||||||
|
endWidth: Int,
|
||||||
|
offset: Float
|
||||||
|
) {
|
||||||
|
indicator.apply {
|
||||||
|
canvas.save()
|
||||||
|
val dx = (r - l - endWidth) / 2
|
||||||
|
canvas.clipRect(l + dx, t, r - dx, b)
|
||||||
|
setBounds(l, t, r, b)
|
||||||
|
if (this is ITabIndicatorDraw) {
|
||||||
|
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
|
||||||
|
} else {
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawIndicatorClipVertical(
|
||||||
|
indicator: Drawable,
|
||||||
|
canvas: Canvas,
|
||||||
|
l: Int,
|
||||||
|
t: Int,
|
||||||
|
r: Int,
|
||||||
|
b: Int,
|
||||||
|
endHeight: Int,
|
||||||
|
offset: Float
|
||||||
|
) {
|
||||||
|
indicator.apply {
|
||||||
|
canvas.save()
|
||||||
|
val dy = (b - t - endHeight) / 2
|
||||||
|
canvas.clipRect(l, t + dy, r, b - dy)
|
||||||
|
setBounds(l, t, r, b)
|
||||||
|
if (this is ITabIndicatorDraw) {
|
||||||
|
onDrawTabIndicator(this@DslTabIndicator, canvas, offset)
|
||||||
|
} else {
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawVertical(canvas: Canvas) {
|
||||||
|
val childSize = tabLayout.dslSelector.visibleViewList.size
|
||||||
|
|
||||||
|
var currentIndex = currentIndex
|
||||||
|
|
||||||
|
if (_targetIndex in 0 until childSize) {
|
||||||
|
currentIndex = max(0, currentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentIndex in 0 until childSize) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
//无效的index
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//"绘制$currentIndex:$currentSelectIndex $positionOffset".logi()
|
||||||
|
|
||||||
|
val drawTargetY = getChildTargetY(currentIndex)
|
||||||
|
val drawWidth = getIndicatorDrawWidth(currentIndex)
|
||||||
|
val drawHeight = getIndicatorDrawHeight(currentIndex)
|
||||||
|
|
||||||
|
val drawTop = drawTargetY - drawHeight / 2 + indicatorYOffset
|
||||||
|
|
||||||
|
//动画过程中的top
|
||||||
|
var animTop = drawTop
|
||||||
|
//height
|
||||||
|
var animHeight = drawHeight
|
||||||
|
//动画执行过程中, 宽度额外变大的值
|
||||||
|
var animExWidth = 0
|
||||||
|
|
||||||
|
//end value
|
||||||
|
val nextDrawTargetY = getChildTargetY(_targetIndex)
|
||||||
|
val nextDrawHeight = getIndicatorDrawHeight(_targetIndex)
|
||||||
|
val nextDrawTop = nextDrawTargetY - nextDrawHeight / 2 + indicatorYOffset
|
||||||
|
|
||||||
|
var animEndHeight = nextDrawHeight
|
||||||
|
var animEndTop = nextDrawTop
|
||||||
|
|
||||||
|
if (_targetIndex in 0 until childSize && _targetIndex != currentIndex) {
|
||||||
|
|
||||||
|
//动画过程参数计算变量
|
||||||
|
val animStartTop = drawTop
|
||||||
|
val animStartHeight = drawHeight
|
||||||
|
|
||||||
|
val animEndWidth = getIndicatorDrawWidth(_targetIndex)
|
||||||
|
|
||||||
|
if (indicatorEnableFlash) {
|
||||||
|
//闪现效果
|
||||||
|
animHeight = (animHeight * (1 - positionOffset)).toInt()
|
||||||
|
animEndHeight = (animEndHeight * positionOffset).toInt()
|
||||||
|
|
||||||
|
animTop = drawTargetY - animHeight / 2 + indicatorXOffset
|
||||||
|
animEndTop = nextDrawTargetY - animEndHeight / 2 + indicatorXOffset
|
||||||
|
} else if (indicatorEnableFlow && (_targetIndex - currentIndex).absoluteValue <= indicatorFlowStep) {
|
||||||
|
//激活了流动效果
|
||||||
|
|
||||||
|
val flowEndHeight: Int
|
||||||
|
if (_targetIndex > currentIndex) {
|
||||||
|
flowEndHeight = animEndTop - animStartTop + animEndHeight
|
||||||
|
|
||||||
|
//目标在下边
|
||||||
|
animTop = if (positionOffset >= 0.5) {
|
||||||
|
(animStartTop + (animEndTop - animStartTop) * (positionOffset - 0.5) / 0.5f).toInt()
|
||||||
|
} else {
|
||||||
|
animStartTop
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flowEndHeight = animStartTop - animEndTop + animStartHeight
|
||||||
|
|
||||||
|
//目标在上边
|
||||||
|
animTop = if (positionOffset >= 0.5) {
|
||||||
|
animEndTop
|
||||||
|
} else {
|
||||||
|
(animStartTop - (animStartTop - animEndTop) * positionOffset / 0.5f).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animHeight = if (positionOffset >= 0.5) {
|
||||||
|
(flowEndHeight - (flowEndHeight - animEndHeight) * (positionOffset - 0.5) / 0.5f).toInt()
|
||||||
|
} else {
|
||||||
|
(animStartHeight + (flowEndHeight - animStartHeight) * positionOffset / 0.5f).toInt()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (_targetIndex > currentIndex) {
|
||||||
|
//目标在下边
|
||||||
|
animTop = (animStartTop + (animEndTop - animStartTop) * positionOffset).toInt()
|
||||||
|
} else {
|
||||||
|
//目标在上边
|
||||||
|
animTop = (animStartTop - (animStartTop - animEndTop) * positionOffset).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
//动画过程中的宽度
|
||||||
|
animHeight =
|
||||||
|
(animStartHeight + (animEndHeight - animStartHeight) * positionOffset).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
animExWidth = ((animEndWidth - drawWidth) * positionOffset).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val drawLeft = when (_indicatorDrawStyle) {
|
||||||
|
INDICATOR_STYLE_BOTTOM -> {
|
||||||
|
//右边/底部绘制
|
||||||
|
viewWidth - drawWidth - indicatorXOffset
|
||||||
|
}
|
||||||
|
INDICATOR_STYLE_TOP -> {
|
||||||
|
//左边/顶部绘制
|
||||||
|
0 + indicatorXOffset
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
//居中绘制
|
||||||
|
paddingLeft + indicatorXOffset + (viewDrawWidth / 2 - drawWidth / 2) -
|
||||||
|
(tabLayout._maxConvexHeight - _childConvexHeight(currentIndex)) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indicatorDrawable?.apply {
|
||||||
|
//flash
|
||||||
|
if (indicatorEnableFlash) {
|
||||||
|
if (indicatorEnableFlashClip) {
|
||||||
|
drawIndicatorClipVertical(
|
||||||
|
this, canvas, drawLeft,
|
||||||
|
drawTop,
|
||||||
|
drawLeft + drawWidth + animExWidth,
|
||||||
|
drawTop + drawHeight,
|
||||||
|
animHeight,
|
||||||
|
1 - positionOffset
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawIndicator(
|
||||||
|
this, canvas, drawLeft,
|
||||||
|
animTop,
|
||||||
|
drawLeft + drawWidth + animExWidth,
|
||||||
|
animTop + animHeight,
|
||||||
|
1 - positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_targetIndex in 0 until childSize) {
|
||||||
|
if (indicatorEnableFlashClip) {
|
||||||
|
drawIndicatorClipVertical(
|
||||||
|
this, canvas, drawLeft,
|
||||||
|
nextDrawTop,
|
||||||
|
drawLeft + drawWidth + animExWidth,
|
||||||
|
nextDrawTop + nextDrawHeight,
|
||||||
|
animEndHeight,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
drawIndicator(
|
||||||
|
this, canvas, drawLeft,
|
||||||
|
animEndTop,
|
||||||
|
drawLeft + drawWidth + animExWidth,
|
||||||
|
animEndTop + animEndHeight,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
drawIndicator(
|
||||||
|
this, canvas, drawLeft,
|
||||||
|
animTop,
|
||||||
|
drawLeft + drawWidth + animExWidth,
|
||||||
|
animTop + animHeight,
|
||||||
|
1 - positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun _childConvexHeight(index: Int): Int {
|
||||||
|
if (attachView is ViewGroup) {
|
||||||
|
((attachView as ViewGroup).getChildAt(index).layoutParams as? DslTabLayout.LayoutParams)?.apply {
|
||||||
|
return layoutConvexHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 距离[_targetIndex]的偏移比例.[0->1]的过程
|
||||||
|
* */
|
||||||
|
var positionOffset: Float = 0f
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
invalidateSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**当前绘制的index*/
|
||||||
|
var currentIndex: Int = -1
|
||||||
|
|
||||||
|
/**滚动目标的index*/
|
||||||
|
var _targetIndex = -1
|
||||||
|
}
|
2043
TabLayout/src/main/java/com/angcyo/tablayout/DslTabLayout.kt
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.TypedValue
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import com.angcyo.tablayout.DslTabIndicator.Companion.NO_COLOR
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/11/26
|
||||||
|
* Copyright (c) 2019 ShenZhen O&M Cloud Co., Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
open class DslTabLayoutConfig(val tabLayout: DslTabLayout) : DslSelectorConfig() {
|
||||||
|
|
||||||
|
/**是否开启文本颜色*/
|
||||||
|
var tabEnableTextColor = true
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (field) {
|
||||||
|
tabEnableIcoColor = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**是否开启颜色渐变效果*/
|
||||||
|
var tabEnableGradientColor = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
if (field) {
|
||||||
|
tabEnableIcoGradientColor = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**是否激活指示器的颜色渐变效果*/
|
||||||
|
var tabEnableIndicatorGradientColor = false
|
||||||
|
|
||||||
|
/**选中的文本颜色*/
|
||||||
|
var tabSelectColor: Int = Color.WHITE //Color.parseColor("#333333")
|
||||||
|
|
||||||
|
/**未选中的文本颜色*/
|
||||||
|
var tabDeselectColor: Int = Color.parseColor("#999999")
|
||||||
|
|
||||||
|
/**是否开启Bold, 文本加粗*/
|
||||||
|
var tabEnableTextBold = false
|
||||||
|
|
||||||
|
/**是否使用粗体字体的方式设置粗体, 否则使用[Paint.FAKE_BOLD_TEXT_FLAG]
|
||||||
|
* 需要先激活[tabEnableTextBold]*/
|
||||||
|
var tabUseTypefaceBold = false
|
||||||
|
|
||||||
|
/**是否开启图标颜色*/
|
||||||
|
var tabEnableIcoColor = true
|
||||||
|
|
||||||
|
/**是否开启图标颜色渐变效果*/
|
||||||
|
var tabEnableIcoGradientColor = false
|
||||||
|
|
||||||
|
/**选中的图标颜色*/
|
||||||
|
var tabIcoSelectColor: Int = NO_COLOR
|
||||||
|
get() {
|
||||||
|
return if (field == NO_COLOR) tabSelectColor else field
|
||||||
|
}
|
||||||
|
|
||||||
|
/**未选中的图标颜色*/
|
||||||
|
var tabIcoDeselectColor: Int = NO_COLOR
|
||||||
|
get() {
|
||||||
|
return if (field == NO_COLOR) tabDeselectColor else field
|
||||||
|
}
|
||||||
|
|
||||||
|
/**是否开启scale渐变效果*/
|
||||||
|
var tabEnableGradientScale = false
|
||||||
|
|
||||||
|
/**最小缩放的比例*/
|
||||||
|
var tabMinScale = 0.8f
|
||||||
|
|
||||||
|
/**最大缩放的比例*/
|
||||||
|
var tabMaxScale = 1.2f
|
||||||
|
|
||||||
|
/**是否开启字体大小渐变效果*/
|
||||||
|
var tabEnableGradientTextSize = true
|
||||||
|
|
||||||
|
/**tab中文本字体未选中时的字体大小, >0时激活*/
|
||||||
|
var tabTextMinSize = -1f
|
||||||
|
|
||||||
|
/**tab中文本字体选中时的字体大小, >0时激活*/
|
||||||
|
var tabTextMaxSize = -1f
|
||||||
|
|
||||||
|
/**渐变效果实现的回调*/
|
||||||
|
var tabGradientCallback = TabGradientCallback()
|
||||||
|
|
||||||
|
/**指定文本控件的id, 所有文本属性改变, 将会发生在这个控件上.
|
||||||
|
* 如果指定的控件不存在, 控件会降权至[ItemView]*/
|
||||||
|
@IdRes
|
||||||
|
var tabTextViewId: Int = View.NO_ID
|
||||||
|
|
||||||
|
/**指定图标控件的id*/
|
||||||
|
@IdRes
|
||||||
|
var tabIconViewId: Int = View.NO_ID
|
||||||
|
|
||||||
|
/**返回用于配置文本样式的控件*/
|
||||||
|
var onGetTextStyleView: (itemView: View, index: Int) -> TextView? = { itemView, _ ->
|
||||||
|
if (tabTextViewId == View.NO_ID) {
|
||||||
|
var tv: TextView? = if (itemView is TextView) itemView else null
|
||||||
|
|
||||||
|
if (tabLayout.tabIndicator.indicatorContentIndex != -1) {
|
||||||
|
itemView.getChildOrNull(tabLayout.tabIndicator.indicatorContentIndex)?.let {
|
||||||
|
if (it is TextView) {
|
||||||
|
tv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabLayout.tabIndicator.indicatorContentId != View.NO_ID) {
|
||||||
|
itemView.findViewById<View>(tabLayout.tabIndicator.indicatorContentId)?.let {
|
||||||
|
if (it is TextView) {
|
||||||
|
tv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lp = itemView.layoutParams
|
||||||
|
if (lp is DslTabLayout.LayoutParams) {
|
||||||
|
if (lp.indicatorContentIndex != -1 && itemView is ViewGroup) {
|
||||||
|
itemView.getChildOrNull(lp.indicatorContentIndex)?.let {
|
||||||
|
if (it is TextView) {
|
||||||
|
tv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lp.indicatorContentId != View.NO_ID) {
|
||||||
|
itemView.findViewById<View>(lp.indicatorContentId)?.let {
|
||||||
|
if (it is TextView) {
|
||||||
|
tv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lp.contentTextViewIndex != -1 && itemView is ViewGroup) {
|
||||||
|
itemView.getChildOrNull(lp.contentTextViewIndex)?.let {
|
||||||
|
if (it is TextView) {
|
||||||
|
tv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lp.contentTextViewId != View.NO_ID) {
|
||||||
|
itemView.findViewById<View>(lp.contentTextViewId)?.let {
|
||||||
|
if (it is TextView) {
|
||||||
|
tv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tv
|
||||||
|
} else {
|
||||||
|
itemView.findViewById(tabTextViewId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**返回用于配置ico样式的控件*/
|
||||||
|
var onGetIcoStyleView: (itemView: View, index: Int) -> View? = { itemView, _ ->
|
||||||
|
if (tabIconViewId == View.NO_ID) {
|
||||||
|
var iv: View? = itemView
|
||||||
|
|
||||||
|
if (tabLayout.tabIndicator.indicatorContentIndex != -1) {
|
||||||
|
itemView.getChildOrNull(tabLayout.tabIndicator.indicatorContentIndex)?.let {
|
||||||
|
iv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabLayout.tabIndicator.indicatorContentId != View.NO_ID) {
|
||||||
|
itemView.findViewById<View>(tabLayout.tabIndicator.indicatorContentId)?.let {
|
||||||
|
iv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lp = itemView.layoutParams
|
||||||
|
if (lp is DslTabLayout.LayoutParams) {
|
||||||
|
if (lp.indicatorContentIndex != -1 && itemView is ViewGroup) {
|
||||||
|
iv = itemView.getChildOrNull(lp.indicatorContentIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lp.indicatorContentId != View.NO_ID) {
|
||||||
|
itemView.findViewById<View>(lp.indicatorContentId)?.let {
|
||||||
|
iv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lp.contentIconViewIndex != -1 && itemView is ViewGroup) {
|
||||||
|
iv = itemView.getChildOrNull(lp.contentIconViewIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lp.contentIconViewId != View.NO_ID) {
|
||||||
|
itemView.findViewById<View>(lp.contentIconViewId)?.let {
|
||||||
|
iv = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iv
|
||||||
|
} else {
|
||||||
|
itemView.findViewById(tabIconViewId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**获取渐变结束时,指示器的颜色.*/
|
||||||
|
var onGetGradientIndicatorColor: (fromIndex: Int, toIndex: Int, positionOffset: Float) -> Int =
|
||||||
|
{ fromIndex, toIndex, positionOffset ->
|
||||||
|
tabLayout.tabIndicator.indicatorColor
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
onStyleItemView = { itemView, index, select ->
|
||||||
|
onUpdateItemStyle(itemView, index, select)
|
||||||
|
}
|
||||||
|
onSelectIndexChange = { fromIndex, selectIndexList, reselect, fromUser ->
|
||||||
|
val toIndex = selectIndexList.last()
|
||||||
|
tabLayout._viewPagerDelegate?.onSetCurrentItem(fromIndex, toIndex, reselect, fromUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**xml属性读取*/
|
||||||
|
open fun initAttribute(context: Context, attributeSet: AttributeSet? = null) {
|
||||||
|
val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.DslTabLayout)
|
||||||
|
|
||||||
|
tabSelectColor =
|
||||||
|
typedArray.getColor(R.styleable.DslTabLayout_tab_select_color, tabSelectColor)
|
||||||
|
tabDeselectColor =
|
||||||
|
typedArray.getColor(
|
||||||
|
R.styleable.DslTabLayout_tab_deselect_color,
|
||||||
|
tabDeselectColor
|
||||||
|
)
|
||||||
|
tabIcoSelectColor =
|
||||||
|
typedArray.getColor(R.styleable.DslTabLayout_tab_ico_select_color, NO_COLOR)
|
||||||
|
tabIcoDeselectColor =
|
||||||
|
typedArray.getColor(R.styleable.DslTabLayout_tab_ico_deselect_color, NO_COLOR)
|
||||||
|
|
||||||
|
tabEnableTextColor = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_enable_text_color,
|
||||||
|
tabEnableTextColor
|
||||||
|
)
|
||||||
|
tabEnableIndicatorGradientColor = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_enable_indicator_gradient_color,
|
||||||
|
tabEnableIndicatorGradientColor
|
||||||
|
)
|
||||||
|
tabEnableGradientColor = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_enable_gradient_color,
|
||||||
|
tabEnableGradientColor
|
||||||
|
)
|
||||||
|
tabEnableIcoColor = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_enable_ico_color,
|
||||||
|
tabEnableIcoColor
|
||||||
|
)
|
||||||
|
tabEnableIcoGradientColor = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_enable_ico_gradient_color,
|
||||||
|
tabEnableIcoGradientColor
|
||||||
|
)
|
||||||
|
|
||||||
|
tabEnableTextBold = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_enable_text_bold,
|
||||||
|
tabEnableTextBold
|
||||||
|
)
|
||||||
|
|
||||||
|
tabUseTypefaceBold = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_use_typeface_bold,
|
||||||
|
tabUseTypefaceBold
|
||||||
|
)
|
||||||
|
|
||||||
|
tabEnableGradientScale = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_enable_gradient_scale,
|
||||||
|
tabEnableGradientScale
|
||||||
|
)
|
||||||
|
tabMinScale = typedArray.getFloat(R.styleable.DslTabLayout_tab_min_scale, tabMinScale)
|
||||||
|
tabMaxScale = typedArray.getFloat(R.styleable.DslTabLayout_tab_max_scale, tabMaxScale)
|
||||||
|
|
||||||
|
tabEnableGradientTextSize = typedArray.getBoolean(
|
||||||
|
R.styleable.DslTabLayout_tab_enable_gradient_text_size,
|
||||||
|
tabEnableGradientTextSize
|
||||||
|
)
|
||||||
|
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_text_min_size)) {
|
||||||
|
tabTextMinSize = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_text_min_size,
|
||||||
|
tabTextMinSize.toInt()
|
||||||
|
).toFloat()
|
||||||
|
}
|
||||||
|
if (typedArray.hasValue(R.styleable.DslTabLayout_tab_text_max_size)) {
|
||||||
|
tabTextMaxSize = typedArray.getDimensionPixelOffset(
|
||||||
|
R.styleable.DslTabLayout_tab_text_max_size,
|
||||||
|
tabTextMaxSize.toInt()
|
||||||
|
).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
tabTextViewId =
|
||||||
|
typedArray.getResourceId(R.styleable.DslTabLayout_tab_text_view_id, tabTextViewId)
|
||||||
|
tabIconViewId =
|
||||||
|
typedArray.getResourceId(R.styleable.DslTabLayout_tab_icon_view_id, tabIconViewId)
|
||||||
|
|
||||||
|
typedArray.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**更新item的样式*/
|
||||||
|
open fun onUpdateItemStyle(itemView: View, index: Int, select: Boolean) {
|
||||||
|
//"$itemView\n$index\n$select".logw()
|
||||||
|
|
||||||
|
(onGetTextStyleView(itemView, index))?.apply {
|
||||||
|
//文本加粗
|
||||||
|
paint?.apply {
|
||||||
|
if (tabEnableTextBold && select) {
|
||||||
|
//设置粗体
|
||||||
|
if (tabUseTypefaceBold) {
|
||||||
|
typeface = Typeface.defaultFromStyle(Typeface.BOLD)
|
||||||
|
} else {
|
||||||
|
flags = flags or Paint.FAKE_BOLD_TEXT_FLAG
|
||||||
|
isFakeBoldText = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//取消粗体
|
||||||
|
if (tabUseTypefaceBold) {
|
||||||
|
typeface = Typeface.defaultFromStyle(Typeface.NORMAL)
|
||||||
|
} else {
|
||||||
|
flags = flags and Paint.FAKE_BOLD_TEXT_FLAG.inv()
|
||||||
|
isFakeBoldText = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabEnableTextColor) {
|
||||||
|
//文本颜色
|
||||||
|
setTextColor(if (select) tabSelectColor else tabDeselectColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabTextMaxSize > 0 || tabTextMinSize > 0) {
|
||||||
|
//文本字体大小
|
||||||
|
val minTextSize = min(tabTextMinSize, tabTextMaxSize)
|
||||||
|
val maxTextSize = max(tabTextMinSize, tabTextMaxSize)
|
||||||
|
setTextSize(
|
||||||
|
TypedValue.COMPLEX_UNIT_PX,
|
||||||
|
if (select) maxTextSize else minTextSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabEnableIcoColor) {
|
||||||
|
onGetIcoStyleView(itemView, index)?.apply {
|
||||||
|
_updateIcoColor(this, if (select) tabIcoSelectColor else tabIcoDeselectColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabEnableGradientScale) {
|
||||||
|
itemView.scaleX = if (select) tabMaxScale else tabMinScale
|
||||||
|
itemView.scaleY = if (select) tabMaxScale else tabMinScale
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabLayout.drawBorder) {
|
||||||
|
tabLayout.tabBorder?.updateItemBackground(tabLayout, itemView, index, select)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [DslTabLayout]滚动时回调.
|
||||||
|
* */
|
||||||
|
open fun onPageIndexScrolled(fromIndex: Int, toIndex: Int, positionOffset: Float) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [onPageIndexScrolled]
|
||||||
|
* */
|
||||||
|
open fun onPageViewScrolled(fromView: View?, toView: View, positionOffset: Float) {
|
||||||
|
//"$fromView\n$toView\n$positionOffset".logi()
|
||||||
|
|
||||||
|
if (fromView != toView) {
|
||||||
|
|
||||||
|
val fromIndex = tabLayout.tabIndicator.currentIndex
|
||||||
|
val toIndex = tabLayout.tabIndicator._targetIndex
|
||||||
|
|
||||||
|
if (tabEnableIndicatorGradientColor) {
|
||||||
|
val startColor = onGetGradientIndicatorColor(fromIndex, fromIndex, 0f)
|
||||||
|
val endColor = onGetGradientIndicatorColor(fromIndex, toIndex, positionOffset)
|
||||||
|
|
||||||
|
tabLayout.tabIndicator.indicatorColor =
|
||||||
|
evaluateColor(positionOffset, startColor, endColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabEnableGradientColor) {
|
||||||
|
//文本渐变
|
||||||
|
fromView?.apply {
|
||||||
|
_gradientColor(
|
||||||
|
onGetTextStyleView(this, fromIndex),
|
||||||
|
tabSelectColor,
|
||||||
|
tabDeselectColor,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_gradientColor(
|
||||||
|
onGetTextStyleView(toView, toIndex),
|
||||||
|
tabDeselectColor,
|
||||||
|
tabSelectColor,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabEnableIcoGradientColor) {
|
||||||
|
//图标渐变
|
||||||
|
fromView?.apply {
|
||||||
|
_gradientIcoColor(
|
||||||
|
onGetIcoStyleView(this, fromIndex),
|
||||||
|
tabIcoSelectColor,
|
||||||
|
tabIcoDeselectColor,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_gradientIcoColor(
|
||||||
|
onGetIcoStyleView(toView, toIndex),
|
||||||
|
tabIcoDeselectColor,
|
||||||
|
tabIcoSelectColor,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabEnableGradientScale) {
|
||||||
|
//scale渐变
|
||||||
|
_gradientScale(fromView, tabMaxScale, tabMinScale, positionOffset)
|
||||||
|
_gradientScale(toView, tabMinScale, tabMaxScale, positionOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabEnableGradientTextSize &&
|
||||||
|
tabTextMaxSize > 0 &&
|
||||||
|
tabTextMinSize > 0 &&
|
||||||
|
tabTextMinSize != tabTextMaxSize
|
||||||
|
) {
|
||||||
|
|
||||||
|
//文本字体大小渐变
|
||||||
|
_gradientTextSize(
|
||||||
|
fromView?.run { onGetTextStyleView(this, fromIndex) },
|
||||||
|
tabTextMaxSize,
|
||||||
|
tabTextMinSize,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
_gradientTextSize(
|
||||||
|
onGetTextStyleView(toView, toIndex),
|
||||||
|
tabTextMinSize,
|
||||||
|
tabTextMaxSize,
|
||||||
|
positionOffset
|
||||||
|
)
|
||||||
|
|
||||||
|
if (toIndex == tabLayout.dslSelector.visibleViewList.lastIndex || toIndex == 0) {
|
||||||
|
tabLayout._scrollToTarget(toIndex, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun _gradientColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
|
||||||
|
tabGradientCallback.onGradientColor(view, startColor, endColor, percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun _gradientIcoColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
|
||||||
|
tabGradientCallback.onGradientIcoColor(view, startColor, endColor, percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun _gradientScale(view: View?, startScale: Float, endScale: Float, percent: Float) {
|
||||||
|
tabGradientCallback.onGradientScale(view, startScale, endScale, percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun _gradientTextSize(
|
||||||
|
view: TextView?,
|
||||||
|
startTextSize: Float,
|
||||||
|
endTextSize: Float,
|
||||||
|
percent: Float
|
||||||
|
) {
|
||||||
|
tabGradientCallback.onGradientTextSize(view, startTextSize, endTextSize, percent)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun _updateIcoColor(view: View?, color: Int) {
|
||||||
|
tabGradientCallback.onUpdateIcoColor(view, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class TabGradientCallback {
|
||||||
|
|
||||||
|
open fun onGradientColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
|
||||||
|
(view as? TextView)?.apply {
|
||||||
|
setTextColor(evaluateColor(percent, startColor, endColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onGradientIcoColor(view: View?, startColor: Int, endColor: Int, percent: Float) {
|
||||||
|
onUpdateIcoColor(view, evaluateColor(percent, startColor, endColor))
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onUpdateIcoColor(view: View?, color: Int) {
|
||||||
|
view?.tintDrawableColor(color)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onGradientScale(view: View?, startScale: Float, endScale: Float, percent: Float) {
|
||||||
|
view?.apply {
|
||||||
|
(startScale + (endScale - startScale) * percent).let {
|
||||||
|
scaleX = it
|
||||||
|
scaleY = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onGradientTextSize(
|
||||||
|
view: TextView?,
|
||||||
|
startTextSize: Float,
|
||||||
|
endTextSize: Float,
|
||||||
|
percent: Float
|
||||||
|
) {
|
||||||
|
view?.apply {
|
||||||
|
setTextSize(
|
||||||
|
TypedValue.COMPLEX_UNIT_PX,
|
||||||
|
(startTextSize + (endTextSize - startTextSize) * percent)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用来实现[DslTabIndicator]的自绘
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2022/02/21
|
||||||
|
* Copyright (c) 2020 ShenZhen Wayto Ltd. All rights reserved.
|
||||||
|
*/
|
||||||
|
interface ITabIndicatorDraw {
|
||||||
|
|
||||||
|
/**绘制指示器
|
||||||
|
* [positionOffset] 页面偏移量*/
|
||||||
|
fun onDrawTabIndicator(
|
||||||
|
tabIndicator: DslTabIndicator,
|
||||||
|
canvas: Canvas,
|
||||||
|
positionOffset: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
334
TabLayout/src/main/java/com/angcyo/tablayout/LibEx.kt
Normal file
@ -0,0 +1,334 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.Window
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
|
import androidx.core.math.MathUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/11/23
|
||||||
|
*/
|
||||||
|
internal val dpi: Int
|
||||||
|
get() = dp.toInt()
|
||||||
|
|
||||||
|
internal val dp: Float
|
||||||
|
get() = Resources.getSystem().displayMetrics.density
|
||||||
|
|
||||||
|
internal val View.dpi: Int
|
||||||
|
get() = context.resources.displayMetrics.density.toInt()
|
||||||
|
|
||||||
|
internal val View.screenWidth: Int
|
||||||
|
get() = context.resources.displayMetrics.widthPixels
|
||||||
|
|
||||||
|
internal val View.screenHeight: Int
|
||||||
|
get() = context.resources.displayMetrics.heightPixels
|
||||||
|
|
||||||
|
internal val View.viewDrawWidth: Int
|
||||||
|
get() = measuredWidth - paddingLeft - paddingRight
|
||||||
|
|
||||||
|
internal val View.viewDrawHeight: Int
|
||||||
|
get() = measuredHeight - paddingTop - paddingBottom
|
||||||
|
|
||||||
|
/**Match_Parent*/
|
||||||
|
internal fun exactlyMeasure(size: Int): Int =
|
||||||
|
View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY)
|
||||||
|
|
||||||
|
internal fun exactlyMeasure(size: Float): Int = exactlyMeasure(size.toInt())
|
||||||
|
|
||||||
|
/**Wrap_Content*/
|
||||||
|
internal fun atmostMeasure(size: Int): Int =
|
||||||
|
View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.AT_MOST)
|
||||||
|
|
||||||
|
internal fun Int.have(value: Int): Boolean = if (this == 0 || value == 0) {
|
||||||
|
false
|
||||||
|
} else if (this == 0 && value == 0) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
((this > 0 && value > 0) || (this < 0 && value < 0)) && this and value == value
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Int.remove(value: Int): Int = this and value.inv()
|
||||||
|
|
||||||
|
internal fun clamp(value: Float, min: Float, max: Float): Float {
|
||||||
|
if (value < min) {
|
||||||
|
return min
|
||||||
|
} else if (value > max) {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun clamp(value: Int, min: Int, max: Int): Int {
|
||||||
|
if (value < min) {
|
||||||
|
return min
|
||||||
|
} else if (value > max) {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Any.logi() {
|
||||||
|
Log.i("DslTabLayout", "$this")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Any.logw() {
|
||||||
|
Log.w("DslTabLayout", "$this")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Any.loge() {
|
||||||
|
Log.e("DslTabLayout", "$this")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun View.calcLayoutWidthHeight(
|
||||||
|
rLayoutWidth: String?, rLayoutHeight: String?,
|
||||||
|
parentWidth: Int, parentHeight: Int,
|
||||||
|
rLayoutWidthExclude: Int = 0, rLayoutHeightExclude: Int = 0
|
||||||
|
): IntArray {
|
||||||
|
val size = intArrayOf(-1, -1)
|
||||||
|
if (TextUtils.isEmpty(rLayoutWidth) && TextUtils.isEmpty(rLayoutHeight)) {
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(rLayoutWidth)) {
|
||||||
|
if (rLayoutWidth!!.contains("sw", true)) {
|
||||||
|
val ratio = rLayoutWidth.replace("sw", "", true).toFloatOrNull()
|
||||||
|
ratio?.let {
|
||||||
|
size[0] = (ratio * (screenWidth - rLayoutWidthExclude)).toInt()
|
||||||
|
}
|
||||||
|
} else if (rLayoutWidth!!.contains("pw", true)) {
|
||||||
|
val ratio = rLayoutWidth.replace("pw", "", true).toFloatOrNull()
|
||||||
|
ratio?.let {
|
||||||
|
size[0] = (ratio * (parentWidth - rLayoutWidthExclude)).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(rLayoutHeight)) {
|
||||||
|
if (rLayoutHeight!!.contains("sh", true)) {
|
||||||
|
val ratio = rLayoutHeight.replace("sh", "", true).toFloatOrNull()
|
||||||
|
ratio?.let {
|
||||||
|
size[1] = (ratio * (screenHeight - rLayoutHeightExclude)).toInt()
|
||||||
|
}
|
||||||
|
} else if (rLayoutHeight!!.contains("ph", true)) {
|
||||||
|
val ratio = rLayoutHeight.replace("ph", "", true).toFloatOrNull()
|
||||||
|
ratio?.let {
|
||||||
|
size[1] = (ratio * (parentHeight - rLayoutHeightExclude)).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun evaluateColor(fraction: Float /*0-1*/, startColor: Int, endColor: Int): Int {
|
||||||
|
val fr = MathUtils.clamp(fraction, 0f, 1f)
|
||||||
|
val startA = startColor shr 24 and 0xff
|
||||||
|
val startR = startColor shr 16 and 0xff
|
||||||
|
val startG = startColor shr 8 and 0xff
|
||||||
|
val startB = startColor and 0xff
|
||||||
|
val endA = endColor shr 24 and 0xff
|
||||||
|
val endR = endColor shr 16 and 0xff
|
||||||
|
val endG = endColor shr 8 and 0xff
|
||||||
|
val endB = endColor and 0xff
|
||||||
|
return startA + (fr * (endA - startA)).toInt() shl 24 or
|
||||||
|
(startR + (fr * (endR - startR)).toInt() shl 16) or
|
||||||
|
(startG + (fr * (endG - startG)).toInt() shl 8) or
|
||||||
|
startB + (fr * (endB - startB)).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Drawable?.tintDrawableColor(color: Int): Drawable? {
|
||||||
|
|
||||||
|
if (this == null) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
val wrappedDrawable =
|
||||||
|
DrawableCompat.wrap(this).mutate()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
DrawableCompat.setTint(wrappedDrawable, color)
|
||||||
|
} else {
|
||||||
|
wrappedDrawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrappedDrawable
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun View?.tintDrawableColor(color: Int) {
|
||||||
|
when (this) {
|
||||||
|
is TextView -> {
|
||||||
|
val drawables = arrayOfNulls<Drawable?>(4)
|
||||||
|
compoundDrawables.forEachIndexed { index, drawable ->
|
||||||
|
drawables[index] = drawable?.tintDrawableColor(color)
|
||||||
|
}
|
||||||
|
setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3])
|
||||||
|
}
|
||||||
|
is ImageView -> {
|
||||||
|
setImageDrawable(drawable?.tintDrawableColor(color))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Paint?.textWidth(text: String?): Float {
|
||||||
|
if (TextUtils.isEmpty(text)) {
|
||||||
|
return 0f
|
||||||
|
}
|
||||||
|
return this?.run {
|
||||||
|
measureText(text)
|
||||||
|
} ?: 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Paint?.textHeight(): Float = this?.run { descent() - ascent() } ?: 0f
|
||||||
|
|
||||||
|
internal fun View.getChildOrNull(index: Int): View? {
|
||||||
|
return if (this is ViewGroup) {
|
||||||
|
return if (index in 0 until childCount) {
|
||||||
|
getChildAt(index)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**获取[View]在指定[parent]中的矩形坐标*/
|
||||||
|
internal fun View.getLocationInParent(parentView: View? = null, result: Rect = Rect()): Rect {
|
||||||
|
val parent: View? = parentView ?: (parent as? View)
|
||||||
|
|
||||||
|
if (parent == null) {
|
||||||
|
getViewRect(result)
|
||||||
|
} else {
|
||||||
|
result.set(0, 0, 0, 0)
|
||||||
|
if (this != parent) {
|
||||||
|
fun doIt(view: View, parent: View, rect: Rect) {
|
||||||
|
val viewParent = view.parent
|
||||||
|
if (viewParent is View) {
|
||||||
|
rect.left += view.left
|
||||||
|
rect.top += view.top
|
||||||
|
if (viewParent != parent) {
|
||||||
|
doIt(viewParent, parent, rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doIt(this, parent, result)
|
||||||
|
}
|
||||||
|
result.right = result.left + this.measuredWidth
|
||||||
|
result.bottom = result.top + this.measuredHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取View, 相对于手机屏幕的矩形
|
||||||
|
* */
|
||||||
|
internal fun View.getViewRect(result: Rect = Rect()): Rect {
|
||||||
|
var offsetX = 0
|
||||||
|
var offsetY = 0
|
||||||
|
|
||||||
|
//横屏, 并且显示了虚拟导航栏的时候. 需要左边偏移
|
||||||
|
//只计算一次
|
||||||
|
(context as? Activity)?.let {
|
||||||
|
it.window.decorView.getGlobalVisibleRect(result)
|
||||||
|
if (result.width() > result.height()) {
|
||||||
|
//横屏了
|
||||||
|
offsetX = navBarHeight(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getViewRect(offsetX, offsetY, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取View, 相对于手机屏幕的矩形, 带皮阿尼一
|
||||||
|
* */
|
||||||
|
internal fun View.getViewRect(offsetX: Int, offsetY: Int, result: Rect = Rect()): Rect {
|
||||||
|
//可见位置的坐标, 超出屏幕的距离会被剃掉
|
||||||
|
//getGlobalVisibleRect(r)
|
||||||
|
val r2 = IntArray(2)
|
||||||
|
//val r3 = IntArray(2)
|
||||||
|
//相对于屏幕的坐标
|
||||||
|
getLocationOnScreen(r2)
|
||||||
|
//相对于窗口的坐标
|
||||||
|
//getLocationInWindow(r3)
|
||||||
|
|
||||||
|
val left = r2[0] + offsetX
|
||||||
|
val top = r2[1] + offsetY
|
||||||
|
|
||||||
|
result.set(left, top, left + measuredWidth, top + measuredHeight)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导航栏的高度(如果显示了)
|
||||||
|
*/
|
||||||
|
internal fun navBarHeight(context: Context): Int {
|
||||||
|
var result = 0
|
||||||
|
|
||||||
|
if (context is Activity) {
|
||||||
|
val decorRect = Rect()
|
||||||
|
val windowRect = Rect()
|
||||||
|
|
||||||
|
context.window.decorView.getGlobalVisibleRect(decorRect)
|
||||||
|
context.window.findViewById<View>(Window.ID_ANDROID_CONTENT)
|
||||||
|
.getGlobalVisibleRect(windowRect)
|
||||||
|
|
||||||
|
if (decorRect.width() > decorRect.height()) {
|
||||||
|
//横屏
|
||||||
|
result = decorRect.width() - windowRect.width()
|
||||||
|
} else {
|
||||||
|
//竖屏
|
||||||
|
result = decorRect.bottom - windowRect.bottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Collection<*>?.size() = this?.size ?: 0
|
||||||
|
|
||||||
|
/**判断2个列表中的数据是否改变过*/
|
||||||
|
internal fun <T> List<T>?.isChange(other: List<T>?): Boolean {
|
||||||
|
if (this.size() != other.size()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
this?.forEachIndexed { index, t ->
|
||||||
|
if (t != other?.getOrNull(index)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Int.isHorizontal() = this == LinearLayout.HORIZONTAL
|
||||||
|
|
||||||
|
fun Int.isVertical() = this == LinearLayout.VERTICAL
|
||||||
|
|
||||||
|
internal fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = true): View {
|
||||||
|
if (layoutId == -1) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
val rootView = LayoutInflater.from(context).inflate(layoutId, this, false)
|
||||||
|
if (attachToRoot) {
|
||||||
|
addView(rootView)
|
||||||
|
}
|
||||||
|
return rootView
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.angcyo.tablayout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不依赖ViewPager和ViewPager2
|
||||||
|
* Email:angcyo@126.com
|
||||||
|
* @author angcyo
|
||||||
|
* @date 2019/12/14
|
||||||
|
*/
|
||||||
|
interface ViewPagerDelegate {
|
||||||
|
companion object {
|
||||||
|
const val SCROLL_STATE_IDLE = 0
|
||||||
|
const val SCROLL_STATE_DRAGGING = 1
|
||||||
|
const val SCROLL_STATE_SETTLING = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/**获取当前页面索引*/
|
||||||
|
fun onGetCurrentItem(): Int
|
||||||
|
|
||||||
|
/**设置当前的页面*/
|
||||||
|
fun onSetCurrentItem(fromIndex: Int, toIndex: Int, reselect: Boolean, fromUser: Boolean)
|
||||||
|
}
|
299
TabLayout/src/main/res/values/attr_dsl_tab_layout.xml
Normal file
@ -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
@ -0,0 +1 @@
|
|||||||
|
/build
|
31
ViewPager2Delegate/build.gradle
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 33
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 28
|
||||||
|
targetSdkVersion 33
|
||||||
|
|
||||||
|
consumerProguardFiles 'consumer-rules.pro'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namespace 'com.angcyo.tablayout.delegate2'
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
//https://github.com/angcyo/DslTabLayout
|
||||||
|
implementation project(':TabLayout')
|
||||||
|
//https://mvnrepository.com/artifact/androidx.viewpager2/viewpager2
|
||||||
|
implementation 'androidx.viewpager2:viewpager2:1.0.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
//apply from: "$gradleHost/master/publish.gradle"
|
0
ViewPager2Delegate/consumer-rules.pro
Normal file
21
ViewPager2Delegate/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
1
ViewPager2Delegate/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
1
app/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
74
app/build.gradle
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.application'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
signingConfigs {
|
||||||
|
debug {
|
||||||
|
storeFile file('C:\\Users\\58381\\Documents\\AndroidSign\\yutou.jks')
|
||||||
|
storePassword '34864394'
|
||||||
|
keyAlias 'yutou'
|
||||||
|
keyPassword '34864394'
|
||||||
|
}
|
||||||
|
yutou {
|
||||||
|
storeFile file('C:\\Users\\58381\\Documents\\AndroidSign\\yutou.jks')
|
||||||
|
storePassword '34864394'
|
||||||
|
keyAlias 'yutou'
|
||||||
|
keyPassword '34864394'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileSdkVersion 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.yutou.passmanage"
|
||||||
|
minSdkVersion 28
|
||||||
|
targetSdkVersion 34
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.3"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
signingConfig signingConfigs.yutou
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
namespace 'com.yutou.passmanage'
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
|
implementation 'com.google.android.material:material:1.12.0'
|
||||||
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
implementation 'androidx.navigation:navigation-fragment:2.7.7'
|
||||||
|
implementation 'androidx.navigation:navigation-ui:2.7.7'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
|
||||||
|
testImplementation 'junit:junit:4.13.2'
|
||||||
|
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||||
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
|
||||||
|
|
||||||
|
implementation 'com.alibaba.fastjson2:fastjson2:2.0.52'
|
||||||
|
|
||||||
|
implementation "androidx.room:room-runtime:2.6.1"
|
||||||
|
annotationProcessor "androidx.room:room-compiler:2.6.1"
|
||||||
|
implementation "androidx.biometric:biometric:1.1.0"
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||||
|
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
|
||||||
|
|
||||||
|
api project(path:':TabLayout')
|
||||||
|
api project(path:':ViewPager2Delegate')
|
||||||
|
api project(path:':netlibs')
|
||||||
|
|
||||||
|
}
|
21
app/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.yutou.passmanage;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
public void useAppContext() {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
assertEquals("com.yutou.passmanage", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
25
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/app_icon"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/Theme.MyPassworldManage"
|
||||||
|
android:usesCleartextTraffic="true" >
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
BIN
app/src/main/app_icon-playstore.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
@ -0,0 +1,265 @@
|
|||||||
|
package com.yutou.passmanage.Adapters;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.yutou.netlibs.http.HttpCallback;
|
||||||
|
import com.yutou.passmanage.Datas.ToolsPassword;
|
||||||
|
import com.yutou.passmanage.Datas.ToolsPasswordDao;
|
||||||
|
import com.yutou.passmanage.Interfaces.NetworkInterface;
|
||||||
|
import com.yutou.passmanage.NetUtils.NetTools;
|
||||||
|
import com.yutou.passmanage.R;
|
||||||
|
import com.yutou.passmanage.Tools.NetworkTool;
|
||||||
|
import com.yutou.passmanage.Tools.ResTools;
|
||||||
|
import com.yutou.passmanage.Tools.RoomDatabaseManager;
|
||||||
|
import com.yutou.passmanage.bean.BaseBean;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.yutou.passmanage.Tools.RoomDatabaseManager.addPassword;
|
||||||
|
|
||||||
|
public class PassWordListAdapter extends RecyclerView.Adapter<PassWordListAdapter.MyViewHolder> {
|
||||||
|
private Context context;
|
||||||
|
private List<ToolsPassword> list;
|
||||||
|
ToolsPasswordDao dao;
|
||||||
|
|
||||||
|
public PassWordListAdapter(Context context, List<ToolsPassword> list) {
|
||||||
|
this.context = context;
|
||||||
|
this.list = list;
|
||||||
|
this.dao = RoomDatabaseManager.build(context).passwordDao();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setList(List<ToolsPassword> list) {
|
||||||
|
this.list = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
return new MyViewHolder(LayoutInflater.from(context).inflate(R.layout.recycler_list, parent, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
|
||||||
|
ToolsPassword data = list.get(holder.getAdapterPosition());
|
||||||
|
String account = ResTools.getString(context, R.string.account) + ":" + data.getAccount();
|
||||||
|
String password = ResTools.getString(context, R.string.password) + ":" + data.getPassword();
|
||||||
|
holder.name.setText(data.getTitle());
|
||||||
|
holder.user.setText(account);
|
||||||
|
holder.password.setText(password);
|
||||||
|
holder.copy.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
NetTools.getApi().getPassword(data.getId()).enqueue(new HttpCallback<String>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(int code, String status, String response) {
|
||||||
|
ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
|
ClipData clip = ClipData.newPlainText("", response);
|
||||||
|
clipboard.setPrimaryClip(clip);
|
||||||
|
Toast.makeText(context, ResTools.getString(context, R.string.toast_copy_password), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable throwable) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
holder.item_layout.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
NetTools.getApi().getPassword(data.getId()).enqueue(new HttpCallback<String>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(int code, String status, String response) {
|
||||||
|
data.setPassword(response);
|
||||||
|
showDialog(data, holder.getAdapterPosition());
|
||||||
|
save(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable throwable) {
|
||||||
|
new Thread(() -> {
|
||||||
|
ToolsPassword queried = dao.queryPassword(data.getId());
|
||||||
|
if (queried != null) {
|
||||||
|
new Handler(Looper.getMainLooper())
|
||||||
|
.post(() -> showDialog(data, holder.getAdapterPosition()));
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(ToolsPassword pwd) {
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ToolsPassword queried = dao.queryPassword(pwd.getId());
|
||||||
|
if (queried == null) {
|
||||||
|
dao.insert(pwd);
|
||||||
|
} else {
|
||||||
|
dao.update(pwd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog removeDialog = null;
|
||||||
|
|
||||||
|
private void showDialog(ToolsPassword data, int position) {
|
||||||
|
View view = LayoutInflater.from(context.getApplicationContext()).inflate(R.layout.dialog_add_passworld, null);
|
||||||
|
Button delete = view.findViewById(R.id.delete);
|
||||||
|
EditText title, account, password, info;
|
||||||
|
title = view.findViewById(R.id.title);
|
||||||
|
account = view.findViewById(R.id.account);
|
||||||
|
password = view.findViewById(R.id.myPassword);
|
||||||
|
info = view.findViewById(R.id.info);
|
||||||
|
title.setText(data.getTitle());
|
||||||
|
account.setText(data.getAccount());
|
||||||
|
password.setText(data.getPassword());
|
||||||
|
info.setText(data.getInfo());
|
||||||
|
delete.setVisibility(View.VISIBLE);
|
||||||
|
delete.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
AlertDialog dialog = new AlertDialog.Builder(context)
|
||||||
|
.setTitle("确认删除操作")
|
||||||
|
.setMessage("确定删除这条密码吗?本操作不可反悔")
|
||||||
|
.setPositiveButton("删除", new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
NetTools.getApi().removePassword(data.getId()).enqueue(new HttpCallback<BaseBean>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(int code, String status, BaseBean response) {
|
||||||
|
remove(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable throwable) {
|
||||||
|
remove(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(boolean isError) {
|
||||||
|
if (isError) {
|
||||||
|
if (data.isUpload()) {
|
||||||
|
data.setRemove(true);
|
||||||
|
dao.update(data);
|
||||||
|
} else {
|
||||||
|
dao.delete(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dao.delete(data);
|
||||||
|
}
|
||||||
|
Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
handler.post(() -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
removeDialog.dismiss();
|
||||||
|
Toast.makeText(context, "删除成功", Toast.LENGTH_LONG).show();
|
||||||
|
list.remove(position);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", (dialog13, which) -> dialog13.dismiss()).create();
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
removeDialog = new AlertDialog.Builder(context)
|
||||||
|
.setTitle("查看信息")
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("更新", (dialog1, which) -> {
|
||||||
|
addPassword(title.getText().toString(), account.getText().toString(), password.getText().toString(), info.getText().toString(), data.getId(), false, new NetworkInterface() {
|
||||||
|
@Override
|
||||||
|
public void httpGetData(Object data, int state) {
|
||||||
|
addDatabase(title.getText().toString(),
|
||||||
|
account.getText().toString(),
|
||||||
|
password.getText().toString(),
|
||||||
|
info.getText().toString(),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void httpError(Exception e) {
|
||||||
|
addDatabase(title.getText().toString(),
|
||||||
|
account.getText().toString(),
|
||||||
|
password.getText().toString(),
|
||||||
|
info.getText().toString(),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addDatabase(String title1, String account1, String password1, String info, boolean upload) {
|
||||||
|
new Thread(() -> {
|
||||||
|
data.setAccount(account1);
|
||||||
|
data.setTitle(title1);
|
||||||
|
data.setPassword(password1);
|
||||||
|
data.setInfo(info);
|
||||||
|
data.setUpload(upload);
|
||||||
|
dao.update(data);
|
||||||
|
Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(context, ResTools.getString(context, R.string.toast_update), Toast.LENGTH_LONG).show();
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog1.dismiss();
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", (dialog12, which) -> dialog12.dismiss()).create();
|
||||||
|
removeDialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return list.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MyViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
ImageButton icon;
|
||||||
|
TextView name, user, password;
|
||||||
|
Button copy;
|
||||||
|
LinearLayout item_layout;
|
||||||
|
|
||||||
|
public MyViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
icon = itemView.findViewById(R.id.icon);
|
||||||
|
name = itemView.findViewById(R.id.name);
|
||||||
|
user = itemView.findViewById(R.id.user);
|
||||||
|
password = itemView.findViewById(R.id.password);
|
||||||
|
copy = itemView.findViewById(R.id.copy);
|
||||||
|
item_layout = itemView.findViewById(R.id.item_layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.yutou.passmanage.Datas;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
public class AppData {
|
||||||
|
public static Handler handler=new Handler(Looper.getMainLooper());
|
||||||
|
public static String key;
|
||||||
|
}
|
134
app/src/main/java/com/yutou/passmanage/Datas/ToolsPassword.java
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package com.yutou.passmanage.Datas;
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.Ignore;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.annotation.JSONField;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tools_password
|
||||||
|
* @author
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
public class ToolsPassword{
|
||||||
|
@PrimaryKey
|
||||||
|
private Integer id;
|
||||||
|
@ColumnInfo
|
||||||
|
private String title;
|
||||||
|
@ColumnInfo
|
||||||
|
@JSONField(name = "username")
|
||||||
|
private String account;
|
||||||
|
@ColumnInfo
|
||||||
|
private String password;
|
||||||
|
@Ignore
|
||||||
|
private String url;
|
||||||
|
@ColumnInfo
|
||||||
|
private String info;
|
||||||
|
@ColumnInfo
|
||||||
|
private Integer type;
|
||||||
|
@Ignore
|
||||||
|
private Integer uid;
|
||||||
|
@ColumnInfo
|
||||||
|
private boolean upload;
|
||||||
|
@ColumnInfo
|
||||||
|
private boolean remove;
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccount() {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccount(String account) {
|
||||||
|
this.account = account;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Integer getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(Integer type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUid(Integer uid) {
|
||||||
|
this.uid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUpload() {
|
||||||
|
return upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpload(boolean upload) {
|
||||||
|
this.upload = upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRemove() {
|
||||||
|
return remove;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemove(boolean remove) {
|
||||||
|
this.remove = remove;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ToolsPassword{" +
|
||||||
|
"id=" + id +
|
||||||
|
", title='" + title + '\'' +
|
||||||
|
", account='" + account + '\'' +
|
||||||
|
", password='" + password + '\'' +
|
||||||
|
", url='" + url + '\'' +
|
||||||
|
", info='" + info + '\'' +
|
||||||
|
", type=" + type +
|
||||||
|
", uid=" + uid +
|
||||||
|
", upload=" + upload +
|
||||||
|
", remove=" + remove +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.yutou.passmanage.Datas;
|
||||||
|
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Delete;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Update;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface ToolsPasswordDao {
|
||||||
|
@Query("select * from ToolsPassword")
|
||||||
|
List<ToolsPassword> getAllAndRemove();
|
||||||
|
@Query("select * from ToolsPassword where remove=0")
|
||||||
|
List<ToolsPassword> getAll();
|
||||||
|
@Query("select * from ToolsPassword where title=:title and account=:account and password=:password and info=:info")
|
||||||
|
ToolsPassword isExist(String title,String account,String password,String info);
|
||||||
|
@Query("select * from ToolsPassword where id=:id")
|
||||||
|
ToolsPassword queryPassword(int id);
|
||||||
|
@Query("select * from ToolsPassword where title like :title and remove=0")
|
||||||
|
List<ToolsPassword> queryPassword(String title);
|
||||||
|
@Insert
|
||||||
|
void insert(ToolsPassword password);
|
||||||
|
@Insert
|
||||||
|
void insertAll(List<ToolsPassword> list);
|
||||||
|
@Delete
|
||||||
|
void delete(ToolsPassword password);
|
||||||
|
@Update
|
||||||
|
void update(ToolsPassword password);
|
||||||
|
@Query("DELETE FROM ToolsPassword")
|
||||||
|
void clear();
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
package com.yutou.passmanage.Datas;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tools_password_type
|
||||||
|
* @author
|
||||||
|
*/
|
||||||
|
public class ToolsPasswordType {
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
private Integer uid;
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Integer id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUid(Integer uid) {
|
||||||
|
this.uid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.yutou.passmanage.Interfaces;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载接口
|
||||||
|
*/
|
||||||
|
public abstract class DownloadInterface {
|
||||||
|
/**
|
||||||
|
* 下载完成
|
||||||
|
* @param oldJar
|
||||||
|
*/
|
||||||
|
public abstract void onDownloadOver(File oldJar);
|
||||||
|
public void onError(Exception e){};
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.yutou.passmanage.Interfaces;
|
||||||
|
|
||||||
|
public abstract class NetworkInterface<T> {
|
||||||
|
/**
|
||||||
|
* 请求成功
|
||||||
|
* @param data 请求参数
|
||||||
|
* @param state http状态
|
||||||
|
*/
|
||||||
|
public void httpGetData(T data, int state){}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求异常
|
||||||
|
* @param e 异常
|
||||||
|
*/
|
||||||
|
public void httpError(Exception e){}
|
||||||
|
}
|
339
app/src/main/java/com/yutou/passmanage/MainActivity.java
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
package com.yutou.passmanage;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.angcyo.tablayout.DslTabLayout;
|
||||||
|
import com.angcyo.tablayout.DslTabLayoutConfig;
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
import com.yutou.netlibs.http.HttpCallback;
|
||||||
|
import com.yutou.passmanage.Adapters.PassWordListAdapter;
|
||||||
|
import com.yutou.passmanage.Datas.AppData;
|
||||||
|
import com.yutou.passmanage.Datas.ToolsPassword;
|
||||||
|
import com.yutou.passmanage.Datas.ToolsPasswordDao;
|
||||||
|
import com.yutou.passmanage.Interfaces.NetworkInterface;
|
||||||
|
import com.yutou.passmanage.NetUtils.NetTools;
|
||||||
|
import com.yutou.passmanage.Tools.NetworkTool;
|
||||||
|
import com.yutou.passmanage.Tools.RoomDatabaseManager;
|
||||||
|
import com.yutou.passmanage.Tools.Tools;
|
||||||
|
import com.yutou.passmanage.bean.BaseBean;
|
||||||
|
import com.yutou.passmanage.bean.PasswordBean;
|
||||||
|
import com.yutou.passmanage.bean.PasswordTypeBean;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.biometric.BiometricManager;
|
||||||
|
import androidx.biometric.BiometricPrompt;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK;
|
||||||
|
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
|
||||||
|
import static com.yutou.passmanage.Tools.RoomDatabaseManager.addPassword;
|
||||||
|
|
||||||
|
import kotlin.Unit;
|
||||||
|
import kotlin.jvm.functions.Function1;
|
||||||
|
import kotlin.jvm.functions.Function4;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
EditText search;
|
||||||
|
ImageView clearSearch;
|
||||||
|
RecyclerView passList;
|
||||||
|
FloatingActionButton floatButton;
|
||||||
|
PassWordListAdapter adapter;
|
||||||
|
TextView authError;
|
||||||
|
DslTabLayout tabLayout;
|
||||||
|
Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
initView();
|
||||||
|
auth();
|
||||||
|
}
|
||||||
|
|
||||||
|
BiometricPrompt prompt;
|
||||||
|
|
||||||
|
void auth() {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
init();
|
||||||
|
authError.setVisibility(View.GONE);
|
||||||
|
passList.setVisibility(View.VISIBLE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BiometricManager manager = BiometricManager.from(this);
|
||||||
|
if (manager.canAuthenticate(BIOMETRIC_WEAK | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS) {
|
||||||
|
|
||||||
|
prompt = new BiometricPrompt(this, ContextCompat.getMainExecutor(this), new BiometricPrompt.AuthenticationCallback() {
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
|
||||||
|
super.onAuthenticationSucceeded(result);
|
||||||
|
Toast.makeText(MainActivity.this, "验证成功", Toast.LENGTH_LONG).show();
|
||||||
|
init();
|
||||||
|
authError.setVisibility(View.GONE);
|
||||||
|
passList.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationFailed() {
|
||||||
|
super.onAuthenticationFailed();
|
||||||
|
Toast.makeText(MainActivity.this, "验证失败,已退出", Toast.LENGTH_LONG).show();
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
|
||||||
|
super.onAuthenticationError(errorCode, errString);
|
||||||
|
System.out.println("errorCode = " + errorCode + ", errString = " + errString);
|
||||||
|
if (errorCode == 10) {
|
||||||
|
authError.setText("未授权,点击授权");
|
||||||
|
authError.setVisibility(View.VISIBLE);
|
||||||
|
passList.setVisibility(View.GONE);
|
||||||
|
} else if (errorCode == 13) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
|
||||||
|
.setTitle("验证指纹")
|
||||||
|
.setSubtitle("使用指纹来解锁应用")
|
||||||
|
.setAllowedAuthenticators(BIOMETRIC_WEAK | DEVICE_CREDENTIAL)
|
||||||
|
.build();
|
||||||
|
prompt.authenticate(promptInfo);
|
||||||
|
System.out.println("调用指纹");
|
||||||
|
} else {
|
||||||
|
|
||||||
|
Toast.makeText(this, "必须在支持指纹的设备上使用", Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void exit() {
|
||||||
|
MainActivity.this.finish();
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void initView() {
|
||||||
|
dao = RoomDatabaseManager.build(this).passwordDao();
|
||||||
|
adapter = new PassWordListAdapter(this, new ArrayList<>());
|
||||||
|
search = findViewById(R.id.search);
|
||||||
|
authError = findViewById(R.id.goneText);
|
||||||
|
clearSearch = findViewById(R.id.clearSearch);
|
||||||
|
passList = findViewById(R.id.passList);
|
||||||
|
floatButton = findViewById(R.id.floatButton);
|
||||||
|
tabLayout = findViewById(R.id.tabLayout);
|
||||||
|
floatButton.setOnClickListener(v -> showAddPasswordDialog());
|
||||||
|
passList.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
passList.setAdapter(adapter);
|
||||||
|
clearSearch.setOnClickListener(v -> {
|
||||||
|
search.setText("");
|
||||||
|
new Thread(this::showData).start();
|
||||||
|
});
|
||||||
|
authError.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
auth();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
search.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
new Thread(() -> {
|
||||||
|
List<ToolsPassword> list;
|
||||||
|
if (Tools.isEmpty(s.toString())) {
|
||||||
|
list = dao.getAll();
|
||||||
|
} else {
|
||||||
|
list = dao.queryPassword("%" + s.toString() + "%");
|
||||||
|
}
|
||||||
|
handler.post(() -> {
|
||||||
|
adapter.setList(list);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
});
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tabLayout.configTabLayoutConfig(new Function1<DslTabLayoutConfig, Unit>() {
|
||||||
|
@Override
|
||||||
|
public Unit invoke(DslTabLayoutConfig dslTabLayoutConfig) {
|
||||||
|
dslTabLayoutConfig.setOnSelectItemView(new Function4<View, Integer, Boolean, Boolean, Boolean>() {
|
||||||
|
@Override
|
||||||
|
public Boolean invoke(View view, Integer integer, Boolean aBoolean, Boolean aBoolean2) {
|
||||||
|
if (aBoolean) {
|
||||||
|
PasswordTypeBean typeBean = (PasswordTypeBean) view.getTag();
|
||||||
|
NetTools.getApi().getTypePassword(typeBean.getId()).enqueue(new HttpCallback<List<ToolsPassword>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(int code, String status, List<ToolsPassword> response) {
|
||||||
|
setShow(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable throwable) {
|
||||||
|
throwable.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void showAddPasswordDialog() {
|
||||||
|
View view = LayoutInflater.from(getApplicationContext()).inflate(R.layout.dialog_add_passworld, null);
|
||||||
|
EditText title, account, password, info;
|
||||||
|
title = view.findViewById(R.id.title);
|
||||||
|
account = view.findViewById(R.id.account);
|
||||||
|
password = view.findViewById(R.id.myPassword);
|
||||||
|
info = view.findViewById(R.id.info);
|
||||||
|
AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
|
||||||
|
.setTitle("添加账号")
|
||||||
|
.setView(view)
|
||||||
|
.setPositiveButton("保存", (dialog1, which) -> {
|
||||||
|
addPassword(title.getText().toString(), account.getText().toString(), password.getText().toString(), info.getText().toString(), -1, true, new NetworkInterface() {
|
||||||
|
@Override
|
||||||
|
public void httpGetData(Object data, int state) {
|
||||||
|
addDatabase(title.getText().toString(),
|
||||||
|
account.getText().toString(),
|
||||||
|
password.getText().toString(),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void httpError(Exception e) {
|
||||||
|
addDatabase(title.getText().toString(),
|
||||||
|
account.getText().toString(),
|
||||||
|
password.getText().toString(),
|
||||||
|
false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addDatabase(String title1, String account1, String password1, boolean upload) {
|
||||||
|
new Thread(() -> {
|
||||||
|
ToolsPassword psd = new ToolsPassword();
|
||||||
|
psd.setTitle(title1);
|
||||||
|
psd.setAccount(account1);
|
||||||
|
psd.setPassword(password1);
|
||||||
|
psd.setUpload(upload);
|
||||||
|
dao.insert(psd);
|
||||||
|
showData();
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dialog1.dismiss();
|
||||||
|
})
|
||||||
|
.setNegativeButton("取消", (dialog12, which) -> dialog12.dismiss()).create();
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void init() {
|
||||||
|
AppData.key = Tools.getConfig("key", this);
|
||||||
|
if (Tools.isEmpty(AppData.key)) {
|
||||||
|
EditText key = new EditText(this);
|
||||||
|
key.setHint("激活码");
|
||||||
|
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||||
|
.setTitle("输入激活码")
|
||||||
|
.setView(key)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton("确定", (dialog1, which) -> {
|
||||||
|
String k = key.getText().toString().trim();
|
||||||
|
if (Tools.isEmpty(k)) {
|
||||||
|
Toast.makeText(MainActivity.this, "激活码不能为空", Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Tools.setConfig("key", k, MainActivity.this);
|
||||||
|
AppData.key = k;
|
||||||
|
dialog1.dismiss();
|
||||||
|
initData();
|
||||||
|
}).create();
|
||||||
|
dialog.show();
|
||||||
|
} else {
|
||||||
|
initData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolsPasswordDao dao;
|
||||||
|
|
||||||
|
void initData() {
|
||||||
|
NetTools.getApi().getAllTypePassword().enqueue(new HttpCallback<List<PasswordTypeBean>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(int code, String status, List<PasswordTypeBean> response) {
|
||||||
|
for (PasswordTypeBean bean : response) {
|
||||||
|
Button tab = new Button(MainActivity.this);
|
||||||
|
tab.setText(bean.getTitle());
|
||||||
|
tab.setTag(bean);
|
||||||
|
tabLayout.addView(tab);
|
||||||
|
}
|
||||||
|
showLocalData();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable throwable) {
|
||||||
|
showLocalData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
if (dao.getAll().isEmpty()) {
|
||||||
|
authError.setText("列表为空,点击右下角+新增配置");
|
||||||
|
authError.setVisibility(View.VISIBLE);
|
||||||
|
passList.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLocalData() {
|
||||||
|
showData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setShow(List<ToolsPassword> data) {
|
||||||
|
adapter.setList(data);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
void showData() {
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
List<ToolsPassword> list = dao.getAll();
|
||||||
|
handler.post(() -> {
|
||||||
|
adapter.setList(list);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
48
app/src/main/java/com/yutou/passmanage/NetUtils/NetApi.java
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package com.yutou.passmanage.NetUtils;
|
||||||
|
|
||||||
|
import com.yutou.netlibs.http.HttpBody;
|
||||||
|
import com.yutou.passmanage.Datas.ToolsPassword;
|
||||||
|
import com.yutou.passmanage.bean.BaseBean;
|
||||||
|
import com.yutou.passmanage.bean.PasswordTypeBean;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.http.Field;
|
||||||
|
import retrofit2.http.FormUrlEncoded;
|
||||||
|
import retrofit2.http.GET;
|
||||||
|
import retrofit2.http.POST;
|
||||||
|
import retrofit2.http.Query;
|
||||||
|
|
||||||
|
public interface NetApi {
|
||||||
|
@GET("/tools/password/type/get/list.do")
|
||||||
|
Call<HttpBody<List<PasswordTypeBean>>> getAllTypePassword();
|
||||||
|
@GET("/tools/password/get/list.do")
|
||||||
|
Call<HttpBody<List<ToolsPassword>>> getTypePassword(
|
||||||
|
@Query("type") int type
|
||||||
|
);
|
||||||
|
|
||||||
|
@GET("/tools/password/get/password.do")
|
||||||
|
Call<HttpBody<String>> getPassword(
|
||||||
|
@Query("id") int id
|
||||||
|
);
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("/tools/password/set/add.do")
|
||||||
|
Call<HttpBody<BaseBean>> addPassword(@Field("title") String title,
|
||||||
|
@Field("account") String account,
|
||||||
|
@Field("password") String password,
|
||||||
|
@Field("info") String info);
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("/tools/password/set/update.do")
|
||||||
|
Call<HttpBody<BaseBean>> updatePassword(@Field("id") int id,
|
||||||
|
@Field("title") String title,
|
||||||
|
@Field("account") String account,
|
||||||
|
@Field("password") String password,
|
||||||
|
@Field("info") String info);
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("/tools/password/set/remove.do")
|
||||||
|
Call<HttpBody<BaseBean>> removePassword(@Field("id") int id);
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.yutou.passmanage.NetUtils;
|
||||||
|
|
||||||
|
import com.yutou.netlibs.http.API;
|
||||||
|
import com.yutou.netlibs.http.BaseAPI;
|
||||||
|
import com.yutou.passmanage.Datas.AppData;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class NetTools extends API<NetApi> {
|
||||||
|
private NetTools(String url,HashMap<String,String> map) {
|
||||||
|
super(url,map);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NetApi getApi(){
|
||||||
|
HashMap<String,String> map=new HashMap<>();
|
||||||
|
map.put("token", AppData.key);
|
||||||
|
return new NetTools("https://tools.yutou233.cn",map).createAPI(NetApi.class);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
package com.yutou.passmanage.Tools;
|
||||||
|
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONException;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.yutou.passmanage.Datas.AppData;
|
||||||
|
import com.yutou.passmanage.Interfaces.NetworkInterface;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
|
public class NetworkTool {
|
||||||
|
|
||||||
|
private final static String TAG = NetworkTool.class.getSimpleName();
|
||||||
|
|
||||||
|
public static class NetworkAPI {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private String getExtUa() {
|
||||||
|
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toGetSplice(JSONObject json) {
|
||||||
|
try {
|
||||||
|
json.put("token", AppData.key);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
StringBuilder string = new StringBuilder();
|
||||||
|
Set<String> keys = json.keySet();
|
||||||
|
for (String key : keys) {
|
||||||
|
try {
|
||||||
|
string.append("&").append(key).append("=").append(URLEncoder.encode(json.getString(key), "UTF-8"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
try {
|
||||||
|
string.append("&").append(URLEncoder.encode(key, "UTF-8")).append("=");
|
||||||
|
// string += "&" + key + "=";
|
||||||
|
} catch (Exception e1) {
|
||||||
|
string.append("&").append(key).append("=");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string = new StringBuilder(string.substring(1, string.length()).replaceAll(" ", ""));
|
||||||
|
return string.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.yutou.passmanage.Tools;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
public class ResTools {
|
||||||
|
public static String getString(Context context,int id){
|
||||||
|
return context.getResources().getString(id);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
package com.yutou.passmanage.Tools;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Database;
|
||||||
|
import androidx.room.DatabaseConfiguration;
|
||||||
|
import androidx.room.InvalidationTracker;
|
||||||
|
import androidx.room.Room;
|
||||||
|
import androidx.room.RoomDatabase;
|
||||||
|
import androidx.room.migration.Migration;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.yutou.netlibs.http.HttpBody;
|
||||||
|
import com.yutou.passmanage.Datas.ToolsPassword;
|
||||||
|
import com.yutou.passmanage.Datas.ToolsPasswordDao;
|
||||||
|
import com.yutou.passmanage.Interfaces.NetworkInterface;
|
||||||
|
import com.yutou.passmanage.NetUtils.NetTools;
|
||||||
|
import com.yutou.passmanage.bean.BaseBean;
|
||||||
|
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.HttpException;
|
||||||
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
@Database(entities = {ToolsPassword.class}, version = 2,exportSchema = false)
|
||||||
|
public abstract class RoomDatabaseManager extends RoomDatabase {
|
||||||
|
public abstract ToolsPasswordDao passwordDao();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
protected InvalidationTracker createInvalidationTracker() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAllTables() {
|
||||||
|
|
||||||
|
}
|
||||||
|
private static final Migration update1to2=new Migration(1,2) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE ToolsPassword ADD COLUMN `info` ` TEXT` ");
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static RoomDatabaseManager build(Context context) {
|
||||||
|
return Room.databaseBuilder(context.getApplicationContext(), RoomDatabaseManager.class, "password.db")
|
||||||
|
.addMigrations(update1to2)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
public static void addPassword(String title, String account, String password,String info,int id,boolean isAdd, NetworkInterface networkInterface) {
|
||||||
|
JSONObject json = new JSONObject();
|
||||||
|
json.put("title", title);
|
||||||
|
json.put("username", account);
|
||||||
|
json.put("password", password);
|
||||||
|
json.put("url", "");
|
||||||
|
json.put("info", info);
|
||||||
|
json.put("type", "-1");
|
||||||
|
if(id!=-1){
|
||||||
|
json.put("id",id);
|
||||||
|
}
|
||||||
|
if(isAdd) {
|
||||||
|
NetTools.getApi().addPassword(title,account,password,info).enqueue(new retrofit2.Callback<HttpBody<BaseBean>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<HttpBody<BaseBean>> call, Response<HttpBody<BaseBean>> response) {
|
||||||
|
networkInterface.httpGetData(response.body().getData(), response.body().getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<HttpBody<BaseBean>> call, Throwable t) {
|
||||||
|
networkInterface.httpError(new RuntimeException(t.getMessage()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
NetTools.getApi().updatePassword(id,title,account,password,info).enqueue(new retrofit2.Callback<HttpBody<BaseBean>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<HttpBody<BaseBean>> call, Response<HttpBody<BaseBean>> response) {
|
||||||
|
networkInterface.httpGetData(response.body().getData(), response.body().getCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<HttpBody<BaseBean>> call, Throwable t) {
|
||||||
|
networkInterface.httpError(new RuntimeException(t.getMessage()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
app/src/main/java/com/yutou/passmanage/Tools/Tools.java
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package com.yutou.passmanage.Tools;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Tools {
|
||||||
|
public static boolean isEmpty(String str){
|
||||||
|
if(str==null|| str.trim().isEmpty()){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static PackageInfo getPackageInfo(String packageName, Context context){
|
||||||
|
List<PackageInfo> list=context.getPackageManager().getInstalledPackages(0);
|
||||||
|
if(list!=null){
|
||||||
|
for (PackageInfo packageInfo : list) {
|
||||||
|
if(packageInfo.packageName.equals(packageName)){
|
||||||
|
return packageInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public static List<PackageInfo> getAllPackageInfo(Context context){
|
||||||
|
return context.getPackageManager().getInstalledPackages(0);
|
||||||
|
}
|
||||||
|
public static void setConfig(String key,String value,Context context){
|
||||||
|
SharedPreferences preferences=context.getSharedPreferences("config.cfg",Context.MODE_PRIVATE);
|
||||||
|
preferences.edit().putString(key,value).apply();
|
||||||
|
}
|
||||||
|
public static String getConfig(String key,Context context){
|
||||||
|
SharedPreferences preferences=context.getSharedPreferences("config.cfg",Context.MODE_PRIVATE);
|
||||||
|
return preferences.getString(key,null);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.yutou.passmanage.bean;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class BaseBean implements Serializable {
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
package com.yutou.passmanage.bean;
|
||||||
|
|
||||||
|
public class PasswordBean extends BaseBean{
|
||||||
|
private int id;
|
||||||
|
private String title;
|
||||||
|
private String titlePinyin;
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String url;
|
||||||
|
private String info;
|
||||||
|
private int type;
|
||||||
|
private int uid;
|
||||||
|
|
||||||
|
public PasswordBean() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitlePinyin() {
|
||||||
|
return titlePinyin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitlePinyin(String titlePinyin) {
|
||||||
|
this.titlePinyin = titlePinyin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInfo() {
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInfo(String info) {
|
||||||
|
this.info = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(int type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUid(int uid) {
|
||||||
|
this.uid = uid;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package com.yutou.passmanage.bean;
|
||||||
|
|
||||||
|
public class PasswordTypeBean extends BaseBean{
|
||||||
|
private int id;
|
||||||
|
private String title;
|
||||||
|
private int uid;
|
||||||
|
|
||||||
|
public PasswordTypeBean() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getUid() {
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUid(int uid) {
|
||||||
|
this.uid = uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "PasswordTypeBean{" +
|
||||||
|
"id=" + id +
|
||||||
|
", title='" + title + '\'' +
|
||||||
|
", uid=" + uid +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
BIN
app/src/main/res/drawable/ic_close.png
Normal file
After Width: | Height: | Size: 683 B |
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
76
app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/search"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:gravity="start|top"
|
||||||
|
android:hint="搜索"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/clearSearch"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/clearSearch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:src="@drawable/ic_close"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.angcyo.tablayout.DslTabLayout
|
||||||
|
android:id="@+id/tabLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
</com.angcyo.tablayout.DslTabLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/passList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/tabLayout"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/search"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/goneText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text=""
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/floatButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="32dp"
|
||||||
|
android:layout_marginBottom="32dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:contentDescription="add"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:srcCompat="@android:drawable/ic_input_add" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
117
app/src/main/res/layout/dialog_add_passworld.xml
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="15dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/title" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:hint="@string/title" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/account" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/account"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="@dimen/edit_layout_height"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ems="10"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
android:hint="@string/account"
|
||||||
|
android:autofillHints="name" />
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/password" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/myPassword"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="@dimen/edit_layout_height"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/password"
|
||||||
|
android:autofillHints="name"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:text="@string/info" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ems="10"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:gravity="start|top"
|
||||||
|
android:hint="这里可以填写备注信息"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
|
android:minLines="5" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/delete"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="删除" />
|
||||||
|
</LinearLayout>
|
73
app/src/main/res/layout/recycler_list.xml
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/item_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="60dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_weight="0"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:srcCompat="@drawable/ic_launcher_foreground" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
android:layout_marginEnd="10dp"
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="TextView"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/user"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center|start"
|
||||||
|
android:gravity="right"
|
||||||
|
android:text="TextView" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start|center"
|
||||||
|
android:gravity="right"
|
||||||
|
android:text="TextView" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/copy"
|
||||||
|
android:layout_width="70dp"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_weight="0.1"
|
||||||
|
android:text="复制" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/divider2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?android:attr/listDivider" />
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
5
app/src/main/res/mipmap-anydpi-v26/app_icon.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/app_icon_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/app_icon_foreground"/>
|
||||||
|
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/app_icon_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/app_icon_foreground"/>
|
||||||
|
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/app_icon.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/app_icon_foreground.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/app_icon_round.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/app_icon.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/src/main/res/mipmap-mdpi/app_icon_foreground.png
Normal file
After Width: | Height: | Size: 973 B |
BIN
app/src/main/res/mipmap-mdpi/app_icon_round.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-xhdpi/app_icon.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/app_icon_foreground.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/app_icon_round.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/app_icon.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/app_icon_foreground.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/app_icon_round.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/app_icon.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/app_icon_foreground.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/app_icon_round.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 16 KiB |
21
app/src/main/res/values-night/themes.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.MyPassworldManage" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_200</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/black</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Base.Theme.MyPassworldManage" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your dark theme here. -->
|
||||||
|
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
4
app/src/main/res/values/app_icon_background.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="app_icon_background">#639BFF</color>
|
||||||
|
</resources>
|
10
app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
56
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">密码管理器</string>
|
||||||
|
<string name="password">密码</string>
|
||||||
|
<string name="account">账号</string>
|
||||||
|
<string name="title">标题</string>
|
||||||
|
<string name="info">备注</string>
|
||||||
|
<string name="name">名字</string>
|
||||||
|
<string name="toast_copy_password">已复制密码</string>
|
||||||
|
<string name="toast_update">更新成功</string>
|
||||||
|
<!-- Strings used for fragments for navigation -->
|
||||||
|
<string name="first_fragment_label">First Fragment</string>
|
||||||
|
<string name="second_fragment_label">Second Fragment</string>
|
||||||
|
<string name="next">Next</string>
|
||||||
|
<string name="previous">Previous</string>
|
||||||
|
|
||||||
|
<string name="lorem_ipsum">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam in scelerisque sem. Mauris
|
||||||
|
volutpat, dolor id interdum ullamcorper, risus dolor egestas lectus, sit amet mattis purus
|
||||||
|
dui nec risus. Maecenas non sodales nisi, vel dictum dolor. Class aptent taciti sociosqu ad
|
||||||
|
litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse blandit eleifend
|
||||||
|
diam, vel rutrum tellus vulputate quis. Aliquam eget libero aliquet, imperdiet nisl a,
|
||||||
|
ornare ex. Sed rhoncus est ut libero porta lobortis. Fusce in dictum tellus.\n\n
|
||||||
|
Suspendisse interdum ornare ante. Aliquam nec cursus lorem. Morbi id magna felis. Vivamus
|
||||||
|
egestas, est a condimentum egestas, turpis nisl iaculis ipsum, in dictum tellus dolor sed
|
||||||
|
neque. Morbi tellus erat, dapibus ut sem a, iaculis tincidunt dui. Interdum et malesuada
|
||||||
|
fames ac ante ipsum primis in faucibus. Curabitur et eros porttitor, ultricies urna vitae,
|
||||||
|
molestie nibh. Phasellus at commodo eros, non aliquet metus. Sed maximus nisl nec dolor
|
||||||
|
bibendum, vel congue leo egestas.\n\n
|
||||||
|
Sed interdum tortor nibh, in sagittis risus mollis quis. Curabitur mi odio, condimentum sit
|
||||||
|
amet auctor at, mollis non turpis. Nullam pretium libero vestibulum, finibus orci vel,
|
||||||
|
molestie quam. Fusce blandit tincidunt nulla, quis sollicitudin libero facilisis et. Integer
|
||||||
|
interdum nunc ligula, et fermentum metus hendrerit id. Vestibulum lectus felis, dictum at
|
||||||
|
lacinia sit amet, tristique id quam. Cras eu consequat dui. Suspendisse sodales nunc ligula,
|
||||||
|
in lobortis sem porta sed. Integer id ultrices magna, in luctus elit. Sed a pellentesque
|
||||||
|
est.\n\n
|
||||||
|
Aenean nunc velit, lacinia sed dolor sed, ultrices viverra nulla. Etiam a venenatis nibh.
|
||||||
|
Morbi laoreet, tortor sed facilisis varius, nibh orci rhoncus nulla, id elementum leo dui
|
||||||
|
non lorem. Nam mollis ipsum quis auctor varius. Quisque elementum eu libero sed commodo. In
|
||||||
|
eros nisl, imperdiet vel imperdiet et, scelerisque a mauris. Pellentesque varius ex nunc,
|
||||||
|
quis imperdiet eros placerat ac. Duis finibus orci et est auctor tincidunt. Sed non viverra
|
||||||
|
ipsum. Nunc quis augue egestas, cursus lorem at, molestie sem. Morbi a consectetur ipsum, a
|
||||||
|
placerat diam. Etiam vulputate dignissim convallis. Integer faucibus mauris sit amet finibus
|
||||||
|
convallis.\n\n
|
||||||
|
Phasellus in aliquet mi. Pellentesque habitant morbi tristique senectus et netus et
|
||||||
|
malesuada fames ac turpis egestas. In volutpat arcu ut felis sagittis, in finibus massa
|
||||||
|
gravida. Pellentesque id tellus orci. Integer dictum, lorem sed efficitur ullamcorper,
|
||||||
|
libero justo consectetur ipsum, in mollis nisl ex sed nisl. Donec maximus ullamcorper
|
||||||
|
sodales. Praesent bibendum rhoncus tellus nec feugiat. In a ornare nulla. Donec rhoncus
|
||||||
|
libero vel nunc consequat, quis tincidunt nisl eleifend. Cras bibendum enim a justo luctus
|
||||||
|
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
|
||||||
|
</string>
|
||||||
|
<string name="title_activity_main2">MainActivity2</string>
|
||||||
|
<string name="title_home">Home</string>
|
||||||
|
<string name="title_dashboard">Dashboard</string>
|
||||||
|
<string name="title_notifications">Notifications</string>
|
||||||
|
</resources>
|
21
app/src/main/res/values/themes.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.MyPassworldManage" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_500</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Base.Theme.MyPassworldManage" parent="Theme.Material3.DayNight.NoActionBar">
|
||||||
|
<!-- Customize your light theme here. -->
|
||||||
|
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
4
app/src/main/res/values/values.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<dimen name="edit_layout_height">48dp</dimen>
|
||||||
|
</resources>
|
17
app/src/test/java/com/yutou/passmanage/ExampleUnitTest.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package com.yutou.passmanage;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
24
build.gradle
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.5.2'
|
||||||
|
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21'
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
27
gradle.properties
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
|
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
|
||||||
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
|
android.nonTransitiveRClass=false
|
||||||
|
android.nonFinalResIds=false
|
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
244
gradlew
vendored
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
92
gradlew.bat
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
1
netlibs/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
34
netlibs/build.gradle
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
plugins {
|
||||||
|
id 'com.android.library'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.yutou.netlibs'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk 28
|
||||||
|
targetSdk 34
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
api 'com.squareup.okhttp3:okhttp:3.14.9'
|
||||||
|
api 'com.squareup.retrofit2:retrofit:2.3.0'
|
||||||
|
api 'com.alibaba.fastjson2:fastjson2:2.0.52'
|
||||||
|
|
||||||
|
}
|