19 Jul 2019
|
Android
package com.snowdeer.animation.sample.fragment
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.os.Bundle
import android.os.Handler
import android.view.*
import androidx.core.animation.doOnEnd
import androidx.fragment.app.Fragment
import com.snowdeer.animation.sample.R
import kotlinx.android.synthetic.main.fragment_voice_bubble.view.*
class VoiceBubbleFragment : Fragment() {
private val handler = Handler()
private var degree = 0
private var isAnimating = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_voice_bubble, container, false)
playScaleLoopAnimation(view.voice_indicator_1)
handler.postDelayed({
playScaleLoopAnimation(view.voice_indicator_2)
}, 500)
view.rotate_button.setOnClickListener {
rotate(view.voice_layout) {}
}
view.show_bubble_button.setOnClickListener {
if ((degree == 0) || (degree == 180)) {
rotate(view.voice_layout) { fadeIn(view.question_layout) }
} else {
fadeIn(view.question_layout)
}
}
view.hide_bubble_button.setOnClickListener {
if ((degree == 90) || (degree == 270)) {
rotate(view.voice_layout) {}
}
fadeOut(view.question_layout)
}
return view
}
private fun playScaleLoopAnimation(target: View) {
val scaleUpX = ObjectAnimator.ofFloat(target, "scaleX", 2.5f)
val scaleUpY = ObjectAnimator.ofFloat(target, "scaleY", 2.5f)
scaleUpX.apply {
duration = 1000
repeatMode = ObjectAnimator.REVERSE
repeatCount = ObjectAnimator.INFINITE
}
scaleUpY.apply {
duration = 1000
repeatMode = ObjectAnimator.REVERSE
repeatCount = ObjectAnimator.INFINITE
}
val scaleDown = AnimatorSet()
scaleDown.play(scaleUpX).with(scaleUpY)
scaleDown.start()
}
private fun rotate(target: ViewGroup, nextAnim: () -> Unit) {
if (!isAnimating) {
isAnimating = true
val targetDegree = degree + 90
val rotateAnimator = ObjectAnimator.ofFloat(target,
"rotation", degree.toFloat(), targetDegree.toFloat())
rotateAnimator.duration = 1000
rotateAnimator.doOnEnd {
degree = targetDegree
isAnimating = false
nextAnim()
}
rotateAnimator.start()
}
}
private fun fadeIn(target: ViewGroup) {
val animator = ObjectAnimator.ofFloat(target, View.ALPHA, 0F, 1f)
animator.duration = 1000
animator.start()
target.visibility = View.VISIBLE
}
private fun fadeOut(target: ViewGroup) {
val animator = ObjectAnimator.ofFloat(target, View.ALPHA, 1F, 0f)
animator.duration = 1000
animator.start()
}
}
18 Jul 2019
|
Android
package com.snowdeer.animation.sample.component
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
private const val THICKNESS = 20F
class CircleIndicatorView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
var min = 0
var max = 1
var progress = 0
private val paint = Paint()
private val erasePaint = Paint()
init {
paint.apply {
color = Color.WHITE
isAntiAlias = true
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
strokeWidth = THICKNESS
}
}
override fun onDraw(canvas: Canvas?) {
val centerX = measuredWidth / 2
val centerY = measuredHeight / 2
val rectF = RectF(0F, 0F, measuredWidth.toFloat(), measuredHeight.toFloat())
val path = Path()
path.arcTo(rectF, 103F, 2F)
canvas?.drawPath(path, paint)
}
}
04 Jul 2019
|
Android
jacoco는 UnitTest 및 Coverage 레포트를 만들어주는 플러그인(plug-in)입니다.
build.gradle에 다음과 같이 작성하면 gradle 옵션에서 jacoco 레포트 생성을 선택할 수 있습니다.
build.gradle (Module)
excludes는 레포트에서 제외할 파일이나 디렉토리, 패키지입니다. Kotlin이나 Java의 환경에 따라 classDirectories의 디렉토리 위치는 변경될 수 있습니다.
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'jacoco'
jacoco {
toolVersion = '0.8.4'
}
tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
}
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport']) {
reports {
xml.enabled = true
html.enabled = true
}
def excludes = [
'**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*',
'android/**/*.*',
'**/treemanager/component/**',
'**/treemanager/tree/*.*',
'**/activity/**'
]
classDirectories = fileTree(
dir: "$buildDir/intermediates/javac/debug/compileDebugJavaWithJavac/classes",
excludes: excludes
) + fileTree(
dir: "$buildDir/tmp/kotlin-classes/debug",
excludes: excludes
)
sourceDirectories = files([
android.sourceSets.main.java.srcDirs,
"src/main/kotlin"
])
executionData = fileTree(dir: project.buildDir, includes: [
'jacoco/testDebugUnitTest.exec', 'outputs/code_coverage/debugAndroidTest/connected/**/*.ec'
])
}
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.snowdeer.jacoco.example"
minSdkVersion 26
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
buildConfigField "long", "TIMESTAMP", System.currentTimeMillis() + "L"
}
buildTypes {
debug {
testCoverageEnabled true
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
packagingOptions {
pickFirst 'lib/arm64-v8a/*'
pickFirst 'lib/armeabi-v7a/*'
}
buildToolsVersion '28.0.3'
}
그런 다음 gradle 옵션에서 jacocoTestReport 항목을 선택하면 레포트가 만들어집니다.
16 May 2019
|
Android
팝업 형태의 다이얼로그(Dialog)로 실행하는 Directory Picker 입니다.
dialog_directory_picker.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark">
<TextView
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginLeft="12dp"
android:text="@string/directory_picker_title"
android:textColor="@color/colorDialogTitle"
android:textSize="24sp"/>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorCurrentPathBackground">
<TextView
android:id="@+id/current_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_marginLeft="12dp"
android:text="current directory path"
android:textColor="@color/colorCurrentPathText"
android:textSize="16dp"/>
</FrameLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:minHeight="320dp"/>
</LinearLayout>
DirectoryListAdapter.kt
import android.content.Context
import android.os.Environment
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.snowdeer.ftpserver.R
import kotlinx.android.synthetic.main.item_directory.view.*
import java.io.File
interface OnDirectoryListEventListener {
fun onDirectoryChanged(path: String)
}
data class DirectoryItem(val name: String, val path: String)
class DirectoryListAdapter(private val ctx: Context) : RecyclerView.Adapter<ViewHolder>() {
private val list = ArrayList<DirectoryItem>()
private val root = Environment.getExternalStorageDirectory().absolutePath
var currentPath: String = ""
var onDirectoryListEventListener: OnDirectoryListEventListener? = null
fun refresh(path: String) {
list.clear()
val file = File(path)
currentPath = file.path
onDirectoryListEventListener?.onDirectoryChanged(path)
addParentDirectory(file)
if (!file.exists()) {
notifyDataSetChanged()
return
}
val files = file.listFiles()
files?.let {
for (f in it) {
if (f.isHidden) {
continue
}
if (f.isDirectory) {
list.add(DirectoryItem(f.name, f.absolutePath))
}
}
}
notifyDataSetChanged()
}
private fun addParentDirectory(file: File) {
val path = file.path
if ((path == "/") || (path == root)) {
return
}
list.add(DirectoryItem("..", file.parentFile.path))
}
override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
val view: View = LayoutInflater.from(ctx).inflate(R.layout.item_directory, parent, false)
return ViewHolder(view)
}
fun getItem(position: Int): DirectoryItem {
return list[position]
}
override fun getItemCount(): Int {
return list.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(holder.adapterPosition)
holder.name.text = item.name
holder.layout_item.setOnClickListener {
refresh(item.path)
}
}
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val layout_item = view.layout_item!!
val name = view.name!!
}
DirectoryPickerDialog.kt
import android.app.Dialog
import android.os.Bundle
import android.support.v4.app.DialogFragment
import android.support.v4.app.FragmentActivity
import android.support.v7.app.AlertDialog
import android.support.v7.widget.LinearLayoutManager
import android.view.LayoutInflater
import android.widget.Toast
import com.snowdeer.ftpserver.R
import com.snowdeer.ftpserver.SnowPreference
import kotlinx.android.synthetic.main.dialog_directory_picker.view.*
import kotlinx.android.synthetic.main.item_directory.view.*
import java.io.File
import java.util.*
interface OnDirectoryPickerEventListener {
fun onDirectorySelected(path: String)
}
class DirectoryPickerDialog : DialogFragment() {
private lateinit var adapter: DirectoryListAdapter
var onDirectoryPickerEventListener: OnDirectoryPickerEventListener? = null
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val builder = AlertDialog.Builder(Objects.requireNonNull<FragmentActivity>(activity))
val inflater = activity!!.layoutInflater
val view = inflater.inflate(R.layout.dialog_directory_picker, null)
adapter = DirectoryListAdapter(activity!!)
adapter.onDirectoryListEventListener = object : OnDirectoryListEventListener {
override fun onDirectoryChanged(path: String) {
view.current_path.text = path
}
}
view.recycler_view.layoutManager = LinearLayoutManager(activity)
view.recycler_view.adapter = adapter
adapter.refresh(SnowPreference.getDirectory(activity!!))
builder.setView(view)
.setPositiveButton(getString(R.string.directory_picker_ok), null)
.setNeutralButton(getString(R.string.directory_picker_new_directory), null)
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
onDirectoryPickerEventListener?.onDirectorySelected(adapter.currentPath)
dialog.dismiss()
}
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener {
showNewDirectoryDialog(adapter.currentPath)
}
}
return dialog
}
private fun showNewDirectoryDialog(path: String) {
val view = LayoutInflater.from(activity).inflate(R.layout.dialog_new_directory, null)
AlertDialog.Builder(activity!!)
.setTitle(getString(R.string.directory_picker_new_directory))
.setView(view)
.setPositiveButton("Ok") { _, _ ->
val newPath = path + "/" + view.name.text.toString()
if (createDirectory(newPath)) {
adapter.refresh(path)
} else {
Toast.makeText(activity!!, getString(R.string.new_directory_failed), Toast.LENGTH_SHORT).show()
}
}
.show()
}
private fun createDirectory(path: String): Boolean {
val file = File(path)
if (file.exists()) {
return false
}
return file.mkdir()
}
}
14 May 2019
|
Android
안드로이드 버전에 따라 모니터링 방법이 조금씩 다르기 때문에 코드 내에서 분기를 태워줍니다.
AndroidManifest.xml
먼저 AndroidManifest.xml 파일에 permission을 추가해줍니다.
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
WifiConnectivityMonitor.kt
import android.annotation.TargetApi
import android.arch.lifecycle.LiveData
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Context.CONNECTIVITY_SERVICE
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkInfo
import android.net.NetworkRequest
import android.os.Build
class WifiConnectivityMonitor(private val ctx: Context) : LiveData<Boolean>() {
private var connectivityManager: ConnectivityManager = ctx.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
private lateinit var connectivityManagerCallback: ConnectivityManager.NetworkCallback
override fun onActive() {
super.onActive()
updateConnection()
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ->
connectivityManager.registerDefaultNetworkCallback(getConnectivityManagerCallback())
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> lollipopNetworkAvailableRequest()
else -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
ctx.registerReceiver(networkReceiver, IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"))
}
}
}
}
override fun onInactive() {
super.onInactive()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
connectivityManager.unregisterNetworkCallback(connectivityManagerCallback)
} else {
ctx.unregisterReceiver(networkReceiver)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private fun lollipopNetworkAvailableRequest() {
val builder = NetworkRequest.Builder()
.addTransportType(android.net.NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(android.net.NetworkCapabilities.TRANSPORT_WIFI)
connectivityManager.registerNetworkCallback(builder.build(), getConnectivityManagerCallback())
}
private fun getConnectivityManagerCallback(): ConnectivityManager.NetworkCallback {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
connectivityManagerCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network?) {
postValue(true)
}
override fun onLost(network: Network?) {
postValue(false)
}
}
return connectivityManagerCallback
} else {
throw IllegalAccessError("Should not happened")
}
}
private val networkReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
updateConnection()
}
}
private fun updateConnection() {
val activeNetwork: NetworkInfo? = connectivityManager.activeNetworkInfo
postValue(activeNetwork?.isConnected == true)
}
}
WifiConnectivityMonitor 클래스 사용 예시
private fun initWifiMonitor() {
val connectivityMonitor = WifiConnectivityMonitor(this)
connectivityMonitor.observe(this, Observer { isConnected ->
isConnected?.let {
if (it) {
ip_address.text = getWifiIPAddress()
port.visibility = View.VISIBLE
} else {
ip_address.text = getString(R.string.wifi_not_available)
port.visibility = View.GONE
}
}
})
}
private fun getWifiIPAddress(): String {
val wm = getSystemService(Service.WIFI_SERVICE) as WifiManager
return Formatter.formatIpAddress(wm.connectionInfo.ipAddress)
}