Unverified Commit 34d56747 authored by Dylan Jones's avatar Dylan Jones
Browse files

Add caching and a slightly better facility detail

parent 6d158a62
Pipeline #5121 passed with stages
in 3 minutes and 51 seconds
...@@ -47,6 +47,7 @@ dependencies { ...@@ -47,6 +47,7 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxjava:2.2.12' implementation 'io.reactivex.rxjava2:rxjava:2.2.12'
implementation 'com.google.android:flexbox:1.1.1' implementation 'com.google.android:flexbox:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
......
package srct.whatsopen package srct.whatsopen
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 //import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith //import org.junit.runner.RunWith
import org.junit.Assert.* import org.junit.Assert.*
...@@ -13,7 +13,7 @@ import org.junit.Assert.* ...@@ -13,7 +13,7 @@ import org.junit.Assert.*
* *
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
@RunWith(AndroidJUnit4::class) //@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
@Test @Test
fun useAppContext() { fun useAppContext() {
......
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution" xmlns:dist="http://schemas.android.com/apk/distribution"
package="srct.whatsopen" > package="srct.whatsopen">
<dist:module dist:instant="true" /> <dist:module dist:instant="true" />
...@@ -13,11 +13,20 @@ ...@@ -13,11 +13,20 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" > android:theme="@style/AppTheme">
<activity
android:name=".FacilityDetailActivity"
android:label="@string/title_activity_facility_detail"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="srct.whatsopen.MainActivity" />
</activity>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar" > android:theme="@style/AppTheme.NoActionBar">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
......
package srct.whatsopen
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_facility_detail.*
class FacilityDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_facility_detail)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
}
...@@ -3,18 +3,16 @@ package srct.whatsopen ...@@ -3,18 +3,16 @@ package srct.whatsopen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.appcompat.app.AppCompatActivity
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.flexbox.FlexboxLayoutManager
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
...@@ -22,14 +20,22 @@ import srct.whatsopen.util.MainViewAdapter ...@@ -22,14 +20,22 @@ import srct.whatsopen.util.MainViewAdapter
import srct.whatsopen.util.WhatsOpenService import srct.whatsopen.util.WhatsOpenService
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private var service: WhatsOpenService? = null private lateinit var service: WhatsOpenService
private var refreshLayout: SwipeRefreshLayout? = null private lateinit var refreshLayout: SwipeRefreshLayout
private lateinit var cache: Cache
private lateinit var okHttpClient: OkHttpClient
private val cacheSize = (8 * 1024 * 1024).toLong() // 8 MB cache
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
cache = Cache(applicationContext.cacheDir, cacheSize)
okHttpClient = OkHttpClient.Builder()
.cache(cache)
.build()
val retrofit = Retrofit.Builder() val retrofit = Retrofit.Builder()
.client(okHttpClient)
.baseUrl("https://api.srct.gmu.edu/whatsopen/v2/") .baseUrl("https://api.srct.gmu.edu/whatsopen/v2/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
...@@ -37,7 +43,10 @@ class MainActivity : AppCompatActivity() { ...@@ -37,7 +43,10 @@ class MainActivity : AppCompatActivity() {
service = retrofit.create(WhatsOpenService::class.java) service = retrofit.create(WhatsOpenService::class.java)
// setup refresh listener // setup refresh listener
refreshLayout = findViewById(R.id.swipe_refresh) refreshLayout = findViewById(R.id.swipe_refresh)
refreshLayout!!.setOnRefreshListener(this::refresh) refreshLayout.setOnRefreshListener {
cache.evictAll() // TODO only evict main API call?
this.refresh()
}
refresh() refresh()
} }
...@@ -60,12 +69,12 @@ class MainActivity : AppCompatActivity() { ...@@ -60,12 +69,12 @@ class MainActivity : AppCompatActivity() {
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private fun refresh() { private fun refresh() {
refreshLayout!!.isRefreshing = true refreshLayout.isRefreshing = true
service!!.getData() service.getData()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ facilities -> .subscribe({ facilities ->
refreshLayout!!.isRefreshing = false refreshLayout.isRefreshing = false
recycle_view.adapter = MainViewAdapter(facilities) recycle_view.adapter = MainViewAdapter(facilities)
}, { err -> }, { err ->
Toast.makeText( Toast.makeText(
......
...@@ -9,8 +9,8 @@ data class Facility( ...@@ -9,8 +9,8 @@ data class Facility(
@SerializedName("main_schedule") val mainSchedule: Schedule, @SerializedName("main_schedule") val mainSchedule: Schedule,
@SerializedName("special_schedules") val specialSchedules: List<Schedule>, @SerializedName("special_schedules") val specialSchedules: List<Schedule>,
@SerializedName("facility_location") val location: Location, @SerializedName("facility_location") val location: Location,
val isFavorite: Boolean, @SerializedName("slug") val slug: String,
val statusDuration: String val isFavorite: Boolean
) { ) {
fun currentSchedule(): Schedule { fun currentSchedule(): Schedule {
val now: Date = Calendar.getInstance(TimeZone.getTimeZone("EST5EDT")).time val now: Date = Calendar.getInstance(TimeZone.getTimeZone("EST5EDT")).time
......
package srct.whatsopen.model package srct.whatsopen.model
import android.util.Log
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import java.util.* import java.util.*
// _Should_ return the number of seconds until the given time. // _Should_ return the number of seconds until the given time.
fun secondsTillTime(hour: Int, minute: Int, day: Int): Int { fun secondsTillTime(hour: Int, minute: Int, day: Int): Int {
val cal = Calendar.getInstance(TimeZone.getTimeZone("EST5EDT")) val cal = Calendar.getInstance(TimeZone.getTimeZone("EST5EDT"))
// Log.d( "timebug", "current: D ${(cal.get(Calendar.DAY_OF_WEEK) + Calendar.MONDAY) % 7} H ${cal.get(Calendar.HOUR_OF_DAY)} M ${cal.get( Calendar.MINUTE )}" ) val days = (day - (cal.get(Calendar.DAY_OF_WEEK) + 5) % 7)
val days = (day - (cal.get(Calendar.DAY_OF_WEEK) + Calendar.MONDAY) % 7)
val hours = hour - cal.get(Calendar.HOUR_OF_DAY) val hours = hour - cal.get(Calendar.HOUR_OF_DAY)
val minutes = minute - cal.get(Calendar.MINUTE) val minutes = minute - cal.get(Calendar.MINUTE)
val seconds = 60 - cal.get(Calendar.SECOND) val seconds = 60 - cal.get(Calendar.SECOND)
// Log.d("timebug", "other : D $day H $hour M $minute")
// Log.d("timebug", "delta : D $days H $hours M $minutes S $seconds")
var time = days * (60 * 60 * 24) + (60 * 60 * hours) + (60 * minutes) + seconds var time = days * (60 * 60 * 24) + (60 * 60 * hours) + (60 * minutes) + seconds
if (time < 0) { if (time < 0) {
time += (7 * 24 * 60 * 60) time += (7 * 24 * 60 * 60)
} // else { }
// Log.d("timebug", " good")
// }
// Log.d( "timebug", "final : D ${time / (24 * 60 * 60)} H ${(time % (24 * 60 * 60)) / (60 * 60)} M ${(time % 3600) / 60} S ${time % 60}\n" )
return time return time
} }
...@@ -28,14 +21,11 @@ fun secondsTillTime(hour: Int, minute: Int, day: Int): Int { ...@@ -28,14 +21,11 @@ fun secondsTillTime(hour: Int, minute: Int, day: Int): Int {
data class OpenTimes( data class OpenTimes(
@SerializedName("start_day") var startDay: Int = 0, @SerializedName("start_day") var startDay: Int = 0,
@SerializedName("end_day") var endDay: Int = 0, @SerializedName("end_day") var endDay: Int = 0,
// These really should be Dates or something but idk exactly how to represent them
@SerializedName("start_time") var startTime: String, @SerializedName("start_time") var startTime: String,
@SerializedName("end_time") var endTime: String @SerializedName("end_time") var endTime: String
) { ) {
fun isOpen(): Boolean { fun isOpen(): Boolean {
// TODO this is _broken_
// Log.d( "srct.whatsopen", "start $startTime end $endTime ttc ${timeTillClose()} tto ${timeTillOpen()}" )
return timeTillClose() < timeTillOpen() return timeTillClose() < timeTillOpen()
} }
...@@ -54,6 +44,19 @@ data class OpenTimes( ...@@ -54,6 +44,19 @@ data class OpenTimes(
val sm = startTime.split(":")[1] val sm = startTime.split(":")[1]
val eh = endTime.split(":")[0].toInt() val eh = endTime.split(":")[0].toInt()
val em = endTime.split(":")[1] val em = endTime.split(":")[1]
return (if (sh < 12) "$sh:${sm}am" else "${(sh - 12)}:${sm}pm") + "-" + (if (eh < 12) "$eh:${em}am" else "${eh - 12}:${em}pm") return (if (sh < 12) "${if (sh == 0) 12 else sh}:${sm}am" else "${if (sh == 12) 12 else sh - 12}:${sm}pm") + "-" + (if (eh < 12) "${if (eh == 0) 12 else eh}:${em}am" else "${if (eh == 12) 12 else eh - 12}:${em}pm")
}
fun getDay(): String {
return when {
startDay == 0 -> "Monday"
startDay == 1 -> "Tuesday"
startDay == 2 -> "Wednesday"
startDay == 3 -> "Thursday"
startDay == 4 -> "Friday"
startDay == 5 -> "Saturday"
startDay == 6 -> "Sunday"
else -> "UnknownDay"
}
} }
} }
\ No newline at end of file
package srct.whatsopen.util package srct.whatsopen.util
import android.util.Log import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import srct.whatsopen.FacilityDetailActivity
import srct.whatsopen.R import srct.whatsopen.R
import srct.whatsopen.model.Facility import srct.whatsopen.model.Facility
class MainViewAdapter(var facilities: List<Facility>) : RecyclerView.Adapter<Holder>() { class MainViewAdapter(var facilities: List<Facility>) : RecyclerView.Adapter<Holder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
// return Holder(View.inflate(parent.context, R.layout.item_facility, null))
return Holder( return Holder(
LayoutInflater.from(parent.context).inflate( LayoutInflater.from(parent.context).inflate(
R.layout.item_facility, R.layout.item_facility,
...@@ -29,13 +29,19 @@ class MainViewAdapter(var facilities: List<Facility>) : RecyclerView.Adapter<Hol ...@@ -29,13 +29,19 @@ class MainViewAdapter(var facilities: List<Facility>) : RecyclerView.Adapter<Hol
override fun onBindViewHolder(holder: Holder, position: Int) { override fun onBindViewHolder(holder: Holder, position: Int) {
val facility = facilities[position] val facility = facilities[position]
holder.itemView.findViewById<TextView>(R.id.facility_title).text = facility.name holder.itemView.findViewById<TextView>(R.id.facility_title).text = facility.name
// Log.d("srct.whatsopen", "title: $facility")
val text: String = if (facility.currentSchedule().isOpen24Hours) { val text: String = if (facility.currentSchedule().isOpen24Hours) {
"Open 24 hours" "Open 24 hours"
} else { } else {
"${if (facility.isOpen()) "Open" else "Closed"}: ${facility.currentHours()}" "${if (facility.isOpen()) "Today: " else "Next Open: ${facility.currentHours()!!.getDay()} "}${facility.currentHours()}"
} }
holder.itemView.findViewById<TextView>(R.id.change_text).text = text holder.itemView.findViewById<TextView>(R.id.change_text).text = text
holder.itemView.setOnClickListener {
run {
val intent = Intent(holder.itemView.context, FacilityDetailActivity::class.java)
intent.putExtra("name", "")
holder.itemView.context.startActivity(intent)
}
}
} }
} }
......
...@@ -2,9 +2,12 @@ package srct.whatsopen.util ...@@ -2,9 +2,12 @@ package srct.whatsopen.util
import io.reactivex.Observable import io.reactivex.Observable
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Headers
import srct.whatsopen.model.Facility import srct.whatsopen.model.Facility
interface WhatsOpenService { interface WhatsOpenService {
// cache for 12 hours by default, allow 1 month stale
@Headers("Cache-Control: public, max-age=43200, max-stale=2628003")
@GET("facilities") @GET("facilities")
fun getData(): Observable<List<Facility>> fun getData(): Observable<List<Facility>>
} }
\ No newline at end of file
...@@ -4,6 +4,6 @@ ...@@ -4,6 +4,6 @@
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path
android:fillColor="#FF000000" android:fillColor="@android:color/background_dark"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/> android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
</vector> </vector>
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_height="192dp"
android:layout_width="match_parent">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:toolbarId="@+id/toolbar"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:layout_scrollInterpolator="@android:anim/decelerate_interpolator"
app:contentScrim="?attr/colorPrimary">
<ImageView
android:id="@+id/facility_bg_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/bg_image_desc"
android:scaleType="centerCrop"
app:layout_collapseMode="parallax" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<TextView
android:id="@+id/open_hours"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Hours" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
...@@ -8,22 +8,14 @@ ...@@ -8,22 +8,14 @@
android:orientation="vertical" android:orientation="vertical"
tools:context=".MainActivity"> tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize" android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"> android:theme="?attr/actionBarTheme" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="@android:color/primary_text_dark"
android:textSize="18sp" />
</androidx.appcompat.widget.Toolbar>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh" android:id="@+id/swipe_refresh"
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
android:id="@+id/facility_title" android:id="@+id/facility_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
tools:text="Facility Name" /> tools:text="Facility Name" />
<TextView <TextView
...@@ -42,6 +43,7 @@ ...@@ -42,6 +43,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:contentDescription="@string/favorited_icon" android:contentDescription="@string/favorited_icon"
app:layout_flexShrink="0"
app:srcCompat="@drawable/ic_favorite_border_24dp" /> app:srcCompat="@drawable/ic_favorite_border_24dp" />
</com.google.android.flexbox.FlexboxLayout> </com.google.android.flexbox.FlexboxLayout>
......
<resources> <resources>
<string name="app_name">What\'s Open</string> <string name="app_name">What\'s Open</string>
<string name="large_text">
"Material is the metaphor.\n\n"
"A material metaphor is the unifying theory of a rationalized space and a system of motion."
"The material is grounded in tactile reality, inspired by the study of paper and ink, yet "
"technologically advanced and open to imagination and magic.\n"
"Surfaces and edges of the material provide visual cues that are grounded in reality. The "
"use of familiar tactile attributes helps users quickly understand affordances. Yet the "
"flexibility of the material creates new affordances that supercede those in the physical "
"world, without breaking the rules of physics.\n"
"The fundamentals of light, surface, and movement are key to conveying how objects move, "
"interact, and exist in space and in relation to each other. Realistic lighting shows "
"seams, divides space, and indicates moving parts.\n\n"
"Bold, graphic, intentional.\n\n"
"The foundational elements of print based design typography, grids, space, scale, color, "
"and use of imagery guide visual treatments. These elements do far more than please the "
"eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
"imagery, large scale typography, and intentional white space create a bold and graphic "
"interface that immerse the user in the experience.\n"
"An emphasis on user actions makes core functionality immediately apparent and provides "
"waypoints for the user.\n\n"
"Motion provides meaning.\n\n"
"Motion respects and reinforces the user as the prime mover. Primary user actions are "
"inflection points that initiate motion, transforming the whole design.\n"
"All action takes place in a single environment. Objects are presented to the user without "
"breaking the continuity of experience even as they transform and reorganize.\n"
"Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
"Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n"
"3D world.\n\n"
"The material environment is a 3D space, which means all objects have x, y, and z "
"dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
"positive z-axis extending towards the viewer. Every sheet of material occupies a single "
"position along the z-axis and has a standard 1dp thickness.\n"
"On the web, the z-axis is used for layering and not for perspective. The 3D world is "
"emulated by manipulating the y-axis.\n\n"
"Light and shadow.\n\n"
"Within the material environment, virtual lights illuminate the scene. Key lights create "
"directional shadows, while ambient light creates soft shadows from all angles.\n"
"Shadows in the material environment are cast by these two light sources. In Android "
"development, shadows occur when light sources are blocked by sheets of material at "
"various positions along the z-axis. On the web, shadows are depicted by manipulating the "
"y-axis only. The following example shows the card with a height of 6dp.\n\n"
"Resting elevation.\n\n"