From 94eba3a6ad79b96460a7fe63eb2d3db68cc23e5f Mon Sep 17 00:00:00 2001 From: aids61517 Date: Wed, 27 Jan 2021 14:11:08 +0800 Subject: [PATCH 1/4] support for multi line --- .../easyratingview/EasyRatingView.kt | 144 +++++++++++++----- .../easyratingview/StarBoundCalculator.kt | 99 ++++++++++++ easyratingview/src/main/res/values/attrs.xml | 2 + 3 files changed, 208 insertions(+), 37 deletions(-) create mode 100644 easyratingview/src/main/java/com/aids61517/easyratingview/StarBoundCalculator.kt diff --git a/easyratingview/src/main/java/com/aids61517/easyratingview/EasyRatingView.kt b/easyratingview/src/main/java/com/aids61517/easyratingview/EasyRatingView.kt index 36b43fa..0ef026e 100644 --- a/easyratingview/src/main/java/com/aids61517/easyratingview/EasyRatingView.kt +++ b/easyratingview/src/main/java/com/aids61517/easyratingview/EasyRatingView.kt @@ -5,8 +5,11 @@ import android.graphics.* import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.util.Log +import android.view.Gravity import android.view.View import androidx.core.content.ContextCompat +import kotlin.math.floor class EasyRatingView @JvmOverloads constructor( context: Context, @@ -57,7 +60,7 @@ class EasyRatingView @JvmOverloads constructor( } } - var numberStars: Int = 5 + var numberOfStars: Int = 5 set(value) { if (field != value) { field = value @@ -75,6 +78,15 @@ class EasyRatingView @JvmOverloads constructor( } } + var verticalSpacing: Int = 0 + set(value) { + if (field != value) { + field = value + invalidate() + requestLayout() + } + } + var step: Float = 0.5f set(value) { if (field != value) { @@ -102,84 +114,142 @@ class EasyRatingView @JvmOverloads constructor( private var fullDrawablePaint: Paint? = null + var countOfStarsPerRow: Int = 0 + private set + + private val boundCalculator by lazy { + StarBoundCalculator(this) + } + init { val typedArray = context.obtainStyledAttributes(attributeSet, R.styleable.EasyRatingView) - numberStars = typedArray.getInt(R.styleable.EasyRatingView_numStars, 5) + numberOfStars = typedArray.getInt(R.styleable.EasyRatingView_numStars, 5) spacing = typedArray.getDimensionPixelSize(R.styleable.EasyRatingView_spacing, 0) + verticalSpacing = + typedArray.getDimensionPixelSize(R.styleable.EasyRatingView_verticalSpacing, 0) rating = typedArray.getFloat(R.styleable.EasyRatingView_rating, 0f) step = typedArray.getFloat(R.styleable.EasyRatingView_step, 0.5f) maxRating = typedArray.getFloat(R.styleable.EasyRatingView_maxRating, 0f) - fullDrawableResourceId = typedArray.getResourceId(R.styleable.EasyRatingView_fullDrawable, 0) - emptyDrawableResourceId = typedArray.getResourceId(R.styleable.EasyRatingView_emptyDrawable, 0) + fullDrawableResourceId = + typedArray.getResourceId(R.styleable.EasyRatingView_fullDrawable, 0) + emptyDrawableResourceId = + typedArray.getResourceId(R.styleable.EasyRatingView_emptyDrawable, 0) + val gravity = typedArray.getInt(R.styleable.EasyRatingView_android_gravity, Gravity.START) + setGravity(gravity) typedArray.recycle() } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - if (emptyDrawable == null || numberStars == 0) { + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + if (emptyDrawable == null || numberOfStars == 0) { setMeasuredDimension(0, 0) return } emptyDrawable?.let { val drawableWidth = it.intrinsicWidth - val drawableHeight = it.intrinsicHeight val expectWidth = - numberStars * drawableWidth + (numberStars - 1) * spacing + paddingStart + paddingEnd - val realWidth = resolveSizeAndState(expectWidth, widthMeasureSpec, 0) - val expectHeight = drawableHeight + paddingTop + paddingBottom + numberOfStars * drawableWidth + (numberOfStars - 1) * spacing + paddingStart + paddingEnd + val calculateWidth = resolveSizeAndState(expectWidth, widthMeasureSpec, 0) + val realWidth = if (calculateWidth > widthSize) widthSize else calculateWidth + Log.d("EasyRatingView", "onMeasure realWidth = $realWidth") + countOfStarsPerRow = calculateCountOfStarsPerRow(realWidth) + Log.d("EasyRatingView", "onMeasure countOfStarsPerRow = $countOfStarsPerRow") + val expectHeight = calculateViewHeight(realWidth) val realHeight = resolveSizeAndState(expectHeight, heightMeasureSpec, 0) + Log.d("EasyRatingView", "onMeasure expectHeight = $expectHeight") + Log.d("EasyRatingView", "onMeasure realHeight = $realHeight") setMeasuredDimension(realWidth, realHeight) } } + private fun calculateCountOfStarsPerRow(width: Int): Int { + // n * bitmapWidth + (n - 1) * spacing < remainingWidth + // n * (bitmapWidth + spacing) < remainingWidth + spacing + val remainingWidth = width - paddingStart - paddingEnd + val bitmapWidth = emptyDrawable!!.intrinsicWidth + val calculateN = (remainingWidth + spacing).toFloat() / (bitmapWidth + spacing) + return floor(calculateN).toInt() + } + + private fun calculateViewHeight(realWidth: Int): Int { + val isMultiLine = (numberOfStars > countOfStarsPerRow) + val drawableHeight = emptyDrawable!!.intrinsicHeight + return if (isMultiLine) { + val lines = (numberOfStars / countOfStarsPerRow) + 1 + lines * drawableHeight + paddingTop + paddingBottom + (lines - 1) * verticalSpacing + } else { + drawableHeight + paddingTop + paddingBottom + } + } + + fun setGravity(gravity: Int) { + boundCalculator.gravity = gravity + } + override fun onDraw(canvas: Canvas) { - if (emptyDrawable == null || numberStars == 0) { + if (emptyDrawable == null || numberOfStars == 0) { return } - val drawStartX = paddingStart - val drawStartY = paddingTop emptyDrawable?.apply { - val width = intrinsicWidth - val height = intrinsicHeight - for (i in 0 until numberStars) { - val startX = i * (width + spacing) + drawStartX - setBounds(startX, drawStartY, startX + width, drawStartY + height) - draw(canvas) - } + drawStar(canvas, this, numberOfStars) } fullDrawable?.apply { - val maxRating = if (maxRating != 0f) maxRating else numberStars.toFloat() - val rating = if (rating > maxRating) maxRating else (rating / maxRating) * numberStars - val finalRating = rating.getFinalRatingByStep(step) - val width = intrinsicWidth - val height = intrinsicHeight - val fullCount = finalRating.toInt() - for (i in 0 until fullCount) { - val startX = i * (width + spacing) + drawStartX - setBounds(startX, drawStartY, startX + width, drawStartY + height) - draw(canvas) - } + val ratingByStep = getRatingByStep(step) + val fullCountOfStar = ratingByStep.toInt() + drawStar(canvas, this, fullCountOfStar) - val offsetX = fullCount * (width + spacing) + drawStartX - val offsetY = drawStartY.toFloat() + val indexLineOfNextStar = (fullCountOfStar + 1) / countOfStarsPerRow + val lineDrawStarX = boundCalculator.getBoundStart(indexLineOfNextStar) + val lineDrawStarY = boundCalculator.getBoundTop(indexLineOfNextStar) + val indexOfStar = (ratingByStep - indexLineOfNextStar * countOfStarsPerRow).toInt() + val drawStartX = lineDrawStarX + indexOfStar * (intrinsicWidth + spacing) canvas.save() - canvas.translate(offsetX.toFloat(), offsetY) - val targetWidth = width * (finalRating % 1) + canvas.translate(drawStartX.toFloat(), lineDrawStarY.toFloat()) + val targetWidth = intrinsicWidth * (ratingByStep % 1) canvas.drawRect( 0f, 0f, targetWidth, - height.toFloat(), + intrinsicHeight.toFloat(), fullDrawablePaint!! ) canvas.restore() } } - private fun Float.getFinalRatingByStep(step: Float): Float { - val newRatingRatio = this / step + private fun drawStar(canvas: Canvas, drawable: Drawable, countOfStar: Int) { + val lines = (countOfStar / countOfStarsPerRow) + 1 + drawable.run { + val drawableWidth = intrinsicWidth + val drawableHeight = intrinsicHeight + repeat(lines) { indexOfLine -> + val drawStartX = boundCalculator.getBoundStart(indexOfLine) + val drawStartY = boundCalculator.getBoundTop(indexOfLine) + val startCountOfStar = indexOfLine * countOfStarsPerRow + val endCountOfStart = + if (startCountOfStar + countOfStarsPerRow > countOfStar) countOfStar else startCountOfStar + countOfStarsPerRow + for (i in 0 until (endCountOfStart - startCountOfStar)) { + val startX = i * (drawableWidth + spacing) + drawStartX + setBounds( + startX, + drawStartY, + startX + drawableWidth, + drawStartY + drawableHeight + ) + draw(canvas) + } + } + + } + } + + private fun getRatingByStep(step: Float): Float { + val maxRating = if (maxRating != 0f) maxRating else numberOfStars.toFloat() + val rating = if (rating > maxRating) maxRating else (rating / maxRating) * numberOfStars + val newRatingRatio = rating / step val multiple = if ((newRatingRatio % 1) >= 0.5) { newRatingRatio.toInt() + 1 } else { diff --git a/easyratingview/src/main/java/com/aids61517/easyratingview/StarBoundCalculator.kt b/easyratingview/src/main/java/com/aids61517/easyratingview/StarBoundCalculator.kt new file mode 100644 index 0000000..52f96e1 --- /dev/null +++ b/easyratingview/src/main/java/com/aids61517/easyratingview/StarBoundCalculator.kt @@ -0,0 +1,99 @@ +package com.aids61517.easyratingview + +import android.view.Gravity + +internal class StarBoundCalculator(private val ratingView: EasyRatingView) { + var gravity = Gravity.START + set(value) { + var g = value + if (g and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK == 0) { + g = g or Gravity.START + } + if (g and Gravity.VERTICAL_GRAVITY_MASK == 0) { + g = g or Gravity.TOP + } + + field = g + } + + fun getBoundStart(indexOfLine: Int): Int { + if (ratingView.emptyDrawable == null) return 0 + return when (gravity and Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) { + Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> { + calculateXOfGravityCenter(indexOfLine) + } + Gravity.RIGHT, Gravity.END -> { + calculateXOfGravityEnd(indexOfLine) + } + else -> { + calculateXOfGravityStart() + } + } + } + + fun getBoundTop(indexOfLine: Int): Int { + if (ratingView.emptyDrawable == null) return 0 + return when (gravity and Gravity.VERTICAL_GRAVITY_MASK) { + Gravity.CENTER, Gravity.CENTER_VERTICAL -> { + calculateYOfGravityCenter(indexOfLine) + } + Gravity.BOTTOM -> { + calculateYOfGravityBottom(indexOfLine) + } + else -> { + calculateYOfGravityTop(indexOfLine) + } + } + } + + private fun calculateYOfGravityCenter(indexOfLine: Int): Int { + val drawableHeight = ratingView.emptyDrawable!!.intrinsicHeight + val topY = ratingView.paddingTop + val bottomY = ratingView.height - ratingView.paddingBottom + val centerY = (topY + bottomY) / 2 + val lines = ratingView.numberOfStars / ratingView.countOfStarsPerRow + 1 + val contentHeight = lines * drawableHeight + (lines - 1) * ratingView.verticalSpacing + val firstLineY = centerY - contentHeight / 2 + return firstLineY + indexOfLine * (drawableHeight + ratingView.verticalSpacing) + } + + private fun calculateYOfGravityBottom(indexOfLine: Int): Int { + val lines = ratingView.numberOfStars / ratingView.countOfStarsPerRow + 1 + val drawableHeight = ratingView.emptyDrawable!!.intrinsicHeight + return ratingView.height - ratingView.paddingBottom - (lines - indexOfLine) * drawableHeight - (lines - indexOfLine - 1) * ratingView.verticalSpacing + } + + private fun calculateYOfGravityTop(indexOfLine: Int): Int { + val drawableHeight = ratingView.emptyDrawable!!.intrinsicHeight + return ratingView.paddingTop + indexOfLine * (drawableHeight + ratingView.verticalSpacing) + } + + private fun calculateXOfGravityCenter(indexOfLine: Int): Int { + val count = getCountOfStars(indexOfLine) + val drawableWidth = ratingView.emptyDrawable!!.intrinsicWidth + val startX = ratingView.paddingStart + val endX = ratingView.width - ratingView.paddingEnd + val centerX = (startX + endX) / 2 + val contentWidth = count * drawableWidth + (count - 1) * ratingView.spacing + return centerX - contentWidth / 2 + } + + private fun calculateXOfGravityStart(): Int { + return ratingView.paddingStart + } + + private fun calculateXOfGravityEnd(indexOfLine: Int): Int { + val count = getCountOfStars(indexOfLine) + val drawableWidth = ratingView.emptyDrawable!!.intrinsicWidth + return ratingView.width - ratingView.paddingEnd - count * drawableWidth - (count - 1) * ratingView.spacing + } + + private fun getCountOfStars(indexOfLine: Int): Int { + val countOfStarsPerRow = ratingView.countOfStarsPerRow + val numberOfStars = ratingView.numberOfStars + val startCountOfStar = indexOfLine * countOfStarsPerRow + val endCountOfStart = + if (startCountOfStar + countOfStarsPerRow > numberOfStars) numberOfStars else startCountOfStar + countOfStarsPerRow + return endCountOfStart - startCountOfStar + } +} \ No newline at end of file diff --git a/easyratingview/src/main/res/values/attrs.xml b/easyratingview/src/main/res/values/attrs.xml index 0c9e662..4706e8c 100644 --- a/easyratingview/src/main/res/values/attrs.xml +++ b/easyratingview/src/main/res/values/attrs.xml @@ -6,7 +6,9 @@ + + \ No newline at end of file From a015902dd244e5c5783ae7c02a66356cb1fd0aa3 Mon Sep 17 00:00:00 2001 From: aids61517 Date: Wed, 27 Jan 2021 16:13:45 +0800 Subject: [PATCH 2/4] upgrade gradle version --- build.gradle | 4 ++-- easyratingview/build.gradle | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 438d280..9c426d9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.41' + ext.kotlin_version = '1.4.21' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/easyratingview/build.gradle b/easyratingview/build.gradle index 941ad19..5e766a1 100644 --- a/easyratingview/build.gradle +++ b/easyratingview/build.gradle @@ -28,12 +28,12 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'androidx.appcompat:appcompat:1.0.2' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation 'androidx.appcompat:appcompat:1.2.0' + testImplementation 'junit:junit:4.13.1' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "androidx.core:core-ktx:1.0.2" + implementation "androidx.core:core-ktx:1.3.2" } repositories { mavenCentral() diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f2e410c..58c69cb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip From f2ca7418131389ca429a3f51c8160faec35877a0 Mon Sep 17 00:00:00 2001 From: aids61517 Date: Wed, 27 Jan 2021 16:14:05 +0800 Subject: [PATCH 3/4] update sample code --- app/src/main/res/layout/activity_main.xml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 15f7ad3..bbef325 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -9,17 +9,20 @@ + app:step="0.5" + app:verticalSpacing="10dp" /> Date: Wed, 27 Jan 2021 16:26:09 +0800 Subject: [PATCH 4/4] modified gitignore --- .gitignore | 1 + .idea/gradle.xml | 19 ------------------- .idea/misc.xml | 14 -------------- 3 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 .idea/gradle.xml delete mode 100644 .idea/misc.xml diff --git a/.gitignore b/.gitignore index 2b75303..95a973d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml +/.idea .DS_Store /build /captures diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 5a05efb..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 703e5d4..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file