Сегодня первый большой снегопад в моем городе и у меня появилось хорошее настроение наскрябать небольшую статейку с некоторыми фишками из 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()
// также можно реализовать через get
private fun Int.dp
get() = (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) {
private val Int.dp
get() = context.resources.displayMetrics.density * this
private val roundedBitmapPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = 0xff424242.toInt()
}
override fun onDraw(canvas: Canvas) {
// получаем наш bitmap
val drawable = drawable ?: return
if (width == 0 || height == 0) {
return
}
val bitmap = (drawable as BitmapDrawable).bitmap
// делаем его круглым
val roundedBitmap = rounded(bitmap, 100.dp)
// и отрисовываем
canvas.drawBitmap(roundedBitmap, 0f, 0f, null)
}
// cоздаем круглую bitmap'у
private fun rounded(bitmap: Bitmap, radius: Float) : Bitmap {
val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
canvas.drawARGB(0, 0, 0, 0)
val rectF = RectF(0f, 0f, bitmap.width / 1f, bitmap.height / 1f)
canvas.drawRoundRect(rectF, radius, radius, roundedBitmapPaint)
roundedBitmapPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
val rect = Rect(0, 0, bitmap.width, bitmap.height)
canvas.drawBitmap(bitmap, rect, rect, roundedBitmapPaint)
return result
}
}
Использование:
<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 разработке.
Как сказал, один из индийских разработчиков, изучайте и делитесь знаниями!!!
Комментарии (8)
anegin
02.11.2021 17:26+2Вместо fun Int.dp() можно сделать
val Int.dp: Float get() = ...
Тогда можно будет без скобок писать 10.dp
А если dimen-ресурсы в проекте не зависят от конфигурации (поворот, локаль и т.п.), то можно вообще отвязаться от Context
val Int.dp: Float get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics)
w201
03.11.2021 06:54+2Ну и круглое изображение создавать через custom view уже довольно давно не актуально ShapeableImageView вам поможет.
Почти все подобные советы устаревают за пол года :)
anegin
создавать объекты в onDraw() - bad practice, о чем даже студия подсказывает
canvas.clipPath() с закруглением будет без anti-alias, будет видна "лесенка" на закруглениях
KiberneticWorm Автор
Спасибо, 1 пункт исправил, что касается "лесенки" на закруглениях то не совсем понимаю как это можно пофиксить, не могли бы вы подсказать?
anegin
Нужно вызывать path.reset() перед тем как заново его сформировать.
Чтобы было без "лесенки" нужно использовать другой подход, используя PorterDuffXferMode - сначала рисуется исходная картинка, затем сверху рисуется нужная маска с нужным режимом.
ookami_kb
Или взять
RoundedBitmapDrawable
KiberneticWorm Автор
Спасибо, уже изменил подход создания кругляшки)