مستندات جامع اندروید
نسخه 1.0.3
هدف این لایه، دریافت نقاطی از پایگاه داده و نمایش یک نشانگر به ازای هر کدام از آنها بر روی نقشه است.
فایل database.sqlite
را در پوشه assets
قرار دادهایم.
activity_database_layer.xml
:
در این صفحه، علاوه بر المان نقشه نشان یک ToggleButton
نیز قرار گرفته است که وظیفه آن فعال و غیرفعال کردن لایه پایگاه داده است.
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".activity.DatabaseLayer">
<org.neshan.mapsdk.MapView
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/map"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ToggleButton
android:checked="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/toggle_button_text_color"
android:textOff="لایه دیتابیس"
android:textOn="لایه دیتابیس"
android:elevation="8dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:drawableStart="@drawable/ic_database_layer"
android:drawableTint="@color/toggle_button_text_color"
android:drawablePadding="8dp"
android:background="@drawable/toggle_button_bg"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:targetApi="m"
android:onClick="toggleDatabaseLayer"/>
</android.support.constraint.ConstraintLayout>
AssetDatabaseHelper.kt
:
وظیفه این کلاس، کپی کردن فایل پایگاه داده به حافظه داخلی موبایل، باز کردن پایگاه داده و برگرداندن آن است.
بخشهای این کلاس به طور کامل کامنتگذاری شدهاست. برای کپی کردن پایگاه داده به حافظه داخلی تلفنهمراه میتوانید از این helper استفاده کنید یا helper خود را بنویسید.
package org.neshan.kotlinsample.database_helper
import android.content.Context
import android.database.SQLException
import android.database.sqlite.SQLiteOpenHelper
import android.database.sqlite.SQLiteDatabase
import kotlin.Throws
import android.database.sqlite.SQLiteException
import java.io.FileOutputStream
import java.io.IOException
import java.lang.Error
import kotlin.jvm.Synchronized
class AssetDatabaseHelper(private val myContext: Context) :
SQLiteOpenHelper(myContext, DB_NAME, null, 1) {
private val DB_PATH: String
private var myDataBase: SQLiteDatabase? = null
/**
* Creates a empty database on the system and rewrites it with your own database.
*/
@Throws(IOException::class)
fun createDataBase() {
//By calling this method and empty database will be created into the default system path
//of your application so we are gonna be able to overwrite that database with our database.
this.readableDatabase
try {
copyDataBase()
} catch (e: IOException) {
throw Error("Error copying database")
}
}
/**
* Check if the database already exist to avoid re-copying the file each time you open the application.
*
* @return true if it exists, false if it doesn't
*/
private fun checkDataBase(): Boolean {
var checkDB: SQLiteDatabase? = null
try {
val myPath = myContext.packageCodePath + DB_NAME
checkDB = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READONLY)
} catch (e: SQLiteException) {
//database does't exist yet.
}
checkDB?.close()
return if (checkDB != null) true else false
}
/**
* Copies your database from your local assets-folder to the just created empty database in the
* system folder, from where it can be accessed and handled.
* This is done by transfering bytestream.
*/
@Throws(IOException::class)
private fun copyDataBase() {
//Open your local db as the input stream
val myInput = myContext.assets.open(DB_NAME)
// Path to the just created empty db
val outFileName = DB_PATH + DB_NAME
//Open the empty db as the output stream
val myOutput = FileOutputStream(outFileName, false)
//transfer bytes from the inputfile to the outputfile
val buffer = ByteArray(1024)
var length: Int
while (myInput.read(buffer).also { length = it } > 0) {
myOutput.write(buffer, 0, length)
}
//Close the streams
myOutput.flush()
myOutput.close()
myInput.close()
}
@Throws(SQLException::class)
fun openDataBase(): SQLiteDatabase? {
//Open the database
val myPath = DB_PATH + DB_NAME
myDataBase = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READONLY)
return myDataBase
}
@Synchronized
override fun close() {
if (myDataBase != null) myDataBase!!.close()
super.close()
}
override fun onCreate(db: SQLiteDatabase) {}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
} // Add your public helper methods to access and get content from the database.
// You could return cursors by doing "return myDataBase.query(....)" so it'd be easy
// to you to create adapters for your views.
companion object {
private const val DB_NAME = "database.sqlite"
}
/**
* Constructor
* Takes and keeps a reference of the passed context in order to access to the application assets and resources.
*
* @param context
*/
init {
DB_PATH = myContext.filesDir.path
}
}
DatabaseLayerActivity.kt
یک شی از نوع SQLiteDatabase
برای ذخیره و کار با پایگاه داده نقاط در نظر گرفته میشود.
// our database points
private var pointsDB: SQLiteDatabase? = null
متد initLayoutRefrences
جهت مقداردهی اولیه کردن به تمامی المانهای مربوط به رابط کاربری نوشته شدهاست. به دلیل این که لازم است تا المان اندرویدی نقشه نشان ابتدا به طور کامل ایجاد شود و سپس با آن تعامل برقرار شود (متدهای مختلف بر روی المان نقشه اجرا شود)، تمامی متدهای مربوط به رابط کاربری باید در متد onStart
انجام شوند.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_database_layer)
}
override fun onStart() {
super.onStart()
// everything related to ui is initialized here
initLayoutReferences()
}
پس از مقداردهی اولیه viewها و نقشه، متد getDBPoints
صدا زده میشود که جلوتر توضیح داده خواهد شد.
// Initializing layout references (views, map and map events)
private fun initLayoutReferences() {
// Initializing views
initViews()
// Initializing mapView element
initMap()
// do after 1 secend delay
Handler(Looper.getMainLooper()).postDelayed({
// copy database.sqlite file from asset folder to /data/data/... and read points and add marker on map
getDBPoints()
}, 1000)
}
در این متد، یک شی از کلاس AssetDatabaseHelper
با نام myDbHelper
ساخته میشود. در ادامه متد createDataBase
بر روی شی myDbHelper
صدا زده میشود. در صورت موفق بودن، یک پایگاه داده در حافظه داخلی تلفن همراه ایجاد شده و محتویات فایل database.sqlite
– که در پوشه assets
وجود دارد – به دیتابیس ساخته شده منتقل میشود.
سپس، پایگاه داده ایجاد شده با استفاده از متد openDataBase
باز میشود و شی بازگردانده شده توسط این متد که از نوع SQLiteDatabase
است، در متغیر pointsDB
ذخیره میشود.
در ادامه یک Cursor
ایجاد شده و با استفاده از متد rawQuery
یک کوئری برای دریافت تمام ردیفهای پایگاه داده – در هر ردیف مختصات جغرافیایی یک نقطه وجود دارد – اجرا می شود. با صدا زدن متد addMarker
به ازای هر ردیف از اطلاعات، نشانگری در موقعیت جغرافیایی به دست آمده بر روی نقشه نشان داده خواهد شد. در ادامه متد moveToNext
بر روی cursor
صدا زده می شود تا همین عملیات در مرحله بعد بر روی داده بعدی انجام شود. همنچین هر نشانه ای که به نقشه اضافه میشود به آرایه markers
هم افزوده میشود تا در ادامه جهت حذف یا تغییرات روی نشانه ها در دسترس باشد.
با استفاده از شی cursor
اطلاعات مربوط به lat و lng هر ردیف به دست میآید و یک شی از نوع LatLng
از این اطلاعات ساخته میشود. همزمان، اندازه lat و lng مینیمم و ماکزیمم را برای مشخص کردن محدودهای که قرار است از نقشه نمایش داده شود به دست میآید.
برای محدود کردن ناحیهای از نقشه که قرار است نمایش داده شود، متد moveToCameraBounds
بر روی map
صدا زده میشود. ورودی اول این متد، یک شی از نوع LatLngBounds
است که مختصات نقطه شمال شرق و جنوب غرب به عنوان ورودی به آن داده میشود. دومین آرگومان ورودی این متد، یک ScreenBounds
است که دو ورودی از نوع ScreenPos
به آن داده میشود که اولین ورودی نقطه ۰ و ۰ است و دومین ورودی اندازه عرض و طول نقشه است. به این ترتیب زوم و مکان نقشه به شکلی تنظیم میشود که محدوده مختصات مشخص شده ( پارامتر اول ) در کل بخش نمایشی نقشه ( پارامتر دوم ) به نمایش در بیاید.
// copy database.sqlite file from asset folder to /data/data/... and read points and add marker on map
@SuppressLint("Range")
private fun getDBPoints() {
// we create an AssetDatabaseHelper object, create a new database in mobile storage
// and copy database.sqlite file into the new created database
// Then we open the database and return the SQLiteDatabase object
val myDbHelper = AssetDatabaseHelper(this)
try {
myDbHelper.createDataBase()
} catch (ioe: IOException) {
throw Error("Unable to create database")
}
try {
pointsDB = myDbHelper.openDataBase()
} catch (sqle: SQLException) {
sqle.printStackTrace()
}
// creating a cursor and query all rows of points table
val cursor: Cursor = pointsDB!!.rawQuery("select * from points", null)
//reading all points and adding a marker for each one
if (cursor.moveToFirst()) {
// variable for creating bound
// min = south-west
// max = north-east
var minLat = Double.MAX_VALUE
var minLng = Double.MAX_VALUE
var maxLat = Double.MIN_VALUE
var maxLng = Double.MIN_VALUE
while (!cursor.isAfterLast) {
val lng:Double = cursor.getDouble(cursor.getColumnIndex("lng"))
val lat:Double = cursor.getDouble(cursor.getColumnIndex("lat"))
Log.i("POINTS", "getDBPoints: $lat $lng")
val LatLng = LatLng(lat, lng)
// validating min and max
minLat = Math.min(LatLng.latitude, minLat)
minLng = Math.min(LatLng.longitude, minLng)
maxLat = Math.max(LatLng.latitude, maxLat)
maxLng = Math.max(LatLng.longitude, maxLng)
markers.add(addMarker(LatLng))
cursor.moveToNext()
}
map.moveToCameraBounds(
LatLngBounds(LatLng(minLat, minLng), LatLng(maxLat, maxLng)),
ScreenBounds(
ScreenPos(0f, 0f), ScreenPos(map.height.toFloat(), map.height.toFloat())
),
true, 0.25f
)
Log.i("BOUND", "getDBPoints: $minLat $minLng----$maxLat $maxLng")
}
cursor.close()
}
این متد برای اضافهکردن نشانگر به نقشه استفاده میشود که در صفحات پیش، توضیح داده شده است.
// This method gets a LatLng as input and adds a marker on that position
private fun addMarker(loc: LatLng): Marker {
// Creating animation for marker. We should use an object of type AnimationStyleBuilder, set
// all animation features on it and then call buildStyle() method that returns an object of type
// AnimationStyle
val animStBl = AnimationStyleBuilder()
animStBl.fadeAnimationType = AnimationType.ANIMATION_TYPE_SMOOTHSTEP
animStBl.sizeAnimationType = AnimationType.ANIMATION_TYPE_SPRING
animStBl.phaseInDuration = 0.5f
animStBl.phaseOutDuration = 0.5f
val animSt = animStBl.buildStyle()
// Creating marker style. We should use an object of type MarkerStyleCreator, set all features on it
// and then call buildStyle method on it. This method returns an object of type MarkerStyle
val markStCr = MarkerStyleBuilder()
markStCr.size = 30f
markStCr.bitmap = BitmapUtils.createBitmapFromAndroidBitmap(
BitmapFactory.decodeResource(
resources, R.drawable.ic_marker
)
)
// AnimationStyle object - that was created before - is used here
markStCr.animationStyle = animSt
val markSt = markStCr.buildStyle()
// Creating marker
val marker = Marker(loc, markSt)
// Adding marker to map!
map.addMarker(marker)
return marker
}
با هر بار انتخاب یا لغو انتخاب ToggleButton
موجود در رابط کاربری، این متد صدا زده میشود. در این متد بررسی میشود که اگر toggleButton
انتخاب شده باشد، متد getDBPoints
صدا زده شود و در غیر این صورت لایه پایگاه داده از مجموعه لایههای نقشه حذف شود.
fun toggleDatabaseLayer(view: View) {
val toggleButton = view as ToggleButton
if (toggleButton.isChecked) getDBPoints() else for (marker in markers) {
map.removeMarker(marker)
}
}