diff --git a/app/src/main/java/s/yarlykov/fixdataproto/Utils.kt b/app/src/main/java/s/yarlykov/fixdataproto/Utils.kt index d15f8da..c66378b 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/Utils.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/Utils.kt @@ -5,4 +5,5 @@ import android.util.Log fun logIt(message: String, tag: String = "APP_TAG") { Log.i(tag, message) System.out.println("$tag: $message") -} \ No newline at end of file +} + diff --git a/app/src/main/java/s/yarlykov/fixdataproto/application/TradingApp.kt b/app/src/main/java/s/yarlykov/fixdataproto/application/TradingApp.kt index 4f24893..c955804 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/application/TradingApp.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/application/TradingApp.kt @@ -4,10 +4,9 @@ import android.app.Application import s.yarlykov.fixdataproto.R import s.yarlykov.fixdataproto.data.BarMarketDataRepoImpl import s.yarlykov.fixdataproto.data.FooMarketDataRepoImpl -import s.yarlykov.fixdataproto.domain.MarketDataHub +import s.yarlykov.fixdataproto.data.MarketDataHub import s.yarlykov.fixdataproto.domain.MarketDataProvider -import s.yarlykov.fixdataproto.domain.MarketDataProviderImpl -import s.yarlykov.fixdataproto.domain.MarketDataRepo +import s.yarlykov.fixdataproto.data.MarketDataProviderImpl class TradingApp : Application() { @@ -16,9 +15,11 @@ class TradingApp : Application() { override fun onCreate() { super.onCreate() + val capacity = 30 + val list = listOf( - MarketDataProviderImpl(getString(R.string.foo), FooMarketDataRepoImpl()), - MarketDataProviderImpl(getString(R.string.bar), BarMarketDataRepoImpl()) + MarketDataProviderImpl(getString(R.string.foo), FooMarketDataRepoImpl(), capacity), + MarketDataProviderImpl(getString(R.string.bar), BarMarketDataRepoImpl(), capacity) ) marketDataHub = MarketDataHub(list) diff --git a/app/src/main/java/s/yarlykov/fixdataproto/data/BarMarketDataRepoImpl.kt b/app/src/main/java/s/yarlykov/fixdataproto/data/BarMarketDataRepoImpl.kt index e7e7e3c..022093e 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/data/BarMarketDataRepoImpl.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/data/BarMarketDataRepoImpl.kt @@ -4,22 +4,22 @@ import io.reactivex.Observable import io.reactivex.schedulers.Schedulers import s.yarlykov.fixdataproto.domain.MarketData import s.yarlykov.fixdataproto.domain.MarketDataRepo -import s.yarlykov.fixdataproto.logIt import java.util.concurrent.TimeUnit import kotlin.random.Random -private const val PRICE_MIN = 50 -private const val PRICE_MAX = 70 +const val BAR_PRICE_MIN = 50 +const val BAR_PRICE_MAX = 70 + +class BarMarketDataRepoImpl : MarketDataRepo() { -class BarMarketDataRepoImpl : MarketDataRepo { override fun connect(): Observable = Observable .interval(1, TimeUnit.SECONDS, Schedulers.newThread()) .map { - MarketData(Random.nextInt(PRICE_MIN, PRICE_MAX)) - } - .doOnNext { - logIt("${it.value} in ${it.time}") + MarketData( + Random.nextInt(BAR_PRICE_MIN, BAR_PRICE_MAX), + timeLineHandler.getMarker(System.currentTimeMillis()) + ) } .publish() .refCount() diff --git a/app/src/main/java/s/yarlykov/fixdataproto/data/FooMarketDataImpl.kt b/app/src/main/java/s/yarlykov/fixdataproto/data/FooMarketDataRepoImpl.kt similarity index 64% rename from app/src/main/java/s/yarlykov/fixdataproto/data/FooMarketDataImpl.kt rename to app/src/main/java/s/yarlykov/fixdataproto/data/FooMarketDataRepoImpl.kt index 5a7a34f..a23cf2d 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/data/FooMarketDataImpl.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/data/FooMarketDataRepoImpl.kt @@ -8,19 +8,20 @@ import s.yarlykov.fixdataproto.logIt import java.util.concurrent.TimeUnit import kotlin.random.Random -private const val PRICE_MIN = 1 -private const val PRICE_MAX = 20 +const val FOO_PRICE_MIN = 1 +const val FOO_PRICE_MAX = 20 -class FooMarketDataRepoImpl : MarketDataRepo { +class FooMarketDataRepoImpl : MarketDataRepo() { override fun connect(): Observable = Observable .interval(1, TimeUnit.SECONDS, Schedulers.newThread()) .map { - MarketData(Random.nextInt(PRICE_MIN, PRICE_MAX)) + MarketData(Random.nextInt(FOO_PRICE_MIN, FOO_PRICE_MAX), + timeLineHandler.getMarker(System.currentTimeMillis())) } .doOnNext { - logIt("${it.value} in ${it.time}") + logIt("${it.value} in ${it.marker.time}") } .publish() .refCount() diff --git a/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataHub.kt b/app/src/main/java/s/yarlykov/fixdataproto/data/MarketDataHub.kt similarity index 71% rename from app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataHub.kt rename to app/src/main/java/s/yarlykov/fixdataproto/data/MarketDataHub.kt index 3fc027b..14ff8f7 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataHub.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/data/MarketDataHub.kt @@ -1,6 +1,9 @@ -package s.yarlykov.fixdataproto.domain +package s.yarlykov.fixdataproto.data import io.reactivex.Observable +import s.yarlykov.fixdataproto.domain.Granularity +import s.yarlykov.fixdataproto.domain.MarketData +import s.yarlykov.fixdataproto.domain.MarketDataProvider class MarketDataHub(providers: List) { diff --git a/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataProviderImpl.kt b/app/src/main/java/s/yarlykov/fixdataproto/data/MarketDataProviderImpl.kt similarity index 83% rename from app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataProviderImpl.kt rename to app/src/main/java/s/yarlykov/fixdataproto/data/MarketDataProviderImpl.kt index a622b49..f16dd85 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataProviderImpl.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/data/MarketDataProviderImpl.kt @@ -1,9 +1,15 @@ -package s.yarlykov.fixdataproto.domain +package s.yarlykov.fixdataproto.data import io.reactivex.Observable import io.reactivex.Observer import io.reactivex.disposables.Disposable import io.reactivex.subjects.BehaviorSubject +import s.yarlykov.fixdataproto.domain.Granularity +import s.yarlykov.fixdataproto.domain.MarketData +import s.yarlykov.fixdataproto.domain.MarketDataProvider +import s.yarlykov.fixdataproto.domain.MarketDataRepo +import s.yarlykov.fixdataproto.domain.time.TimeEvent +import s.yarlykov.fixdataproto.domain.time.TimeLineMarker /** * Класс реализует двусвязный список на массиве. Массив удобен для первичной инииализации. @@ -19,7 +25,12 @@ class MarketDataProviderImpl( // Массив для хранения котировок private var history: Array = Array(capacity) { index -> - Link(index, MarketData(0), null, null) + Link( + index, + MarketData(0, TimeLineMarker(0, TimeEvent.SECOND)), + null, + null + ) } // Этот указатель будет передвигаться по кругу и указывать @@ -38,7 +49,7 @@ class MarketDataProviderImpl( override fun onNext(fixData: MarketData) { head.marketData = fixData head = head.next!! - aggregatedDataStream.onNext(collectAscent()) + aggregatedDataStream.onNext(headIsPastTailIsNow()) } override fun onError(e: Throwable) { @@ -78,7 +89,7 @@ class MarketDataProviderImpl( } // Список котировок по убывающей дате - private fun collectDescent(): List { + private fun headIsNowTailIsPast(): List { val list = mutableListOf() @@ -94,7 +105,7 @@ class MarketDataProviderImpl( } // Список котировок по возрастающей дате - private fun collectAscent() : List { + private fun headIsPastTailIsNow(): List { val list = mutableListOf() var item = head diff --git a/app/src/main/java/s/yarlykov/fixdataproto/domain/ChartOptions.kt b/app/src/main/java/s/yarlykov/fixdataproto/domain/ChartOptions.kt new file mode 100644 index 0000000..a70be3e --- /dev/null +++ b/app/src/main/java/s/yarlykov/fixdataproto/domain/ChartOptions.kt @@ -0,0 +1,8 @@ +package s.yarlykov.fixdataproto.domain + +data class ChartOptions( + val title : String, + val axisX : String, + val axisY: String, + val min : Int, + val max : Int) \ No newline at end of file diff --git a/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketData.kt b/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketData.kt index 2a9f10e..0254575 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketData.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketData.kt @@ -1,3 +1,8 @@ package s.yarlykov.fixdataproto.domain -data class MarketData(val value : Int, val time : Long = System.currentTimeMillis()) \ No newline at end of file +import s.yarlykov.fixdataproto.domain.time.TimeLineMarker + +data class MarketData( + val value: Int, + val marker: TimeLineMarker +) \ No newline at end of file diff --git a/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataRepo.kt b/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataRepo.kt index ad2eacf..8063532 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataRepo.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/domain/MarketDataRepo.kt @@ -1,7 +1,11 @@ package s.yarlykov.fixdataproto.domain import io.reactivex.Observable +import s.yarlykov.fixdataproto.domain.time.TimeLineHandler -interface MarketDataRepo { - fun connect() : Observable +abstract class MarketDataRepo { + + val timeLineHandler = TimeLineHandler() + + abstract fun connect(): Observable } \ No newline at end of file diff --git a/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeEvent.kt b/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeEvent.kt new file mode 100644 index 0000000..1aa4004 --- /dev/null +++ b/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeEvent.kt @@ -0,0 +1,8 @@ +package s.yarlykov.fixdataproto.domain.time + +enum class TimeEvent(val value : Int) { + SECOND(1), + MINUTE(60), + HOUR(3600), + DAY(24 * 60 * 60) +} \ No newline at end of file diff --git a/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeLineHandler.kt b/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeLineHandler.kt new file mode 100644 index 0000000..face7b4 --- /dev/null +++ b/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeLineHandler.kt @@ -0,0 +1,57 @@ +package s.yarlykov.fixdataproto.domain.time + +import java.text.SimpleDateFormat +import java.util.* + +class TimeLineHandler(startTime: Long = System.currentTimeMillis()) { + + private val timeFormat = "dd-HH-mm-ss" + + private val day = 0 + private val hour = 1 + private val minute = 2 + + private val parsedStartTime = parseTime(startTime) + + private var lastDay: Int = parsedStartTime[day] + private var lastHour: Int = parsedStartTime[hour] + private var lastMinute: Int = parsedStartTime[minute] + + /** + * Вернуть маркер, который клеится к маркет дате + */ + fun getMarker(time: Long): TimeLineMarker = + TimeLineMarker(time, getEvent(time)) + + /** + * Определить произошел ли переход дня/часа/минуты + * Переход дня подразумевает также переход часа и минуты, + * а переход часа - переход минуты. + */ + private fun getEvent(time: Long): TimeEvent { + val currentTime = parseTime(time) + + return if (currentTime[day] != lastDay) { + lastDay = currentTime[day] + TimeEvent.DAY + } else if (currentTime[hour] != lastHour) { + lastHour = currentTime[hour] + TimeEvent.HOUR + } else if (currentTime[minute] != lastMinute) { + lastMinute = currentTime[minute] + TimeEvent.MINUTE + } else { + TimeEvent.SECOND + } + } + + private fun parseTime(time: Long): List { + val sdf = SimpleDateFormat(timeFormat, Locale.getDefault()) + return sdf + .format(time) + .split("-".toRegex()) + .map { + it.toInt() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeLineMarker.kt b/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeLineMarker.kt new file mode 100644 index 0000000..057ea40 --- /dev/null +++ b/app/src/main/java/s/yarlykov/fixdataproto/domain/time/TimeLineMarker.kt @@ -0,0 +1,3 @@ +package s.yarlykov.fixdataproto.domain.time + +data class TimeLineMarker(val time: Long, val timeEvent: TimeEvent) \ No newline at end of file diff --git a/app/src/main/java/s/yarlykov/fixdataproto/presentation/MainActivity.kt b/app/src/main/java/s/yarlykov/fixdataproto/presentation/MainActivity.kt index f128a9e..bc42e8c 100644 --- a/app/src/main/java/s/yarlykov/fixdataproto/presentation/MainActivity.kt +++ b/app/src/main/java/s/yarlykov/fixdataproto/presentation/MainActivity.kt @@ -7,6 +7,9 @@ import io.reactivex.disposables.CompositeDisposable import kotlinx.android.synthetic.main.activity_main.* import s.yarlykov.fixdataproto.R import s.yarlykov.fixdataproto.application.TradingApp +import s.yarlykov.fixdataproto.data.FOO_PRICE_MAX +import s.yarlykov.fixdataproto.data.FOO_PRICE_MIN +import s.yarlykov.fixdataproto.domain.ChartOptions import s.yarlykov.fixdataproto.domain.MarketData import java.text.SimpleDateFormat import java.util.* @@ -25,6 +28,14 @@ class MainActivity : AppCompatActivity() { val hub = (application as TradingApp).getHub() + graph.setChartOptions(ChartOptions( + getString(R.string.foo), + "x", + "y", + FOO_PRICE_MIN, + FOO_PRICE_MAX + )) + disposable.add( hub .marketDataStream(getString(R.string.foo)) @@ -32,6 +43,7 @@ class MainActivity : AppCompatActivity() { .subscribe { val message = it.print() tvFoo.text = message + graph.update(it) } ) @@ -55,11 +67,10 @@ class MainActivity : AppCompatActivity() { val sdf = SimpleDateFormat("ss", Locale.getDefault()) - val li = mutableListOf() this.forEach {md -> if(md.value > 0) { - val s = "${"%02d".format(md.value)}: ${sdf.format(md.time)}s" + val s = "${"%02d".format(md.value)}: ${sdf.format(md.marker.time)}s" li.add(s) } } diff --git a/app/src/main/java/s/yarlykov/fixdataproto/presentation/chart/ChartView.kt b/app/src/main/java/s/yarlykov/fixdataproto/presentation/chart/ChartView.kt new file mode 100644 index 0000000..3bbbfd8 --- /dev/null +++ b/app/src/main/java/s/yarlykov/fixdataproto/presentation/chart/ChartView.kt @@ -0,0 +1,9 @@ +package s.yarlykov.fixdataproto.presentation.chart + +import s.yarlykov.fixdataproto.domain.ChartOptions +import s.yarlykov.fixdataproto.domain.MarketData + +interface ChartView { + fun setChartOptions(options : ChartOptions) + fun update(data : List) +} \ No newline at end of file diff --git a/app/src/main/java/s/yarlykov/fixdataproto/presentation/chart/LineChartView.kt b/app/src/main/java/s/yarlykov/fixdataproto/presentation/chart/LineChartView.kt new file mode 100644 index 0000000..94308a3 --- /dev/null +++ b/app/src/main/java/s/yarlykov/fixdataproto/presentation/chart/LineChartView.kt @@ -0,0 +1,169 @@ +package s.yarlykov.fixdataproto.presentation.chart + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import androidx.core.content.res.ResourcesCompat +import s.yarlykov.fixdataproto.R +import s.yarlykov.fixdataproto.domain.ChartOptions +import s.yarlykov.fixdataproto.domain.MarketData +import s.yarlykov.fixdataproto.domain.time.TimeEvent + +class LineChartView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr), ChartView { + + /** + * Все рисование делаем в отдельной битмапе. Потом в onDraw() + * копируем её контент в битмапу нашей View. + * @cacheBitmap + * @cacheCanvas + */ + private lateinit var cacheBitmap: Bitmap + private lateinit var cacheCanvas: Canvas + private lateinit var options: ChartOptions + private var pathAxis = Path() + + /** + * Paddind'и для области рисования и прямоугольник для рисования, который + * содержит координаты (L,T R,B) + */ + private var chartPaddings = Rect() + private var chartArea = Rect() + + /** + * Цвета рамки и области рисования + */ + private val colorChartFrame = ResourcesCompat.getColor(resources, R.color.colorFrame, null) + private val colorChartArea = ResourcesCompat.getColor(resources, R.color.colorBackground, null) + + /** + * Кисти + */ + private val paintChartArea = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = colorChartArea + } + + // Кисть для осей координат и надписей на них + private val paintAxis = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + textAlign = Paint.Align.CENTER + textSize = 55.0f + typeface = Typeface.create("", Typeface.NORMAL) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawBitmap(cacheBitmap, 0f, 0f, null) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + // Чтобы не было утечки памяти, удалить старую битмапу перед созданием новой + if (::cacheBitmap.isInitialized) cacheBitmap.recycle() + + // Пересчитать отступы области рисования + with(chartPaddings) { + left = w / 10 + top = h / 20 + bottom = h / 10 + right = w / 20 + } + + // Пересчитать координаты области рисования + with(chartArea) { + left = chartPaddings.left + top = chartPaddings.top + bottom = h - chartPaddings.bottom + right = w - chartPaddings.right + } + +// pathAxis = createAxisPath(w, h) + + cacheBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) + cacheCanvas = Canvas(cacheBitmap) + + cacheCanvas.drawColor(colorChartFrame) + cacheCanvas.drawRect(chartArea, paintChartArea) +// cacheCanvas.drawPath(pathAxis, paintAxis) + + if (::options.isInitialized) { + decorateChart() + } + } + + override fun setChartOptions(options: ChartOptions) { + this.options = options + } + + override fun update(data: List) { + + // Предыдущие координаты для отрисовки более плавного перехода + // к новым координатам с помощью path.quadTo() + var xPrev = 0f + var yPrev = 0f + + // Область рисования и её смещения внутри View + val w = chartArea.width() + val h = chartArea.height() + val shiftX = chartArea.left.toFloat() + val shiftY = chartArea.top.toFloat() + + // Размер "единицы измерения по каждой из осей + val xStep = w / data.size + val yStep = h / (options.max - options.min) + val yBase = options.min + + val pathChart = Path() + + data.withIndex().forEach { d -> + + val x = (xStep * d.index).toFloat() + shiftX + val y = h - ((d.value.value - yBase) * yStep).toFloat() + shiftY + + if (d.index != 0) { + pathChart.quadTo(xPrev, yPrev, (x + xPrev) / 2, (y + yPrev) / 2) + + if (d.value.marker.timeEvent == TimeEvent.MINUTE) { + cacheCanvas.drawText("m", x, y, paintAxis) + } + } else { + pathChart.moveTo(x, y) + } + + xPrev = x + yPrev = y + } + + cacheCanvas.drawColor(colorChartFrame) + cacheCanvas.drawRect(chartArea, paintChartArea) + cacheCanvas.drawPath(pathChart, paintAxis) + + pathChart.reset() + invalidate() + } + + /** + * Надписи вдоль осей координат + */ + private fun decorateChart() { + + } + + private fun createAxisPath(w: Int, h: Int): Path { + + val paddingH = w.toFloat() / 20f + val paddingV = h.toFloat() / 20f + + return Path().apply { + moveTo(paddingH, paddingV) + lineTo(paddingH, h.toFloat() - paddingV) + lineTo(w - paddingH, h.toFloat() - paddingV) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c878861..e42970b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,6 +7,18 @@ android:layout_height="match_parent" tools:context=".presentation.MainActivity"> + + + + app:layout_constraintTop_toBottomOf="@id/graph" > #008577 #00574B #D81B60 + + #FF858585 + #FFe5e5e5 +