Project Name | Stars | Downloads | Repos Using This | Packages Using This | Most Recent Commit | Total Releases | Latest Release | Open Issues | License | Language |
---|---|---|---|---|---|---|---|---|---|---|
Android Best Practices | 19,837 | 2 years ago | 30 | other | ||||||
Do's and Don'ts for Android development, by Futurice developers | ||||||||||
Androiddevtools | 7,581 | 13 hours ago | 8 | |||||||
收集整理Android开发所需的Android SDK、开发中用到的工具、Android开发教程、Android设计规范,免费的设计素材等。 | ||||||||||
Shadow | 6,926 | 2 months ago | 253 | bsd-3-clause | Java | |||||
零反射全动态Android插件框架 | ||||||||||
Weiciyuan | 2,664 | 7 years ago | 63 | gpl-3.0 | Java | |||||
Sina Weibo Android Client | ||||||||||
Androidprocess | 2,640 | 4 years ago | 15 | apache-2.0 | Java | |||||
判断App位于前台或者后台的6种方法 | ||||||||||
Android Samples | 2,338 | 10 days ago | 7 | apache-2.0 | Java | |||||
Samples demonstrating how to use Maps SDK for Android | ||||||||||
Condom | 2,321 | 4 years ago | apache-2.0 | Java | ||||||
一个超轻超薄的Android工具库,阻止三方SDK中常见的有害行为,而不影响应用自身的功能。(例如严重影响用户体验的『链式唤醒』) | ||||||||||
Open Keychain | 1,882 | 2 months ago | 428 | gpl-3.0 | Java | |||||
OpenKeychain is an OpenPGP implementation for Android. | ||||||||||
Notifyutil | 1,631 | 3 years ago | 13 | apache-2.0 | Java | |||||
通知工具类 | ||||||||||
1,548 | 9 months ago | 12 | Java | |||||||
第三方新浪微博客户端 |
dingding-21 |
kmebin |
MJ Studio |
: , , , , ,
: , , , Dialog, ,
: , , , ,
30
29.0.3
23
30
true
true
release {
signingConfig signingConfigs.release
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
1.8
true
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
1.8
kotlinOptions {
jvmTarget = '1.8'
}
true
true
buildFeatures {
dataBinding true
viewBinding true
}
testOptions {
unitTests.returnDefaultValues = true
unitTests {
includeAndroidResources = true
}
}
lintOptions {
abortOnError false
}
main
defaultmain
push
(`origin`) `push`
Create merge commit
main
pull
push
, apkname: Android Build
on: [push]
defaults:
run:
shell: bash
working-directory: .
jobs:
build:
runs-on: ubuntu-latest
name: InstrumentationTest + Build
if: "!contains(toJSON(github.event.commits.*.message), '[skip action]') && !startsWith(github.ref, 'refs/tags/')"
steps:
- name: Checkout repository
uses: actions/[email protected]
- name: Gradle cache
uses: actions/[email protected]
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
# - uses: actions/[email protected]
# with:
# java-version: '1.8'
# - name: Android Emulator Runner, Test
# uses: ReactiveCircus/[email protected]
# with:
# api-level: 29
# script: ./gradlew connectedCheck
- name: Build Release
if: ${{ contains(github.ref, 'main') }}
run: ./gradlew assembleRelease
- name: Build Debug
if: ${{ !contains(github.ref, 'main') }}
run: ./gradlew assembleDebug
- name: Archive artifacts
uses: actions/[email protected]
with:
path: app/build/outputs
- name: Update Release apk name
if: ${{ success() && contains(github.ref, 'main') }}
run: |
mv app/build/outputs/apk/release/app-release.apk -.apk
echo 'apk=-.apk' >> $GITHUB_ENV
- name: Upload APK
if: ${{ success() && contains(github.ref, 'main') }}
run: |
curl -X POST \
-F [email protected]$apk \
-F channels=${{secrets.SLACK_CHANNEL_ID}} \
-H "Authorization: Bearer ${{secrets.SLACK_BOT_API_TOKEN}}" \
https://slack.com/api/files.upload
- name: On success, Notify with Slack
if: ${{ success() && contains(github.ref, 'main') }}
uses: rtCamp/[email protected]
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_ANDROID }}
SLACK_TITLE: ' '
MSG_MINIMAL: true
SLACK_MESSAGE: 'apk '
- name: On fail, Notify with Slack
if: ${{ failure() }}
uses: rtCamp/[email protected]
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_ANDROID }}
SLACK_TITLE: ' '
MSG_MINIMAL: false
SLACK_MESSAGE: ' '
ImageView
.OkHttp3
Rest API .Activity
Fragment
ConstraintLayout
. MotionLayout
java.time
desugaringclass MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
binding.vm = viewModel
binding.lifecycleOwner = this
setContentView(binding.root)
...
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
initialJob = GlobalScope.launch(Dispatchers.Main) {
...
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
initialJob.cancel()
}
java.time.LocalDate
, java.util.Calendar
fun convertMonthlyIndexToDateToFirstDateOfMonthCalendar(index: Int): Pair<LocalDate, LocalDate> {
val cur = LocalDate.now()
val diffMonth = MonthlyAdapter.MAX_ITEM_COUNT - index - 1
val monthSubtracted = cur.minusMonths(diffMonth.toLong())
val firstDateOfMonth = monthSubtracted.withDayOfMonth(1)
val startIdx = firstDateOfMonth.dayOfWeekIndex
return firstDateOfMonth.minusDays(startIdx.toLong()) to firstDateOfMonth
}
LocationService
, FusedLocationProviderClient
, Geocoder
)LocationManager
SharedPreferences
.
LocationUtil.kt
@SuppressLint("MissingPermission")
class LocationUtil @Inject constructor(app: Application, private val spUtil: SPUtil) : DefaultLifecycleObserver {
private val locationManager = app.getSystemService(LocationManager::class.java)
private val _lastLocation = MutableStateFlow<Location?>(null)
val lastLocation: StateFlow<Location?> = _lastLocation
private val _isOtherPlaceSelected: MutableStateFlow<Boolean> = MutableStateFlow(false)
val isOtherPlaceSelected: StateFlow<Boolean> = _isOtherPlaceSelected
val selectedWeatherLocation: MutableStateFlow<OverviewWeather?> = MutableStateFlow(null)
private var isRegistered = false
override fun onCreate(owner: LifecycleOwner) {
registerLocationListener()
_isOtherPlaceSelected.value = spUtil.isOtherPlaceSelected
}
override fun onDestroy(owner: LifecycleOwner) {
unregisterLocationListener()
}
private val locationListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
_lastLocation.value = location
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
}
override fun onProviderEnabled(provider: String) {
}
override fun onProviderDisabled(provider: String) {
}
}
fun registerLocationListener() {
if (isRegistered) return
debugE("registerLocationListener")
try {
_lastLocation.value = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
val enabledProviders = locationManager.allProviders.filter {
locationManager.isProviderEnabled(it)
}
val provider =
if (LocationManager.GPS_PROVIDER in enabledProviders) LocationManager.GPS_PROVIDER else enabledProviders.first()
locationManager.requestLocationUpdates(provider, 1000, 1f, locationListener)
isRegistered = true
} catch (e: Throwable) {
debugE(e)
}
}
private fun unregisterLocationListener() {
debugE("unregisterLocationListener")
locationManager.removeUpdates(locationListener)
isRegistered = false
}
fun selectPlace(weather: OverviewWeather) {
spUtil.lastSelectedLocationCode = weather.region.code
selectedWeatherLocation.value = weather
spUtil.isOtherPlaceSelected = false
_isOtherPlaceSelected.value = false
}
fun selectOtherPlace(weather: OverviewWeather) {
spUtil.lastSelectedLocationCode = weather.region.code
selectedWeatherLocation.value = weather
spUtil.isOtherPlaceSelected = true
_isOtherPlaceSelected.value = true
}
}
WeathyCardView
)MaterialShapeDrawable ShapeAppearanceModel MDC .
WeathyCardView.kt
class WeathyCardView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
FrameLayout(context, attrs) {
private val defaultShadowColor = Color.BLACK
var radius by OnChangeProp(35.dpFloat) {
updateUI()
}
var shadowColor by OnChangeProp(defaultShadowColor) {
updateUI()
}
var disableShadow by OnChangeProp(false) {
updateUI()
}
var strokeColor by OnChangeProp(Color.TRANSPARENT) {
updateUI()
}
var strokeWidth by OnChangeProp(0f) {
updateUI()
}
var cardBackgroundColor by OnChangeProp(Color.WHITE) {
updateUI()
}
init {
if (attrs != null) {
getStyleableAttrs(attrs)
}
elevation = if (disableShadow) 0f else px(8).toFloat()
updateUI()
}
private fun getStyleableAttrs(attr: AttributeSet) {
context.theme.obtainStyledAttributes(attr, R.styleable.WeathyCardView, 0, 0).use { arr ->
radius = arr.getDimension(R.styleable.WeathyCardView_weathy_radius, 35.dpFloat)
shadowColor = arr.getColor(R.styleable.WeathyCardView_weathy_shadow_color, defaultShadowColor)
disableShadow = arr.getBoolean(R.styleable.WeathyCardView_weathy_disable_shadow, false)
strokeColor = arr.getColor(R.styleable.WeathyCardView_weathy_stroke_color, Color.TRANSPARENT)
strokeWidth = arr.getDimension(R.styleable.WeathyCardView_weathy_stroke_width, 0f)
cardBackgroundColor = arr.getColor(R.styleable.WeathyCardView_weathy_background_color, Color.WHITE)
}
}
private fun updateUI() {
background = MaterialShapeDrawable(ShapeAppearanceModel().withCornerSize(radius)).apply {
fillColor = ColorStateList.valueOf(cardBackgroundColor)
strokeWidth = [email protected]
strokeColor = ColorStateList.valueOf([email protected])
}
setShadowColorIfAvailable(shadowColor)
}
}
CalendarView
) .
, . , index index , two-way binding . .
java.time gradle desugaring LocalDate
, LocalDateTime
.
<team.weathy.view.calendar.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="220dp"
app:layout_constraintTop_toTopOf="parent" />
CalendarView.kt
class CalendarView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
ConstraintLayout(context, attrs) {
private val today = LocalDate.now()
var onDateChangeListener: ((date: LocalDate) -> Unit)? = null
var onSelectedDateChangeListener: ((date: LocalDate) -> Unit)? = null
private val curDateLiveData = MutableLiveData(LocalDate.now())
var curDate: LocalDate by OnChangeProp(LocalDate.now()) {
onCurDateChanged()
}
private val selectedDateLiveData = MutableLiveData(LocalDate.now())
var selectedDate: LocalDate by OnChangeProp(LocalDate.now()) {
selectedDateLiveData.value = it
onSelectedDateChangeListener?.invoke(it)
curDate = it
invalidate()
}
private var rowCount = 4
private val dataLiveData = MutableLiveData<Map<YearMonthFormat, List<CalendarPreview?>>>(mapOf())
var data: Map<YearMonthFormat, List<CalendarPreview?>> by OnChangeProp(mapOf()) {
dataLiveData.value = it
}
var onClickYearMonthText: (() -> Unit)? = null
private val isTodayInCurrentMonth
get() = curDate.year == today.year && curDate.month == today.month
private val isTodayInCurrentWeek
get() = isTodayInCurrentMonth && curDate.weekOfMonth == today.weekOfMonth
private val isSelectedInCurrentWeek
get() = selectedDate.year == curDate.year && selectedDate.month == curDate.month && selectedDate.weekOfMonth == curDate.weekOfMonth
private val animLiveData = MutableLiveData(0f)
private var animValue by OnChangeProp(0f) {
animLiveData.value = it
onAnimValueChanged()
}
private val scrollEnabled = MutableLiveData(false)
private val onScrollToTop = SimpleEventLiveData()
private val collapsedHeight
get() = px(MIN_HEIGHT_DP)
private val expandedHeight
get() = screenHeight - px(EXPAND_MARGIN_BOTTOM_DP)
private val paddingHorizontal = px(24)
private val yearMonthText = TextView(context).apply {
id = ViewCompat.generateViewId()
setTextSize(TypedValue.COMPLEX_UNIT_DIP, 25f)
if (!isInEditMode) typeface = ResourcesCompat.getFont(context, R.font.roboto_medium)
setTextColor(getColor(R.color.main_grey))
gravity = Gravity.CENTER
stateListAnimator = AnimatorInflater.loadStateListAnimator(context, R.animator.pressed_alpha_state_list_anim)
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
topToTop = parentId
leftToLeft = parentId
rightToRight = parentId
topMargin = px(26)
}
setOnDebounceClickListener {
onClickYearMonthText?.invoke()
}
}
private val downArrow = ImageView(context).apply {
id = ViewCompat.generateViewId()
setImageResource(R.drawable.calendar_btn_arrow)
scaleType = FIT_CENTER
layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
topToTop = yearMonthText.id
bottomToBottom = yearMonthText.id
leftToRight = yearMonthText.id
leftMargin = 4.dp
}
}
private val todayButton = ImageButton(context).apply {
setImageResource(R.drawable.ic_today)
scaleType = FIT_CENTER
val outValue = TypedValue()
context.theme.resolveAttribute(attr.selectableItemBackgroundBorderless, outValue, true)
setBackgroundResource(outValue.resourceId)
setOnDebounceClickListener {
curDate = today
selectedDate = today
}
layoutParams = LayoutParams(px(32), px(32)).apply {
setPadding(px(6), px(6), px(6), px(6))
topToTop = yearMonthText.id
bottomToBottom = yearMonthText.id
rightToRight = parentId
rightMargin = px(0)
}
}
private val topDivider = View(context).apply {
id = ViewCompat.generateViewId()
setBackgroundColor(getColor(R.color.sub_grey_5))
layoutParams = LayoutParams(MATCH_PARENT, px(1)).apply {
topToBottom = yearMonthText.id
topMargin = px(11)
}
}
private val weekTextLayout = LinearLayout(context).apply {
id = ViewCompat.generateViewId()
orientation = LinearLayout.HORIZONTAL
weightSum = 7f
layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT).apply {
topToBottom = topDivider.id
topMargin = px(16)
}
}
private val weekTexts = (0..6).map {
TextView(context).apply {
id = ViewCompat.generateViewId()
setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13f)
text = listOf("", "", "", "", "", "", "")[it]
gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT, 1f)
}
}
private var isExpanded = false
private fun expand() {
isExpanded = true
notifyEnableScroll()
enableTouchMonthlyPagerOnly()
springAnim = AnimUtil.runSpringAnimation(animValue * 500f, 500f) {
animValue = it / 500f
}
onExpandedChange()
}
private fun collapse() {
isExpanded = false
notifyDisableScroll()
enableTouchWeeklyPagerOnly()
springAnim = AnimUtil.runSpringAnimation(animValue * 500f, 0f) {
animValue = it / 500f
}
onExpandedChange()
}
private fun onExpandedChange() {
notifyScrollToTop()
invalidate()
}
private val fragmentViewLifecycleOwner
get() = findFragment<Fragment>().viewLifecycleOwner
private val monthlyViewPagerGenerator = {
ViewPager2(context).apply {
layoutParams = LayoutParams(MATCH_PARENT, 0).apply {
topToBottom = weekTextLayout.id
bottomToBottom = parentId
bottomMargin = px(32)
}
adapter = MonthlyAdapter(
animLiveData,
scrollEnabled,
onScrollToTop,
dataLiveData,
selectedDateLiveData,
fragmentViewLifecycleOwner,
) {
if (!it.isFuture()) selectedDate = it
}
setCurrentItem(MonthlyAdapter.MAX_ITEM_COUNT, false)
alpha = 0f
setPageTransformer { page, position ->
page.pivotX = if (position < 0) page.width.toFloat() else 0f
page.pivotY = page.height * 0.5f
page.rotationY = 25f * position
}
registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val (_, firstDateOfMonth) = convertMonthlyIndexToDateToFirstDateOfMonthCalendar(
position
)
if (isExpanded && curDate != firstDateOfMonth) {
curDate = firstDateOfMonth
}
}
})
offscreenPageLimit = 1
}
}
private var monthlyViewPager: ViewPager2? = null
private val weeklyViewPagerGenerator = {
ViewPager2(context).apply {
layoutParams = LayoutParams(MATCH_PARENT, 0).apply {
topToBottom = weekTextLayout.id
height = px(WeeklyView.ITEM_HEIGHT_DP)
}
adapter = WeeklyAdapter(animLiveData, dataLiveData, fragmentViewLifecycleOwner) {
if (!it.isFuture()) selectedDate = it
}
setCurrentItem(WeeklyAdapter.MAX_ITEM_COUNT, false)
setPageTransformer { page, position ->
page.pivotX = if (position < 0) page.width.toFloat() else 0f
page.pivotY = page.height * 0.5f
page.rotationY = 40f * position
}
registerOnPageChangeCallback(object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val newDate = convertWeeklyIndexToFirstDateOfWeekCalendar(position)
if (!isExpanded && curDate != newDate) {
curDate = newDate
}
}
})
offscreenPageLimit = 1
}
}
private var weeklyViewPager: ViewPager2? = null
init {
initContainer()
addViews()
configureExpandGestureHandling()
enableTouchWeeklyPagerOnly()
changeWeekTextsColor()
onCurDateChanged()
}
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private lateinit var lazyPagerAddJob: Job
override fun onAttachedToWindow() {
super.onAttachedToWindow()
lazyPagerAddJob = scope.launch {
if (weeklyViewPager == null) {
weeklyViewPager = weeklyViewPagerGenerator()
TransitionManager.beginDelayedTransition([email protected])
addView(weeklyViewPager!!, 0)
}
delay(400)
if (monthlyViewPager == null) {
monthlyViewPager = monthlyViewPagerGenerator()
addView(monthlyViewPager!!, 0)
}
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
lazyPagerAddJob.cancel()
}
private fun initContainer() {
setPadding(paddingHorizontal, 0, paddingHorizontal, 0)
background = MaterialShapeDrawable(
ShapeAppearanceModel().toBuilder().setBottomLeftCorner(CornerFamily.ROUNDED, px(35).toFloat())
.setBottomRightCorner(CornerFamily.ROUNDED, px(35).toFloat()).build()
).apply {
fillColor = ColorStateList.valueOf(getColor(R.color.white))
}
elevation = px(4).toFloat()
}
private fun addViews() {
addView(yearMonthText)
addView(downArrow)
addView(todayButton)
addView(topDivider)
addWeekLayoutAndWeekTexts()
}
private fun addWeekLayoutAndWeekTexts() = weekTextLayout.also { layout ->
addView(layout)
weekTexts.forEach(layout::addView)
}
private fun onCurDateChanged() {
curDateLiveData.value = curDate
rowCount = calculateRequiredRow(curDate)
setYearMonthTextWithDate(curDate)
selectPagerItemsWithDate(curDate)
onDateChangeListener?.invoke(curDate)
changeWeekTextsColor()
notifyScrollToTop()
invalidate()
}
private fun setYearMonthTextWithDate(date: LocalDate) {
yearMonthText.text = "${date.year} .${date.monthValue}"
}
private fun selectPagerItemsWithDate(date: LocalDate) {
val nextMonthlyIndex = convertDateToMonthlyIndex(date)
val nextWeeklyIndex = convertDateToWeeklyIndex(date)
if (monthlyViewPager?.currentItem != nextMonthlyIndex) {
monthlyViewPager?.setCurrentItem(
nextMonthlyIndex, false
)
}
if (weeklyViewPager?.currentItem != nextWeeklyIndex) {
weeklyViewPager?.setCurrentItem(
nextWeeklyIndex, false
)
}
}
private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = getColor(R.color.main_mint)
}
private val greyCapsulePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = getColor(R.color.sub_grey_5)
setShadowLayer(12f, 0f, 0f, getColor(R.color.sub_grey_5))
}
private val capsulePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = getColor(R.color.main_mint)
setShadowLayer(12f, 0f, 0f, getColor(R.color.main_mint))
}
override fun onDraw(canvas: Canvas) {
// bar
canvas.drawRoundRect(
width / 2f - px(30),
height - pxFloat(16),
width / 2f + px(30),
height - pxFloat(11),
pxFloat(10),
pxFloat(10),
barPaint,
)
// capsule
val widthWithoutPadding = width - paddingHorizontal * 2f
val rawWidth = widthWithoutPadding / 7f
val maxWidth = pxFloat(42)
val capsuleWidth = rawWidth.coerceAtMost(maxWidth)
val capsuleLeftPadding = if (rawWidth >= maxWidth) (rawWidth - maxWidth) / 2f else 0f
val capsuleHeight = pxFloat(64)
val capsuleLeft = paddingHorizontal + capsuleLeftPadding + today.dayOfWeekIndex * rawWidth
val capsuleWidthRadius = capsuleWidth / 2f
val capsuleTop = pxFloat(72)
val greyCapsuleLeft = paddingHorizontal + capsuleLeftPadding + selectedDate.dayOfWeekIndex * rawWidth
if (isSelectedInCurrentWeek) {
canvas.drawRoundRect(
greyCapsuleLeft,
capsuleTop,
greyCapsuleLeft + capsuleWidth,
capsuleTop + capsuleHeight,
capsuleWidthRadius,
capsuleWidthRadius,
greyCapsulePaint
)
}
if (isTodayInCurrentWeek) {
canvas.drawRoundRect(
capsuleLeft,
capsuleTop,
capsuleLeft + capsuleWidth,
capsuleTop + capsuleHeight,
capsuleWidthRadius,
capsuleWidthRadius,
capsulePaint,
)
}
}
private var springAnim: SpringAnimation? = null
private var tracker: VelocityTracker? = null
@SuppressLint("Recycle")
private fun configureExpandGestureHandling() {
setOnTouchListener { view, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
if (event.y <= view.height - px(30)) {
[email protected] false
}
springAnim?.cancel()
tracker?.clear()
tracker = tracker ?: VelocityTracker.obtain()
tracker?.addMovement(event)
}
MotionEvent.ACTION_MOVE -> {
tracker?.apply {
addMovement(event)
computeCurrentVelocity(1000)
}
animValue = ((event.y - collapsedHeight) / (expandedHeight - collapsedHeight)).clamp(0f, 1.2f)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (tracker!!.yVelocity > 0) expand()
else collapse()
tracker?.also {
it.recycle()
tracker = null
}
}
}
true
}
}
private fun onAnimValueChanged() {
animateHeight()
changeWeekTextsColor()
animCapsulePaintAlpha()
animPagersAlpha()
}
private fun animateHeight() = updateLayoutParams<ViewGroup.LayoutParams> {
height = MathUtils.lerp(collapsedHeight.toFloat(), expandedHeight.toFloat(), animValue).toInt()
}
private fun changeWeekTextsColor() {
weekTexts.forEachIndexed { idx, textView ->
textView.setTextColor(
CalendarUtil.getWeekTextColor(
context, idx, animValue, isTodayInCurrentWeek && today.dayOfWeekIndex == idx
)
)
}
}
private fun animCapsulePaintAlpha() {
capsulePaint.alpha = (255 - animValue * 255).toInt().clamp(0, 255)
greyCapsulePaint.alpha = (255 - animValue * 255).toInt().clamp(0, 255)
}
private fun animPagersAlpha() {
weeklyViewPager?.alpha = 1 - animValue
monthlyViewPager?.alpha = animValue
}
private fun notifyDisableScroll() {
scrollEnabled.value = false
}
private fun notifyEnableScroll() {
scrollEnabled.value = true
}
private fun notifyScrollToTop() {
onScrollToTop.emit()
}
private fun enableTouchWeeklyPagerOnly() {
weeklyViewPager?.isUserInputEnabled = true
monthlyViewPager?.isUserInputEnabled = false
if (weeklyViewPager != null && monthlyViewPager != null) {
removeViewAt(0)
removeViewAt(0)
addView(monthlyViewPager, 0)
addView(weeklyViewPager, 1)
}
}
private fun enableTouchMonthlyPagerOnly() {
weeklyViewPager?.isUserInputEnabled = false
monthlyViewPager?.isUserInputEnabled = true
if (weeklyViewPager != null && monthlyViewPager != null) {
removeViewAt(0)
removeViewAt(0)
addView(weeklyViewPager, 0)
addView(monthlyViewPager, 1)
}
}
companion object {
private const val parentId = ConstraintSet.PARENT_ID
private const val MIN_HEIGHT_DP = 224
private const val EXPAND_MARGIN_BOTTOM_DP = 120
}
}
DateTime.kt
fun convertMonthlyIndexToDateToFirstDateOfMonthCalendar(index: Int): Pair<LocalDate, LocalDate> {
val cur = LocalDate.now()
val diffMonth = MonthlyAdapter.MAX_ITEM_COUNT - index - 1
val monthSubtracted = cur.minusMonths(diffMonth.toLong())
val firstDateOfMonth = monthSubtracted.withDayOfMonth(1)
val startIdx = firstDateOfMonth.dayOfWeekIndex
return firstDateOfMonth.minusDays(startIdx.toLong()) to firstDateOfMonth
}
fun convertWeeklyIndexToFirstDateOfWeekCalendar(index: Int): LocalDate {
val cur = LocalDate.now()
val diffWeek = WeeklyAdapter.MAX_ITEM_COUNT - index - 1
val weekSubtracted = cur.minusWeeks(diffWeek.toLong())
val startIdx = weekSubtracted.dayOfWeekIndex
return weekSubtracted.minusDays(startIdx.toLong())
}
fun convertDateToMonthlyIndex(date: LocalDate): Int {
val now = LocalDate.now()
val yearDiff = now.year - date.year
val diffIndex = now.monthValue - date.monthValue + yearDiff * 12
return MonthlyAdapter.MAX_ITEM_COUNT - diffIndex - 1
}
fun convertDateToWeeklyIndex(date: LocalDate): Int {
val now = LocalDate.now()
val nowDayOfWeekIndex = now.dayOfWeekIndex
val nowFirstDayOfWeek = now.minusDays(nowDayOfWeekIndex.toLong())
val dateDayOfWeekIndex = date.dayOfWeekIndex
val dateFirstDayOfWeek = date.minusDays(dateDayOfWeekIndex.toLong())
val weekDiff = dateFirstDayOfWeek.until(nowFirstDayOfWeek, ChronoUnit.WEEKS).toInt()
return WeeklyAdapter.MAX_ITEM_COUNT - weekDiff - 1
}
fun calculateRequiredRow(date: LocalDate): Int {
return (date.lengthOfMonth() + date.withDayOfMonth(1).dayOfWeekIndex - 1) / 7 + 1
}
fun getStartDateStringInCalendar(year: Int, month: Int): DateString {
val date = LocalDate.of(year, month, 1)
val startDayIndex = date.dayOfWeekIndex
val startDateInCalendar = date.minusDays(startDayIndex.toLong())
return startDateInCalendar.dateString
}
fun getEndDateStringInCalendar(year: Int, month: Int): DateString {
val firstDate = LocalDate.of(year, month, 1)
val lastDate = LocalDate.of(year, month, firstDate.lengthOfMonth())
val endDayIndex = lastDate.dayOfWeekIndex
val endDateInCalendar = lastDate.plusDays(6 - endDayIndex.toLong())
return endDateInCalendar.dateString
}
Dexter
.
PermissionUtil.kt
/**
* Dexter
*
* @author MJStudio
* @see android.Manifest.permission
*/
object PermissionUtil {
fun requestLocationPermissions(activity: Activity, listener: PermissionListener) {
requestPermissions(
activity, listOf(
android.Manifest.permission.ACCESS_COARSE_LOCATION,
android.Manifest.permission.ACCESS_FINE_LOCATION,
), listener
)
}
/**
*
*
* @param activity Dexter Activity
* @param listener
* @param permissions [android.Manifest.permission]
*/
private fun requestPermissions(activity: Activity, permissions: Collection<String>, listener: PermissionListener) {
val callbackListener: MultiplePermissionsListener = object : BaseMultiplePermissionsListener() {
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
val deniedPermissions = report.deniedPermissionResponses.map { it.permissionName }
val permanentlyDeniedPermissions =
report.deniedPermissionResponses.filter { it.isPermanentlyDenied }.map { it.permissionName }
// ,
when {
report.areAllPermissionsGranted() -> {
listener.onPermissionGranted()
}
//
report.isAnyPermissionPermanentlyDenied -> {
listener.onAnyPermissionsPermanentlyDeined(deniedPermissions, permanentlyDeniedPermissions)
}
//
else -> {
listener.onPermissionShouldBeGranted(deniedPermissions)
}
}
}
}
/**
* Dexter activity
*/
Dexter.withActivity(activity).withPermissions(permissions).withListener(callbackListener).check()
}
fun openPermissionSettings(context: Context) {
context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
})
}
interface PermissionListener {
/**
* .
*/
fun onPermissionGranted() {}
/**
* .
*
* @param deniedPermissions
*/
fun onPermissionShouldBeGranted(deniedPermissions: List<String>) {}
/**
* .
*
* @param deniedPermissions
* @param permanentDeniedPermissions
*/
fun onAnyPermissionsPermanentlyDeined(
deniedPermissions: List<String>, permanentDeniedPermissions: List<String>
) {
}
}
}
Status bar . Window .
StatusBarUtil.kt
@Suppress("DEPRECATION")
object StatusBarUtil {
fun changeColor(activity: Activity, @ColorInt color: Int) {
activity.window?.run {
statusBarColor = color
}
}
}
Dagger mock api module
Qualifier @Api Api @ApiMock Mocking Api . .
ApiModule.kt
@Qualifier
@Retention(BINARY)
annotation class Api
@Qualifier
@Retention(BINARY)
annotation class ApiMock
@Module
@InstallIn(ApplicationComponent::class)
class ApiModule {
@Provides
@Singleton
fun provideRetrofitProvider(uniqueId: UniqueIdentifier) = ApiFactory(uniqueId)
@Provides
@Singleton
@Api
fun provideAuth(provider: ApiFactory) = provider.createApi(AuthAPI::class)
@Provides
@Singleton
@Api
fun provideCalendar(provider: ApiFactory) = provider.createApi(CalendarAPI::class)
@Provides
@Singleton
@Api
fun provideClothes(provider: ApiFactory) = provider.createApi(ClothesAPI::class)
@Provides
@Singleton
@Api
fun provideUser(provider: ApiFactory) = provider.createApi(UserAPI::class)
@Provides
@Singleton
@Api
fun provideWeather(provider: ApiFactory) = provider.createApi(WeatherAPI::class)
@Provides
@Singleton
@Api
fun provideWeahy(provider: ApiFactory) = provider.createApi(WeathyAPI::class)
}
@Module
@InstallIn(ApplicationComponent::class)
abstract class ApiModuleMock {
@Singleton
@Binds
@ApiMock
abstract fun bindUser(api: MockUserAPI): UserAPI
@Singleton
@Binds
@ApiMock
abstract fun bindCalendar(api: MockCalendarAPI): CalendarAPI
@Singleton
@Binds
@ApiMock
abstract fun bindWeather(api: MockWeatherAPI): WeatherAPI
@Singleton
@Binds
@ApiMock
abstract fun bindWeathy(api: MockWeathyAPI): WeathyAPI
}
DialogFragment
DialogFragment
Dialog . argument , Dialog .
@AndroidEntryPoint
class CommonDialog : DialogFragment() {
private var binding by AutoClearedValue<DialogCommonBinding>()
@Inject
lateinit var pixelRatio: PixelRatio
private val title: String
get() = arguments?.getString("title") ?: ""
private val body: String
get() = arguments?.getString("body") ?: ""
private val btnText: String
get() = arguments?.getString("btnText") ?: ""
private val color: Int
get() = arguments?.getInt("color", getColor(R.color.blue_temp)) ?: getColor(R.color.blue_temp)
private val showCancel: Boolean
get() = arguments?.getBoolean("showCancel") ?: false
private val clickListener: ClickListener?
get() = if (parentFragment == null) (activity as? ClickListener) else (parentFragment as? ClickListener)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
DialogCommonBinding.inflate(inflater, container, false).also {
binding = it
}.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
isCancelable = false
binding.title.text = title
binding.body.text = body
binding.btn.text = btnText
binding.btn setOnDebounceClickListener {
clickListener?.onClickYes()
dismiss()
}
binding.title.setTextColor(color)
binding.btn.backgroundTintList = ColorStateList.valueOf(color)
if (showCancel) {
binding.btn.updateLayoutParams<ConstraintLayout.LayoutParams> {
leftMargin = 13.dp
}
binding.btnCancel.isVisible = true
binding.btnCancel setOnDebounceClickListener {
clickListener?.onClickNo()
dismiss()
}
}
}
override fun onResume() {
super.onResume()
val width = (pixelRatio.screenShort * 0.88f).coerceAtMost(pixelRatio.toPixel(309).toFloat())
dialog?.window?.run {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
setDimAmount(0.2f)
setLayout(width.roundToInt(), WRAP_CONTENT)
}
}
interface ClickListener {
fun onClickYes() {}
fun onClickNo() {}
}
companion object {
fun newInstance(
title: String? = null,
body: String? = null,
btnText: String? = null,
color: Int? = null,
showCancel: Boolean = false
) = CommonDialog().apply {
arguments = bundleOf(
"title" to title, "body" to body, "btnText" to btnText, "color" to color, "showCancel" to showCancel
)
}
}
}
one time event LiveData .
typealias SimpleEventLiveData = EventLiveData<Unit>
##
class EventLiveData<T> : LiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner) {
if (pending.compareAndSet(true, false)) {
observer.onChanged(it)
}
}
}
@MainThread
fun emit(value: T) {
pending.set(true)
setValue(value)
}
}
fun SimpleEventLiveData.emit() {
emit(Unit)
}
SharedFlow /. LiveData Flow lifecycle Flow ViewModel .
fun SimpleSharedFlow() = MutableSharedFlow<Unit>(1, 0, DROP_OLDEST)
fun MutableSharedFlow<Unit>.emit() = tryEmit(Unit)
object AppEvent {
val onWeathyUpdated = SimpleSharedFlow()
}
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
app:layoutDescription="@xml/home_motion">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guide_left"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="26dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guide_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="26dp" />
<ImageView
android:id="@+id/topBlur"
android:layout_width="match_parent"
android:layout_height="78dp"
android:elevation="9dp"
android:outlineProvider="none"
android:scaleType="fitXY"
srcResource="@{vm.weatherSecondBackground}"
app:layout_constraintTop_toTopOf="parent" />
......
WeathyCardView
MaterialShapeDrawable
ShapeAppearanceModel
fragment_home.xml
activity_developer_info.xml