Skip to content

Commit

Permalink
Merge branch 'trunk' into feature/notifications_refresh_p2
Browse files Browse the repository at this point in the history
  • Loading branch information
Antonis Lilis committed Apr 26, 2024
2 parents 19ea81b + d68a3f7 commit f476a2f
Show file tree
Hide file tree
Showing 21 changed files with 1,201 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type.
import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type.PIE_CHART
import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type.QUICK_SCAN_ITEM
import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type.REFERRED_ITEM
import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type.SUBSCRIBERS_CHART
import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type.TABS
import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type.TAG_ITEM
import org.wordpress.android.ui.stats.refresh.lists.sections.BlockListItem.Type.TEXT
Expand Down Expand Up @@ -71,6 +72,7 @@ class BlockDiffCallback(
BAR_CHART,
PIE_CHART,
LINE_CHART,
SUBSCRIBERS_CHART,
ACTIVITY_ITEM,
LIST_ITEM -> oldItem.itemId == newItem.itemId
LINK,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.T
import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.TotalFollowersUseCase.TotalFollowersUseCaseFactory
import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.TotalLikesUseCase.TotalLikesUseCaseFactory
import org.wordpress.android.ui.stats.refresh.lists.sections.insights.usecases.ViewsAndVisitorsUseCase.ViewsAndVisitorsUseCaseFactory
import org.wordpress.android.ui.stats.refresh.lists.sections.subscribers.usecases.SubscribersUseCase
import org.wordpress.android.ui.stats.refresh.utils.SelectedTrafficGranularityManager
import org.wordpress.android.ui.stats.refresh.utils.StatsSiteProvider
import org.wordpress.android.util.config.StatsTrafficSubscribersTabFeatureConfig
Expand All @@ -85,7 +86,7 @@ const val LIST_STATS_USE_CASES = "ListStatsUseCases"
const val BLOCK_INSIGHTS_USE_CASES = "BlockInsightsUseCases"
const val VIEW_ALL_INSIGHTS_USE_CASES = "ViewAllInsightsUseCases"
const val GRANULAR_USE_CASE_FACTORIES = "GranularUseCaseFactories"
const val SUBSCRIBER_USE_CASE_FACTORIES = "SubscriberUseCaseFactories"
const val BLOCK_SUBSCRIBERS_USE_CASES = "BlockSubscribersUseCases"

// These are injected only internally
private const val BLOCK_DETAIL_USE_CASES = "BlockDetailUseCases"
Expand Down Expand Up @@ -245,6 +246,18 @@ class StatsModule {
)
}

/**
* Provides a list of use cases for the Subscribers screen based in Stats. Modify this method when you want to add
* more blocks to the Insights screen.
*/
@Provides
@Singleton
@Named(BLOCK_SUBSCRIBERS_USE_CASES)
@Suppress("LongParameterList")
fun provideBlockSubscribersUseCases(
subscribersUseCase: SubscribersUseCase
): List<@JvmSuppressWildcards BaseStatsUseCase<*, *>> = listOf(subscribersUseCase)

/**
* Provides a singleton usecase that represents the Insights screen. It consists of list of use cases that build
* the insights blocks.
Expand Down Expand Up @@ -300,41 +313,28 @@ class StatsModule {
}

/**
* Provides a list of use case factories that build use cases for the Subscribers stats screen based on the given
* granularity (Day, Week, Month, Year).
* Provides a singleton use case that represents the Subscribers Stats screen. It consists of list of use cases
* that build the subscribers blocks.
*/
@Provides
@Singleton
@Named(SUBSCRIBER_USE_CASE_FACTORIES)
@Suppress("LongParameterList")
fun provideSubscriberUseCaseFactories(
): List<@JvmSuppressWildcards BaseStatsUseCase<*, *>> {
return listOf(
)
}

/**
* Provides a singleton use case that represents the Subscribers Stats screen.
* @param useCasesFactories build the use cases for the DAYS granularity
*/
@Provides
@Named(SUBSCRIBERS_USE_CASE)
@Suppress("LongParameterList")
fun provideSubscribersUseCase(
statsStore: StatsStore,
@Named(BG_THREAD) bgDispatcher: CoroutineDispatcher,
@Named(UI_THREAD) mainDispatcher: CoroutineDispatcher,
statsSiteProvider: StatsSiteProvider,
@Named(SUBSCRIBER_USE_CASE_FACTORIES) useCases: List<@JvmSuppressWildcards BaseStatsUseCase<*, *>>,
@Named(BLOCK_SUBSCRIBERS_USE_CASES) useCases: List<@JvmSuppressWildcards BaseStatsUseCase<*, *>>,
uiModelMapper: UiModelMapper
): BaseListUseCase {
return BaseListUseCase(
bgDispatcher,
mainDispatcher,
statsSiteProvider,
useCases,
{ statsStore.getInsightTypes(it) },
uiModelMapper::mapInsights
{ statsStore.getSubscriberTypes() },
uiModelMapper::mapSubscribers
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package org.wordpress.android.ui.stats.refresh

import android.content.Context
import android.graphics.BlurMaskFilter
import android.graphics.BlurMaskFilter.Blur.NORMAL
import android.graphics.Canvas
import android.graphics.CornerPathEffect
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Path.Direction.CW
import android.graphics.RectF
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.components.MarkerView
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.utils.MPPointF
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.R
import org.wordpress.android.ui.stats.refresh.utils.StatsDateFormatter
import org.wordpress.android.ui.stats.refresh.utils.StatsUtils
import javax.inject.Inject

@AndroidEntryPoint
class SubscribersChartMarkerView @Inject constructor(
context: Context
) : MarkerView(context, R.layout.stats_subscribers_chart_marker) {
@Inject
lateinit var statsUtils: StatsUtils

@Inject
lateinit var statsDateFormatter: StatsDateFormatter
private val countView = findViewById<TextView>(R.id.marker_text1)
private val dateView = findViewById<TextView>(R.id.marker_text2)

override fun refreshContent(e: Entry?, highlight: Highlight?) {
val lineChart = chartView as? LineChart ?: return
val xValue = e?.x?.toInt() ?: return

val dataSet = lineChart.lineData.dataSets.first() as LineDataSet
// get the corresponding Y axis value according to the current X axis position
val index = if (xValue < dataSet.values.size) xValue else 0
val yValue = dataSet.values[index].y

val count = yValue.toLong()
countView.text = count.toString()
val date = statsDateFormatter.getStatsDateFromPeriodDay(e.data.toString())
dateView.text = date

super.refreshContent(e, highlight)
}

override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF {
// posY posX refers to the position of the upper left corner of the markerView on the chart
val width = width.toFloat()
val height = height.toFloat()

// If the y coordinate of the point is less than the height of the markerView,
// if it is not processed, it will exceed the upper boundary. After processing,
// the arrow is up at this time, and we need to move the icon down by the size of the arrow
if (posY <= height + ARROW_SIZE) {
offset.y = ARROW_SIZE
} else {
// Otherwise, it is normal, because our default is that the arrow is facing downwards,
// and then the normal offset is that you need to offset the height of the markerView and the arrow size,
// plus a stroke width, because you need to see the upper border of the dialog box
offset.y = -height - ARROW_SIZE - STROKE_WIDTH
}

// handle X direction, left, middle, and right side of the chart
if (posX > chartView.width - width) { // If it exceeds the right boundary, offset the view width to the left
offset.x = -width
} else { // by default, no offset (because the point is in the upper left corner)
offset.x = 0F
// If it is greater than half of the markerView, the arrow is in the middle,
// so it is offset by half the width to the right
if (posX > width / 2) {
offset.x = -width / 2
}
}

return offset
}

override fun draw(canvas: Canvas, posX: Float, posY: Float) {
super.draw(canvas, posX, posY)

val saveId = canvas.save()

drawToolTip(canvas, posX, posY)
draw(canvas)

canvas.restoreToCount(saveId)
}

@Suppress("LongMethod")
private fun drawToolTip(canvas: Canvas?, posX: Float, posY: Float) {
val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = STROKE_WIDTH
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
pathEffect = CornerPathEffect(CORNER_RADIUS)
color = context.getColor(R.color.blue_100)
}

val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
strokeCap = Paint.Cap.ROUND
pathEffect = CornerPathEffect(CORNER_RADIUS)
color = context.getColor(R.color.blue_100)
}

val chart = chartView
val width = width.toFloat()
val height = height.toFloat()

val offset = getOffsetForDrawingAtPoint(posX, posY)

val path = Path()

if (posY < height + ARROW_SIZE) { // Processing exceeds the upper boundary
path.moveTo(0f, 0f)
if (posX > chart.width - width) { // Exceed the right boundary
path.lineTo(width - ARROW_SIZE, 0f)
path.lineTo(width, -ARROW_SIZE + CIRCLE_OFFSET)
path.lineTo(width, 0f)
} else {
if (posX > width / 2) { // In the middle of the chart
path.lineTo(width / 2 - ARROW_SIZE / 2, 0f)
path.lineTo(width / 2, -ARROW_SIZE + CIRCLE_OFFSET)
path.lineTo(width / 2 + ARROW_SIZE / 2, 0f)
} else { // Exceed the left margin
path.lineTo(0f, -ARROW_SIZE + CIRCLE_OFFSET)
path.lineTo(0 + ARROW_SIZE, 0f)
}
}
path.lineTo(0 + width, 0f)
path.lineTo(0 + width, 0 + height)
path.lineTo(0f, 0 + height)
path.lineTo(0f, 0f)
path.offset(posX + offset.x, posY + offset.y)
} else { // Does not exceed the upper boundary
path.moveTo(0f, 0f)
path.lineTo(0 + width, 0f)
path.lineTo(0 + width, 0 + height)
if (posX > chart.width - width) {
path.lineTo(width, height + ARROW_SIZE - CIRCLE_OFFSET)
path.lineTo(width - ARROW_SIZE, 0 + height)
path.lineTo(0f, 0 + height)
} else {
if (posX > width / 2) {
path.lineTo(width / 2 + ARROW_SIZE / 2, 0 + height)
path.lineTo(width / 2, height + ARROW_SIZE - CIRCLE_OFFSET)
path.lineTo(width / 2 - ARROW_SIZE / 2, 0 + height)
path.lineTo(0f, 0 + height)
} else {
path.lineTo(0 + ARROW_SIZE, 0 + height)
path.lineTo(0f, height + ARROW_SIZE - CIRCLE_OFFSET)
path.lineTo(0f, 0 + height)
}
}
path.lineTo(0f, 0f)
path.offset(posX + offset.x, posY + offset.y)
}
path.close()

// translate to the correct position and draw
canvas?.apply {
drawPath(path, bgPaint)
drawPath(path, borderPaint)
drawDataPoint(canvas, posX, posY)
translate(posX + offset.x, posY + offset.y)
}
}

private fun drawDataPoint(canvas: Canvas?, posX: Float, posY: Float) {
val circleShadowPaint = Paint().apply {
style = Paint.Style.FILL
color = ContextCompat.getColor(context, R.color.gray_10)
maskFilter = BlurMaskFilter(MASK_FILTER_RADIUS, NORMAL)
}

val circleBorderPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = CIRCLE_STROKE_WIDTH
isAntiAlias = true
isDither = true
color = ContextCompat.getColor(context, R.color.blue_0)
}

val circleFillPaint = Paint().apply {
style = Paint.Style.FILL
isAntiAlias = true
isDither = true
color = ContextCompat.getColor(context, R.color.blue_50)
}

val circleShadowPath = Path().apply {
addCircle(posX, posY, CIRCLE_SHADOW_RADIUS, CW)
}

val circleFillPath = Path().apply {
addCircle(posX, posY, CIRCLE_RADIUS, CW)
}

val circleBorderPath = Path().apply {
addCircle(posX, posY, CIRCLE_RADIUS, CW)
fillType = Path.FillType.EVEN_ODD
}

val innerCircle = RectF().apply {
inset(CIRCLE_STROKE_WIDTH, CIRCLE_STROKE_WIDTH)
}
if (innerCircle.width() > 0 && innerCircle.height() > 0) {
circleBorderPath.addCircle(posX, posY, CIRCLE_RADIUS, CW)
}

canvas?.apply {
drawPath(circleShadowPath, circleShadowPaint)
drawPath(circleFillPath, circleFillPaint)
drawPath(circleBorderPath, circleBorderPaint)
}
}

companion object {
const val CORNER_RADIUS = 10F
const val ARROW_SIZE = 40F
const val STROKE_WIDTH = 5F
const val CIRCLE_OFFSET = 14F

const val CIRCLE_RADIUS = 12F
const val CIRCLE_SHADOW_RADIUS = CIRCLE_RADIUS + 2F
const val CIRCLE_STROKE_WIDTH = 4F
const val MASK_FILTER_RADIUS = 5F
}
}
Loading

0 comments on commit f476a2f

Please sign in to comment.