diff --git a/app/screenshots/marketHoursView_2MarketSessions_320xWRAP_CONTENT_English_LightTheme.png b/app/screenshots/marketHoursView_2MarketSessions_320xWRAP_CONTENT_English_LightTheme.png new file mode 100644 index 0000000..427db2e Binary files /dev/null and b/app/screenshots/marketHoursView_2MarketSessions_320xWRAP_CONTENT_English_LightTheme.png differ diff --git a/app/screenshots/marketHoursView_2MarketSessions_600xWRAP_CONTENT_English_DarkTheme.png b/app/screenshots/marketHoursView_2MarketSessions_600xWRAP_CONTENT_English_DarkTheme.png new file mode 100644 index 0000000..214adc6 Binary files /dev/null and b/app/screenshots/marketHoursView_2MarketSessions_600xWRAP_CONTENT_English_DarkTheme.png differ diff --git a/app/screenshots/marketHoursView_displayTimesWithMinuteOffsets_320xWRAP_CONTENT_English_LightTheme.png b/app/screenshots/marketHoursView_displayTimesWithMinuteOffsets_320xWRAP_CONTENT_English_LightTheme.png new file mode 100644 index 0000000..e9c5f8b Binary files /dev/null and b/app/screenshots/marketHoursView_displayTimesWithMinuteOffsets_320xWRAP_CONTENT_English_LightTheme.png differ diff --git a/app/screenshots/marketHoursView_displayTimesWithMinuteOffsets_600xWRAP_CONTENT_English_DarkTheme.png b/app/screenshots/marketHoursView_displayTimesWithMinuteOffsets_600xWRAP_CONTENT_English_DarkTheme.png new file mode 100644 index 0000000..dde9a75 Binary files /dev/null and b/app/screenshots/marketHoursView_displayTimesWithMinuteOffsets_600xWRAP_CONTENT_English_DarkTheme.png differ diff --git a/app/screenshots/marketHoursView_evenHourDisplayTimes_320xWRAP_CONTENT_English_LightTheme.png b/app/screenshots/marketHoursView_evenHourDisplayTimes_320xWRAP_CONTENT_English_LightTheme.png new file mode 100644 index 0000000..5e7e9eb Binary files /dev/null and b/app/screenshots/marketHoursView_evenHourDisplayTimes_320xWRAP_CONTENT_English_LightTheme.png differ diff --git a/app/screenshots/marketHoursView_evenHourDisplayTimes_600xWRAP_CONTENT_English_DarkTheme.png b/app/screenshots/marketHoursView_evenHourDisplayTimes_600xWRAP_CONTENT_English_DarkTheme.png new file mode 100644 index 0000000..2fa41f4 Binary files /dev/null and b/app/screenshots/marketHoursView_evenHourDisplayTimes_600xWRAP_CONTENT_English_DarkTheme.png differ diff --git a/app/screenshots/marketHoursView_multipleMarketSessions_320xWRAP_CONTENT_English_LightTheme.png b/app/screenshots/marketHoursView_multipleMarketSessions_320xWRAP_CONTENT_English_LightTheme.png new file mode 100644 index 0000000..98effb9 Binary files /dev/null and b/app/screenshots/marketHoursView_multipleMarketSessions_320xWRAP_CONTENT_English_LightTheme.png differ diff --git a/app/screenshots/marketHoursView_multipleMarketSessions_600xWRAP_CONTENT_English_DarkTheme.png b/app/screenshots/marketHoursView_multipleMarketSessions_600xWRAP_CONTENT_English_DarkTheme.png new file mode 100644 index 0000000..cec2fdd Binary files /dev/null and b/app/screenshots/marketHoursView_multipleMarketSessions_600xWRAP_CONTENT_English_DarkTheme.png differ diff --git a/app/screenshots/marketHoursView_oddHourDisplayTimes_320xWRAP_CONTENT_English_LightTheme.png b/app/screenshots/marketHoursView_oddHourDisplayTimes_320xWRAP_CONTENT_English_LightTheme.png new file mode 100644 index 0000000..7b5fca2 Binary files /dev/null and b/app/screenshots/marketHoursView_oddHourDisplayTimes_320xWRAP_CONTENT_English_LightTheme.png differ diff --git a/app/screenshots/marketHoursView_oddHourDisplayTimes_600xWRAP_CONTENT_English_DarkTheme.png b/app/screenshots/marketHoursView_oddHourDisplayTimes_600xWRAP_CONTENT_English_DarkTheme.png new file mode 100644 index 0000000..735cdfb Binary files /dev/null and b/app/screenshots/marketHoursView_oddHourDisplayTimes_600xWRAP_CONTENT_English_DarkTheme.png differ diff --git a/app/screenshots/marketHoursView_oddHourDisplaytimes_WhenCloseToEdgeValues_320xWRAP_CONTENT_English_LightTheme.png b/app/screenshots/marketHoursView_oddHourDisplaytimes_WhenCloseToEdgeValues_320xWRAP_CONTENT_English_LightTheme.png new file mode 100644 index 0000000..420b54e Binary files /dev/null and b/app/screenshots/marketHoursView_oddHourDisplaytimes_WhenCloseToEdgeValues_320xWRAP_CONTENT_English_LightTheme.png differ diff --git a/app/screenshots/marketHoursView_oddHourDisplaytimes_WhenCloseToEdgeValues_600xWRAP_CONTENT_English_DarkTheme.png b/app/screenshots/marketHoursView_oddHourDisplaytimes_WhenCloseToEdgeValues_600xWRAP_CONTENT_English_DarkTheme.png new file mode 100644 index 0000000..79d3d8a Binary files /dev/null and b/app/screenshots/marketHoursView_oddHourDisplaytimes_WhenCloseToEdgeValues_600xWRAP_CONTENT_English_DarkTheme.png differ diff --git a/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTime24_320xWRAP_CONTENT_English_LightTheme.png b/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTime24_320xWRAP_CONTENT_English_LightTheme.png new file mode 100644 index 0000000..b78a657 Binary files /dev/null and b/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTime24_320xWRAP_CONTENT_English_LightTheme.png differ diff --git a/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTime24_600xWRAP_CONTENT_English_DarkTheme.png b/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTime24_600xWRAP_CONTENT_English_DarkTheme.png new file mode 100644 index 0000000..5c54a6c Binary files /dev/null and b/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTime24_600xWRAP_CONTENT_English_DarkTheme.png differ diff --git a/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTimeZero_320xWRAP_CONTENT_English_LightTheme.png b/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTimeZero_320xWRAP_CONTENT_English_LightTheme.png new file mode 100644 index 0000000..53115cc Binary files /dev/null and b/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTimeZero_320xWRAP_CONTENT_English_LightTheme.png differ diff --git a/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTimeZero_600xWRAP_CONTENT_English_DarkTheme.png b/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTimeZero_600xWRAP_CONTENT_English_DarkTheme.png new file mode 100644 index 0000000..12315ec Binary files /dev/null and b/app/screenshots/marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTimeZero_600xWRAP_CONTENT_English_DarkTheme.png differ diff --git a/app/src/androidTest/java/com/xm/screenshot/testing/ScreenshotTestBase.kt b/app/src/androidTest/java/com/xm/screenshot/testing/ScreenshotTestBase.kt index 10286ca..fa7d177 100644 --- a/app/src/androidTest/java/com/xm/screenshot/testing/ScreenshotTestBase.kt +++ b/app/src/androidTest/java/com/xm/screenshot/testing/ScreenshotTestBase.kt @@ -74,7 +74,8 @@ open class ScreenshotTestBase { protected fun getViewConfigCombos(height: Int): List { val devices = ArrayList() - val languages = arrayOf("en", "el") + // here we can add more languages but in order to reduce the number of screenshots for the PR demo we removed 'el' + val languages = arrayOf("en") devices.addAll(generateCombos(languages, TestDevice(DEVICE_1.width, height), false)) devices.addAll(generateCombos(languages, TestDevice(DEVICE_4.width, height), true)) diff --git a/app/src/androidTest/java/com/xm/screenshot/testing/TestMarketHoursView.kt b/app/src/androidTest/java/com/xm/screenshot/testing/TestMarketHoursView.kt new file mode 100644 index 0000000..3599b19 --- /dev/null +++ b/app/src/androidTest/java/com/xm/screenshot/testing/TestMarketHoursView.kt @@ -0,0 +1,195 @@ +package com.xm.screenshot.testing + +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import com.xm.MarketHoursView +import org.junit.Before +import org.junit.Test + +/** + * Created by Christoforos Filippou on 30/03/2019 + */ +class TestMarketHoursView : ScreenshotTestBase() { + + private lateinit var combos: List + + @Before + override fun setUp() { + super.setUp() + + combos = getViewConfigCombos(WRAP_CONTENT) + } + + @Test + fun marketHoursView_evenHourDisplayTimes() { + val marketDisplayInfo = MarketHoursView.MarketDisplayInfo( + listOf( + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(6, 0), + MarketHoursView.DisplayedTime(20, 0) + ) + ), + MarketHoursView.DisplayedTime(14, 0), + true + ) + + for (combo in combos) { + updateResources(combo.locale, combo.isDarkTheme) + createViewAndRecord(marketDisplayInfo, combo) + } + } + + @Test + fun marketHoursView_oddHourDisplayTimes() { + val marketDisplayInfo = MarketHoursView.MarketDisplayInfo( + listOf( + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(7, 0), + MarketHoursView.DisplayedTime(21, 0) + ) + ), + MarketHoursView.DisplayedTime(15, 0), + true + ) + + for (combo in combos) { + updateResources(combo.locale, combo.isDarkTheme) + createViewAndRecord(marketDisplayInfo, combo) + } + } + + @Test + fun marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTimeZero() { + val marketDisplayInfo = MarketHoursView.MarketDisplayInfo( + listOf( + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(0, 0), + MarketHoursView.DisplayedTime(24, 0) + ) + ), + MarketHoursView.DisplayedTime(0, 0), + true + ) + + for (combo in combos) { + updateResources(combo.locale, combo.isDarkTheme) + createViewAndRecord(marketDisplayInfo, combo) + } + } + + @Test + fun marketHoursView_zeroAndTwentyFourHourDisplayTimes_AndCurrentTime24() { + val marketDisplayInfo = MarketHoursView.MarketDisplayInfo( + listOf( + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(0, 0), + MarketHoursView.DisplayedTime(24, 0) + ) + ), + MarketHoursView.DisplayedTime(24, 0), + true + ) + + for (combo in combos) { + updateResources(combo.locale, combo.isDarkTheme) + createViewAndRecord(marketDisplayInfo, combo) + } + } + + @Test + fun marketHoursView_oddHourDisplaytimes_WhenCloseToEdgeValues() { + val marketDisplayInfo = MarketHoursView.MarketDisplayInfo( + listOf( + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(1, 0), + MarketHoursView.DisplayedTime(23, 0) + ) + ), + MarketHoursView.DisplayedTime(1, 0), + true + ) + + for (combo in combos) { + updateResources(combo.locale, combo.isDarkTheme) + createViewAndRecord(marketDisplayInfo, combo) + } + } + + @Test + fun marketHoursView_displayTimesWithMinuteOffsets() { + val marketDisplayInfo = MarketHoursView.MarketDisplayInfo( + listOf( + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(7, 30), + MarketHoursView.DisplayedTime(21, 30) + ) + ), + MarketHoursView.DisplayedTime(6, 30), + false + ) + + for (combo in combos) { + updateResources(combo.locale, combo.isDarkTheme) + createViewAndRecord(marketDisplayInfo, combo) + } + } + + @Test + fun marketHoursView_2MarketSessions() { + val marketDisplayInfo = MarketHoursView.MarketDisplayInfo( + listOf( + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(1, 0), + MarketHoursView.DisplayedTime(5, 0) + ), + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(21, 0), + MarketHoursView.DisplayedTime(24, 0) + ) + ), + MarketHoursView.DisplayedTime(17, 0), + false + ) + + for (combo in combos) { + updateResources(combo.locale, combo.isDarkTheme) + createViewAndRecord(marketDisplayInfo, combo) + } + } + + @Test + fun marketHoursView_multipleMarketSessions() { + val marketDisplayInfo = MarketHoursView.MarketDisplayInfo( + listOf( + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(1, 0), + MarketHoursView.DisplayedTime(5, 0) + ), + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(7, 0), + MarketHoursView.DisplayedTime(18, 0) + ), + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(19, 0), + MarketHoursView.DisplayedTime(20, 30) + ), + MarketHoursView.MarketHoursSession( + MarketHoursView.DisplayedTime(21, 0), + MarketHoursView.DisplayedTime(24, 0) + ) + ), + MarketHoursView.DisplayedTime(18, 30), + false + ) + + for (combo in combos) { + updateResources(combo.locale, combo.isDarkTheme) + createViewAndRecord(marketDisplayInfo, combo) + } + } + + private fun createViewAndRecord(marketDisplayInfo: MarketHoursView.MarketDisplayInfo, device: TestDevice) { + val marketHoursView = MarketHoursView(context) + marketHoursView.setMarketDisplayInfo(marketDisplayInfo) + record(marketHoursView, device) + } +} diff --git a/app/src/main/java/com/xm/MarketHoursView.kt b/app/src/main/java/com/xm/MarketHoursView.kt new file mode 100644 index 0000000..dafc4e7 --- /dev/null +++ b/app/src/main/java/com/xm/MarketHoursView.kt @@ -0,0 +1,195 @@ +package com.xm + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import android.util.AttributeSet +import android.view.View +import android.widget.TextView +import kotlinx.android.synthetic.main.market_hours_view.view.* +import java.util.concurrent.TimeUnit + +/** + * Created by Christoforos Filippou on 30/03/2019 + * + * A view for representing the market sessions on timeline bar. + * The step between hour indicator is two hours. + */ +class MarketHoursView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) { + + data class DisplayedTime(val hour: Int, val minute: Int) + + data class MarketHoursSession(val marketOpenTime: DisplayedTime, + val marketCloseTime: DisplayedTime) + + data class MarketDisplayInfo(val marketHoursSessions: List, + val currentTime: DisplayedTime, + val isOpen: Boolean) + + data class CoordinatesRange(val start: Float, val end: Float) { + constructor(start: Int, end: Int) : this(start.toFloat(), end.toFloat()) + } + + companion object { + private const val HOURS_PER_BLOCK = 2 + } + + private val mCurrentLinePaint: Paint = Paint() + private val mTineRangePaint: Paint = Paint() + + private lateinit var mMarketDisplayInfo: MarketDisplayInfo + + init { + View.inflate(context, R.layout.market_hours_view, this) + // By default drawing for this view is disabled so we have to clear this flag in order to draw + setWillNotDraw(false) + + mCurrentLinePaint.color = ContextCompat.getColor(context, R.color.lineColor) + mCurrentLinePaint.isAntiAlias = true + mCurrentLinePaint.style = Paint.Style.STROKE + mCurrentLinePaint.strokeWidth = context.resources.getDimensionPixelSize(R.dimen.market_hours_current_time_line_width).toFloat() + + mTineRangePaint.color = ContextCompat.getColor(context, R.color.green) + mTineRangePaint.isAntiAlias = true + mTineRangePaint.style = Paint.Style.FILL + mTineRangePaint.strokeWidth = context.resources.getDimensionPixelSize(R.dimen.market_hours_time_bar_height).toFloat() + } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + + drawTimeRanges(canvas) + drawCurrentTimeLine(canvas) + } + + fun setMarketDisplayInfo(marketDisplayInfo: MarketDisplayInfo) { + mMarketDisplayInfo = marketDisplayInfo + //after setting the time ranges apply any changes needed to the the market state text + setUpForMarketStatus(marketDisplayInfo.isOpen) + //invalidate in order to force redraw which will fire drawing the time ranges + invalidate() + } + + private fun setUpForMarketStatus(isOpen: Boolean) { + if (isOpen) { + market_state.text = context.getString(R.string.details_info_market_hours_open) + market_state.setBackgroundColor(ContextCompat.getColor(context, R.color.green)) + } else { + market_state.text = context.getString(R.string.details_info_market_hours_closed) + market_state.setBackgroundColor(ContextCompat.getColor(context, R.color.grey)) + } + } + + private fun drawTimeRanges(canvas: Canvas) { + // Line's width will expand from the specified point upwards and downwards + // So we need to shift the center of it to the center of the time bar view + val barPosition = time_bar.top.toFloat() + (time_bar.height / 2) + + mMarketDisplayInfo.marketHoursSessions.forEach { + val lineRange = getDisplayedTimeRange(it) + canvas.drawLine(lineRange.start, barPosition, lineRange.end, barPosition, mTineRangePaint) + } + } + + private fun drawCurrentTimeLine(canvas: Canvas) { + // find the time block where the current time line should be drawn and normalize the value + val normalizedCurrentTime = normalizeTime(mMarketDisplayInfo.currentTime.hour, + mMarketDisplayInfo.currentTime.minute, + findHourViewsRange(mMarketDisplayInfo.currentTime.hour)) + // calculate the exact x position + val lineXPosition = when (mMarketDisplayInfo.currentTime.hour) { + 0, 24 -> normalizedCurrentTime + else -> normalizedCurrentTime + (mCurrentLinePaint.strokeWidth / 2) //center line to the time line point + } + + //calculate how much the line should expand above and below the time bar + val height = (context.resources.getDimensionPixelSize(R.dimen.market_hours_current_time_line_height).toFloat() - time_bar.height) / 2 + + val lineYStartPosition = time_bar.top.toFloat() - height + val lineYEndPosition = time_bar.bottom.toFloat() + height + + canvas.drawLine(lineXPosition, lineYStartPosition, lineXPosition, lineYEndPosition, mCurrentLinePaint) + } + + private fun getDisplayedTimeRange(marketHoursSession: MarketHoursSession): CoordinatesRange { + + val lineStart = normalizeTime(marketHoursSession.marketOpenTime.hour, + marketHoursSession.marketOpenTime.minute, + findHourViewsRange(marketHoursSession.marketOpenTime.hour)) + + val lineEnd = normalizeTime(marketHoursSession.marketCloseTime.hour, + marketHoursSession.marketCloseTime.minute, + findHourViewsRange(marketHoursSession.marketCloseTime.hour)) + + return CoordinatesRange(lineStart, lineEnd) + } + + /** + * A method to normalize the given time inside the range of a specific 2-hour block + * To do that for a given range [a,b] with x values we use this formula: + * n = (b-a) * ((x(i) - x(min)) / (x(max) - x(min))) + a + */ + private fun normalizeTime(hour: Int, minute: Int, hourCoordinatesRange: CoordinatesRange): Float { + val timeInMinutes = timeToMinutes(hour, minute) + + val maxHour = getRangeMaxHour(hour) + val minHour = getRangeMinHour(hour) + + val normalized = (timeInMinutes - timeToMinutes(minHour)) / (timeToMinutes(maxHour) - timeToMinutes(minHour)) + + return (normalized * (hourCoordinatesRange.end - hourCoordinatesRange.start)) + hourCoordinatesRange.start + } + + private fun timeToMinutes(hour: Int): Float { + return timeToMinutes(hour, 0) + } + + /** + * A method to convert hours an minutes, only in minutes + */ + private fun timeToMinutes(hour: Int, minute: Int): Float { + return (TimeUnit.HOURS.toMinutes(hour.toLong()) + minute).toFloat() + } + + /** + * A method to find the range where the given hour value is + * Every block should start and finish at the middle of the hour numbers except + * the first and last value where the block should start/finish at the start/end of th view + */ + private fun findHourViewsRange(hour: Int): CoordinatesRange { + val minHour = getRangeMinHour(hour) + val maxHour = getRangeMaxHour(hour) + + val leftHourView = findViewById(getHourViewId(minHour)) + val rightHourView = findViewById(getHourViewId(maxHour)) + + return when { + minHour == 0 -> CoordinatesRange(leftHourView.left, rightHourView.left + (rightHourView.width / 2)) + maxHour == 24 -> CoordinatesRange(leftHourView.left + (leftHourView.width / 2), rightHourView.right) + else -> CoordinatesRange(leftHourView.left + (leftHourView.width / 2), rightHourView.left + (rightHourView.width / 2)) + } + } + + //find the smallest hour in the surrounding HOURS_PER_BLOCK block + private fun getRangeMinHour(hour: Int): Int { + return when { + hour == 24 -> hour - HOURS_PER_BLOCK //if it is the max possible hour then subtract by HOURS_PER_BLOCK + (hour % HOURS_PER_BLOCK) == 0 -> hour //this means that the value is already the minimum + else -> Math.max(hour - (hour % HOURS_PER_BLOCK), 0) + } + } + + //find the biggest hour in the surrounding HOURS_PER_BLOCK block + private fun getRangeMaxHour(hour: Int): Int { + return when { + (hour % HOURS_PER_BLOCK) == 0 -> Math.min(hour + HOURS_PER_BLOCK, 24) + else -> Math.min(hour + (hour % HOURS_PER_BLOCK), 24) + } + } + + private fun getHourViewId(hour: Int): Int { + return context.resources.getIdentifier("hour_$hour", "id", context.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/market_hours_view.xml b/app/src/main/res/layout/market_hours_view.xml new file mode 100644 index 0000000..3dd1c7e --- /dev/null +++ b/app/src/main/res/layout/market_hours_view.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file