Сегодня первый большой снегопад в моем городе и у меня появилось хорошее настроение наскрябать небольшую статейку с некоторыми фишками из Android разработки.
Надеюсь, что моя статейка окажется полезной и ее не закидают плохими словами злые программисты.
Да именно злые, зло плодится, а вы не знали? Шучу конечно :)
Ну поехали :)
Получение цвета темы
Недавно я писал свою кастомную вьюху и мне нужно было установить цвет по умолчанию primaryColor
и поэтому я создал небольшую Kotlin функцию, которая получает его из темы приложения:
// Kotlin расширение для получения цвета
fun Context.themeColor(@AttrRes attrRes: Int): Int {
val typedValue = TypedValue()
theme.resolveAttribute (attrRes, typedValue, true)
return typedValue.data
}
// получаем primaryColor из темы прилы
context.themeColor(android.R.attr.colorPrimary)
Перевести dp в пиксели
Иногда нам нужно прописать оступ в коде и чтобы мы могли использовать независимые от плотности пиксели я довольно часто пишу следующее Kotlin расширения для своих кастомных вьюшках:
// Kotlin расширение, определенное внутри кастомной вьюшки
private fun Int.dp() = (context.resources.displayMetrics.density * this)
.toInt()
// устанавливаем padding в 10 dp
setPadding(10.dp(), 10.dp(), 10.dp(), 10.dp())
Создание круглого изображения
С этой проблемой я сталкивался ни один раз и поэтому решений довольно много, но я решил показать одно из самых неочевидных по моему мнению, создание кастомной вьюшки:
// отображает круглое изображение
class RoundedImageView @JvmOverloads constructor(
ctx: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatImageView(ctx, attrs, defStyleAttr) {
// обратите внимание, здесь мы не приводим к Int, поэтому возвращается Float
private fun Int.dp() = context.resources.displayMetrics.density * this
override fun draw(canvas: Canvas) {
// создаем Path с полностью закругленным прямоугольником
val path = Path().apply {
val rectF = RectF(0f, 0f, width.toFloat(), height.toFloat())
val radius = 100.dp()
addRoundRect(rectF, radius, radius, Path.Direction.CW)
}
// юзаем clipPath для закругления
canvas.clipPath(path)
super.draw(canvas)
}
}
Использование:
<ru.freeit.personalapp.RoundedImageView
android:id="@+id/avatar_img"
android:layout_width="150dp"
android:layout_height="150dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/pony"
android:scaleType="centerCrop" />
Анимация в кастомных вьюшках
В основном, я использую ValueAnimator
:
override fun onTouchEvent(event: MotionEvent): Boolean {
return when (event.action) {
MotionEvent.ACTION_DOWN -> {
// запускаем анимацию, когда произошло прикосновение к экрану
val animator = ValueAnimator.ofFloat(0f, width.toFloat())
animator.addUpdateListener {
// обновляем ширину и перерисовываем View
animWidth = it.animatedValue as Float
invalidate()
}
// задержка анимации
animator.duration = 400L
animator.doOnEnd {
// возвращает к исходному состоянию
animWidth = 0f
invalidate()
}
animator.start()
true
}
MotionEvent.ACTION_UP -> {
listener.firstOrNull()?.invoke()
true
}
else -> super.onTouchEvent(event)
}
}
override fun dispatchDraw(canvas: Canvas) {
canvas.drawRect(8f, 8f, width - 8f, height - 8f, borderPaint)
// значение animWidth меняется с каждым кадром анимации
canvas.drawRect(0f, 0f, animWidth, height.toFloat(), bgPaint)
super.dispatchDraw(canvas)
}
Здесь я привел простенький пример без отмены предыдущей анимации.
Небольшая оберточка для SharedPreferences
В одном из приложений для простоты использования я реализовал простенькую обертку вокруг SharedPreferences
и до сих пор ее юзаю:
// сохранение и чтение Int значений
interface IntPrefs {
fun saveInt(ket: String, value: Int)
fun int(key: String, default: Int) : Int
}
// сохранение и чтение String значений
interface StringPrefs {
fun saveStr(key: String, value: String)
fun str(key: String, default: String) : String
}
// обертка вокруг SharedPreferences
class LocalPrefsDataSource(ctx: Context) : IntPrefs, StringPrefs {
private val prefsName = "app_prefs"
private val sharedPrefs = ctx.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
override fun saveInt(key: String, value: Int) {
sharedPrefs.edit().putInt(key, value).apply()
}
override fun saveStr(key: String, value: String) {
sharedPrefs.edit().putString(key, value).apply()
}
override fun str(key: String, default: String) : String {
return sharedPrefs.getString(key, default) ?: default
}
override fun int(key: String, default: Int) : Int {
return sharedPrefs.getInt(key, default)
}
}
Добавление навигации по кнопки назад в WebView
Если вы юзали WebView
то вы знаете, что по умолчанию по нажатию на кнопку назад мы просто выйдем из приложения (если конечно backstack
состоит только из одной активити или одного фрагмента).
Для организации навигации в WebView
есть простое решение:
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.ali_express_web_view)
val progressPageLoading = findViewById<CircularProgressIndicator>(R.id.progress_page_loading)
webView.webViewClient = object: WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
progressPageLoading.visibility = View.GONE
}
}
webView.settings.javaScriptEnabled = true
// грузим наш любимый AliExpress :)
webView.loadUrl("https://best.aliexpress.ru/")
}
// переопределяем onKeyDown для корректной навигации по сайтам в WebView
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (event?.action == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (webView.canGoBack()) {
webView.goBack()
} else {
finish()
}
return true
}
}
return super.onKeyDown(keyCode, event)
}
}
Создание кастомного фрагмента для Google Maps
В документации по Google Maps рекомендуется использовать предопределенный фрагмент SupportMapFragment
.
Но бывают ситуации, когда нам нужно кастомизировать View
фрагмента, добавить поверх карты какие-либо элементы и поэтому нужно реализовать свой GoogleMapFragment
:
class GoogleMapFragment : Fragment() {
private var mapView : MapView? = null
private var googleMap: GoogleMap? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = GoogleMapFragmentBinding.inflate(inflater, container, false)
this.mapView = binding.mapView
// MapView имеет методы жизненного цикла которые нужно вызывать
binding.mapView.onCreate(savedInstanceState)
binding.mapView.getMapAsync { googleMap ->
// можем юзать Google Maps API
this.googleMap = googleMap
}
return binding.root
}
override fun onStart() {
super.onStart()
mapView?.onStart()
}
override fun onResume() {
super.onResume()
mapView?.onResume()
}
override fun onPause() {
super.onPause()
mapView?.onPause()
}
override fun onStop() {
super.onStop()
mapView?.onStop()
}
override fun onDestroy() {
super.onDestroy()
mapView?.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView?.onSaveInstanceState(outState)
}
override fun onLowMemory() {
super.onLowMemory()
mapView?.onLowMemory()
}
}
Разметка нашего фрагмента (google_map_fragment.layout
):
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.google.android.gms.maps.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- мы можем добавить что-нибудь поверх карты -->
</FrameLayout>
Заключение
Ну и напоследок я хотел бы поделиться некоторыми моими репозиторчиками:
Kotlin-Algorithms-and-Design-Patterns - я почти каждый день добавляю новые алгоритмы, структуры данных и паттерны проектирования.
LearningApps - репозиторий с мини приложениями, в каждом из которых проработана одна или несколько тем по Android разработке.
Как сказал, один из индийских разработчиков, изучайте и делитесь знаниями!!!