Android 부유 창 기능 구현
우 리 는 대부분 두 가지 상황 에서 부상 창 을 볼 수 있다.하 나 는 영상 통화 시의 부상 창 이 고 다른 하 나 는 360 위 사의 부상 구 이다.이 기능 을 실현 하 는 방식 이 비교적 많다.여 기 는 영상 통화 부상 창 에서 의 수 요 를 예 로 들 자.인 코딩 은 Kotlin 을 사용 합 니 다.자바 버 전 메모 메 일 로 보 내주 시 면 됩 니 다.
비 즈 니스 장면
위 챗 영상 통 화 를 예 로 들 면 영상 통 화 를 할 때 우 리 는 다른 앱 을 열거 나 홈 키 를 눌 러 종료 할 때 또는 크기 조정 아이콘 을 누 르 면 부상 창 이 다른 앱 에 표시 되 고 통화 페이지 가 작 아 졌 다 는 허상 을 주 며 부상 창 을 눌 러 페이지 를 통 해 돌아 와 부상 창 이 사라 진다.통화 페이지 부상 창 을 종료 하고 사라 집 니 다.
비 즈 니스 장면 기술 분석
인 코딩 을 하기 전에 우 리 는 절 차 를 잘 정리 해 야 인 코딩 의 실현 에 더욱 유리 하 다.하나의 기능 을 실현 하 는 데 10 분 이 걸 리 면 생각 하 는 시간 은 7 분 이 고 인 코딩 이 차지 하 는 시간 은 3 분 이다.
1.부상 창 은 다른 응용 프로그램 이나 launchers 에 표시 할 수 있 습 니 다.이것 은 반드시 부상 창 권한 이 필요 합 니 다.부상 창 권한 은 특수 권한 에 속 하기 때문에 사용자 로 하여 금 위험 권한 처럼 직접 신청 할 수 없 도록 유도 할 수 있 습 니 다.백 엔 드 디 스 플레이 를 할 수 있 으 면 부상 창 이 Service 라 는 것 을 설명 합 니 다.
2.통화 페이지 가 숨 어 있 을 때 부상 창 이 나타 나 고 통화 페이지 가 나타 날 때 부상 창 이 숨겨 져 있 으 며 부상 창 과 Activity 의 생명주기 가 연결 되 어 있 음 을 알 수 있 기 때문에 부상 창의 Service 와 통화 페이지 의 Activity 는 bid 를 통 해 연 결 됩 니 다.
3.Service 와 Activity 가 bind 를 통 해 연결 되 어 있 는 이상 부상 창 이 표 시 될 때 통화 Activity 가 보이 지 않 지만 실행 되 고 있다 는 것 을 설명 합 니 다.
상술 한 기술 문제 분석 과 결합 하여,우 리 는 거꾸로 서술 하여 일일이 인 코딩 을 통 해 실현 한다.
부상 창 실현 방안
실현 효과
준비 작업
우선 새 항목 을 만 듭 니 다.프로젝트 에 두 개의 Activity 가 있 습 니 다.두 번 째 Activity 에서 통화 시 뮬 레이 션 페이지 를 만 듭 니 다.두 번 째 페이지 에 있 는 이 유 는 나중에 말씀 드 리 겠 습 니 다.
어떻게 acitivity 를 배경 에 놓 습 니까?
사실 매우 간단 하 니,우 리 는 하나의 방법 을 호출 하면 된다.
moveTaskToBack(true);
이 방법의 의 미 는 현재 의 퀘 스 트 전 을 백 스테이지 에 두 는 것 입 니 다.so,왜 제 가 두 번 째 Activity 에서 실현 해 야 하 는 이유 중 하나 입 니 다.기본 적 인 Activity 의 시작 모델 은 표준 모델 이 고 위의 방법 은 퀘 스 트 스 택 을 단독 Activity 가 아 닌 백 스테이지 에 두 기 때 문 입 니 다.그래서 우 리 는 부상 창 을 표시 할 때 조작 소프트웨어 의 다른 기능 에 영향 을 주지 않 습 니 다.우 리 는 통화 페이지 의 Activity 를 single Instance 로 설정 하려 고 합 니 다.이렇게 하면 위의 방법 을 호출 할 때 통화 페이지 가 있 는 Activity 스 택 을 배경 에 두 는 것 입 니 다.시작 모드 를 모 르 면 이전 글 인 Activity 의 시작 모드 로 이동 할 수 있 습 니 다.우 리 는 현재 오른쪽 위 에 있 는 클릭 이벤트 에 상기 코드 를 추가 하면 통화 페이지 의 Activity 가 배경 에서 실행 되 고 있 음 을 볼 수 있 습 니 다.
부상 창 권한 이 있 는 지 판단 하기
왼쪽 상단 아이콘 을 누 르 면 현재 app 에 부상 창 권한 이 있 는 지 판단 해 야 합 니 다.먼저 설정 파일 에'부상 창 권한'을 추가 합 니 다.
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
(많은 글 제목 은 부상 창 이 권한 을 어떻게 돌아 가 는 지,어떤 설정 유형 이 TOAST 나 PHONE 인지,불가능 한 일 을 말 하고 싶 습 니 다.TOAST 유형의 일부 모델 은 표시 할 수 있 지만 일반적인 TOSAT 는 자동 으로 사라 집 니 다)그러면 우 리 는 어떻게 부상 창 권한 이 있 는 지 판단 합 니까?이 서로 다른 제조 업 체 의 처리 방안 은 다 를 수 있 습 니 다.여기 서 우 리 는 통용 되 는 처리 방안 을 사용 하여(vivo 부분)이 무효 라 는 것 을 제외 하고 다른 대부분 기종 이 ok 임 을 나타 냅 니 다.그리고 비보 의 일부 기종 인 위 챗 통화 에 도 알림 이 뜨 지 않 습 니 다.(안심 합 니 다~)
fun zoom(v: View) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, " , ", Toast.LENGTH_SHORT)
GlobalDialogSingle(this, "", " ", " ", DialogInterface.OnClickListener { dialog, which ->
dialog.dismiss()
startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0)
}).show()
} else {
moveTaskToBack(true)
val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)
hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)
}
}
}
우 리 는Settings.canDrawOverlays(this)
을 통 해 현재 응용 프로그램 에 부상 창 권한 이 있 는 지 판단 합 니 다.없 으 면 창 알림 을 통 해
startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0)
부상 창 열기 권한 페이지 로 이동 합 니 다.부상 창 권한 이 열 렸 다 면 현재 퀘 스 트 스 택 을 배경 에 두 고 서 비 스 를 시작 하면 됩 니 다.사실 리 턴 방법 은 우리 에 게 성공 권한 을 수 여 받 았 는 지 직접 알려 주지 않 았 기 때문에 우 리 는 리 턴 에서 다시 판단 해 야 한다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
if (requestCode == 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, " ", Toast.LENGTH_SHORT).show()
} else {
Handler().postDelayed({
val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)
intent.putExtra("rangeTime", rangeTime)
hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)
moveTaskToBack(true)
}, 1000)
}
}
}
}
여기 서 우 리 는 리 셋 에서 1 초 지연 되 는 것 을 볼 수 있다.테스트 에서 일부 기종 의 반응 이'너무 빠르다'는 것 을 발 견 했 기 때문에 리 셋 을 받 았 을 때 권한 이 성공 하지 못 한 줄 알 았 는데 사실은 이미 성공 했다.바 인 딩 서비스 에 ServiceConnection 대상 이 필요 합 니 다.
internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
//
val binder = service as FloatWinfowServices.MyBinder
binder.service
}
override fun onServiceDisconnected(name: ComponentName) {}
}
Main2Activity 의 전체 코드 는 다음 과 같 습 니 다.
/**
* @author Huanglinqing
*/
class Main2Activity : AppCompatActivity() {
private val chronometer: Chronometer? = null
private var hasBind = false
private val rangeTime: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main2)
}
fun zoom(v: View) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, " , ", Toast.LENGTH_SHORT)
GlobalDialogSingle(this, "", " ", " ", DialogInterface.OnClickListener { dialog, which ->
dialog.dismiss()
startActivityForResult(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + packageName)), 0)
}).show()
} else {
moveTaskToBack(true)
val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)
hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)
}
}
}
internal var mVideoServiceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
//
val binder = service as FloatWinfowServices.MyBinder
binder.service
}
override fun onServiceDisconnected(name: ComponentName) {}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
if (requestCode == 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Toast.makeText(this, " ", Toast.LENGTH_SHORT).show()
} else {
Handler().postDelayed({
val intent = Intent(this@Main2Activity, FloatWinfowServices::class.java)
intent.putExtra("rangeTime", rangeTime)
hasBind = bindService(intent, mVideoServiceConnection, Context.BIND_AUTO_CREATE)
moveTaskToBack(true)
}, 1000)
}
}
}
}
override fun onRestart() {
super.onRestart()
Log.d("RemoteView", " ")
//
if (hasBind) {
unbindService(mVideoServiceConnection)
hasBind = false
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
}
override fun onDestroy() {
super.onDestroy()
}
}
새 부상 창 서비스새 부상 창 Service Float Winfow Services 를 만 듭 니 다.저희 가 사용 하 는 BindService 때문에 onBind 방법 에서 service 의 레이아웃 을 초기 화 합 니 다.
override fun onBind(intent: Intent): IBinder? {
initWindow()
//
initFloating()
return MyBinder()
}
service 에서 저 희 는 Window Manager 를 통 해 레이아웃 디 스 플레이 를 추가 합 니 다.
/**
*
*/
private fun initWindow() {
winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager
//
wmParams = params
//
wmParams!!.gravity = Gravity.LEFT or Gravity.TOP
// , , x=0;y=0
wmParams!!.x = winManager!!.defaultDisplay.width
wmParams!!.y = 210
// , inflater
inflater = LayoutInflater.from(applicationContext)
//
mFloatingLayout = inflater!!.inflate(R.layout.remoteview, null)
//
winManager!!.addView(mFloatingLayout, wmParams)
}
부상 창의 매개 변 수 는 주로 부상 창의 유형 을 설정 합 니 다.
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
8.0 이하 설정 가능:
wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE
코드 는 다음 과 같다.
private // window type 2002 ,2003
//
//
val params: WindowManager.LayoutParams
get() {
wmParams = WindowManager.LayoutParams()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE
}
wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
wmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENT
wmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENT
return wmParams
}
부상 창 을 클릭 할 때 Activity 2 페이지 로 돌아 가 고 부상 창 이 사라 지기 때문에 우 리 는 부상 창 에 클릭 이벤트 만 추가 해 야 합 니 다.
linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }
Service 가 onDestory 에 도 착 했 을 때 view 를 제거 합 니 다.Activity 2 페이지 에 서 는 onResume 일 때 Service 를 풀 고 onstop 일 때 Service 를 연결 합 니 다.효과 도 에서 우 리 는 부상 창 을 끌 어 당 길 수 있 는 것 을 볼 수 있 기 때문에 터치 이 벤트 를 설정 해 야 한다.이동 거리 가 특정한 값 을 초과 할 때 onTouch 소비 이 벤트 를 사용 하면 클릭 이 벤트 를 촉발 하지 않 는 다.이것 은 view 의 비교적 기본 적 인 지식 이 라 고 할 수 있 으 니 모두 가 알 것 이 라 고 믿 습 니 다.
// , ( )
private var mTouchStartX: Int = 0
private var mTouchStartY: Int = 0
private var mTouchCurrentX: Int = 0
private var mTouchCurrentY: Int = 0
// ( )
private var mStartX: Int = 0
private var mStartY: Int = 0
private var mStopX: Int = 0
private var mStopY: Int = 0
// , ,
private var isMove: Boolean = false
private inner class FloatingListener : View.OnTouchListener {
override fun onTouch(v: View, event: MotionEvent): Boolean {
val action = event.action
when (action) {
MotionEvent.ACTION_DOWN -> {
isMove = false
mTouchStartX = event.rawX.toInt()
mTouchStartY = event.rawY.toInt()
mStartX = event.x.toInt()
mStartY = event.y.toInt()
}
MotionEvent.ACTION_MOVE -> {
mTouchCurrentX = event.rawX.toInt()
mTouchCurrentY = event.rawY.toInt()
wmParams!!.x += mTouchCurrentX - mTouchStartX
wmParams!!.y += mTouchCurrentY - mTouchStartY
winManager!!.updateViewLayout(mFloatingLayout, wmParams)
mTouchStartX = mTouchCurrentX
mTouchStartY = mTouchCurrentY
}
MotionEvent.ACTION_UP -> {
mStopX = event.x.toInt()
mStopY = event.y.toInt()
if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {
isMove = true
}
}
else -> {
}
}
// OnClick ,
return isMove
}
}
Float Winfow Services 의 모든 코드 는 다음 과 같 습 니 다.
class FloatWinfowServices : Service() {
private var winManager: WindowManager? = null
private var wmParams: WindowManager.LayoutParams? = null
private var inflater: LayoutInflater? = null
//
private var mFloatingLayout: View? = null
private var linearLayout: LinearLayout? = null
private var chronometer: Chronometer? = null
override fun onBind(intent: Intent): IBinder? {
initWindow()
//
initFloating()
return MyBinder()
}
inner class MyBinder : Binder() {
val service: FloatWinfowServices
get() = this@FloatWinfowServices
}
override fun onCreate() {
super.onCreate()
}
/**
*
*/
private fun initFloating() {
linearLayout = mFloatingLayout!!.findViewById<LinearLayout>(R.id.line1)
linearLayout!!.setOnClickListener { startActivity(Intent(this@FloatWinfowServices, Main2Activity::class.java)) }
// ,
linearLayout!!.setOnTouchListener(FloatingListener())
}
// , ( )
private var mTouchStartX: Int = 0
private var mTouchStartY: Int = 0
private var mTouchCurrentX: Int = 0
private var mTouchCurrentY: Int = 0
// ( )
private var mStartX: Int = 0
private var mStartY: Int = 0
private var mStopX: Int = 0
private var mStopY: Int = 0
// , ,
private var isMove: Boolean = false
private inner class FloatingListener : View.OnTouchListener {
override fun onTouch(v: View, event: MotionEvent): Boolean {
val action = event.action
when (action) {
MotionEvent.ACTION_DOWN -> {
isMove = false
mTouchStartX = event.rawX.toInt()
mTouchStartY = event.rawY.toInt()
mStartX = event.x.toInt()
mStartY = event.y.toInt()
}
MotionEvent.ACTION_MOVE -> {
mTouchCurrentX = event.rawX.toInt()
mTouchCurrentY = event.rawY.toInt()
wmParams!!.x += mTouchCurrentX - mTouchStartX
wmParams!!.y += mTouchCurrentY - mTouchStartY
winManager!!.updateViewLayout(mFloatingLayout, wmParams)
mTouchStartX = mTouchCurrentX
mTouchStartY = mTouchCurrentY
}
MotionEvent.ACTION_UP -> {
mStopX = event.x.toInt()
mStopY = event.y.toInt()
if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {
isMove = true
}
}
else -> {
}
}
// OnClick ,
return isMove
}
}
/**
*
*/
private fun initWindow() {
winManager = application.getSystemService(Context.WINDOW_SERVICE) as WindowManager
//
wmParams = params
//
wmParams!!.gravity = Gravity.LEFT or Gravity.TOP
// , , x=0;y=0
wmParams!!.x = winManager!!.defaultDisplay.width
wmParams!!.y = 210
// , inflater
inflater = LayoutInflater.from(applicationContext)
//
mFloatingLayout = inflater!!.inflate(R.layout.remoteview, null)
chronometer = mFloatingLayout!!.findViewById<Chronometer>(R.id.chronometer)
chronometer!!.start()
//
winManager!!.addView(mFloatingLayout, wmParams)
}
private // window type 2002 ,2003
//
//
val params: WindowManager.LayoutParams
get() {
wmParams = WindowManager.LayoutParams()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
wmParams!!.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
wmParams!!.type = WindowManager.LayoutParams.TYPE_PHONE
}
wmParams!!.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
wmParams!!.width = WindowManager.LayoutParams.WRAP_CONTENT
wmParams!!.height = WindowManager.LayoutParams.WRAP_CONTENT
return wmParams
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
return super.onStartCommand(intent, flags, startId)
}
override fun onDestroy() {
super.onDestroy()
winManager!!.removeView(mFloatingLayout)
}
}
실제 응용 에서 고려 해 야 할 다른 문제 들사용 하 는 과정 에서 우 리 는 반드시 다른 문제 에 부 딪 힐 것 이다.
1.사용자 가 사용 하 는 과정 에서 홈 버튼 을 직접 누 를 수 있 습 니 다.이 럴 때 어떻게 알려 드릴 까요?
문제 의 원인:사용자 가 홈 키 를 누 르 면 개발 자가 홈 키 논 리 를 다시 쓸 수 없 기 때 문 입 니 다.이때 응용 프로그램 이 프론트 데스크 에서 실행 되 지 않 고 창 알림 을 할 수 없습니다.이때 사용자 가 APP 아이콘 을 누 르 면 첫 번 째 스 택 에 들 어 갑 니 다.이때 사용 자 는 통화 페이지 의 입구 에 들 어가 지 않 았 습 니 다.
해결 방안:
첫 번 째 해결 방안 은 우리 가 위 챗 을 본 떠 서 할 수 있다.바로 전체 통화 과정 에서 안내 데스크 톱 알림 을 열 고 사용자 가 알림 을 클릭 할 때 통화 페이지 에 들 어 가 는 것 이다.
두 번 째 솔 루 션 은 응용 프로그램 이 프론트 데스크 에 있 는 지 확인 하 는 것 입 니 다.통화 페이지 가 실 행 될 때 다시 프론트 데스크 로 돌아 가 는 것 을 사용 합 니 다.저 희 는 다른 페이지 로 방송 하고 권한 유 도 를 제시 하면 됩 니 다.
2.사용자 가 통화 페이지(singleInstance 모드)에서 홈 버튼 을 클릭
응용 프로그램 이 배경 에서 실 행 될 때 통화 가 끝나 고 Activity 가 finish 되 었 습 니 다.이때 작업 프로그램 에서 응용 프로그램 을 자 르 면 열 린 것 이 통화 페이지 라 는 것 을 알 수 있 습 니 다!
이 문 제 는 쉽게 말 하면 통화 페이지 에서 누 군 가 를 부 르 고 통화 과정 에서 홈 버튼 을 누 르 고 전 화 를 끊 으 면 작업 프로그램 에서 응용 프로그램 을 자 르 면 다시 이 사람 을 부 를 것 이다.즉,이런 상태 에서 onCreate 방법 으로 다시 돌아 간 것 이다.
문제 발생 원인:
1.통화 페이지 는 single Instance 모드 이기 때문에 두 개의 퀘 스 트 스 택 이 있 습 니 다.홈 키 를 누 른 다음 퀘 스 트 프로그램 에서 되 돌 립 니 다.이때 응용 프로그램 은 두 번 째 퀘 스 트 스 택 만 유지 하고 첫 번 째 퀘 스 트 스 택 과 의 관 계 를 잃 었 습 니 다.finish 이후 첫 번 째 퀘 스 트 스 택 으로 돌아 갈 수 없습니다.
해결 방안:
1.(추천 하지 않 음)통화 페이지 는 single Instance 모드 를 사용 하지 않 습 니 다.이 경우 통화 과정 에서 소프트웨어 의 다른 기능 을 조작 할 수 없 으 며 일반적으로 사용 하지 않 습 니 다.
2.(나의 현재 솔 루 션)태그 위 치 를 설정 하여 현재 통화 중인 지 여 부 를 표시 합 니 다.onCreate 에서 통화 가 끝 났 으 면 과도 페이지(표준 모드)로 넘 어가 면 됩 니 다.과도 페이지 에 finish 를 추가 하면 됩 니 다.과도 페이지 를 추가 하 는 이 유 는 우리 가 이전 페이지 가 어디 인지 모 르 기 때 문 입 니 다.왜냐하면 우 리 는 전 화 를 받 으 면 임의의 페이지 일 수 있 기 때 문 입 니 다.우 리 는 페이지 finsh 를 넘 긴 후에 다시 첫 번 째 작업 스 택 으로 돌 아 왔 다.
총결산
위 에서 말 한 것 은 소 편 이 소개 한 안 드 로 이 드 가 부상 창 기능 을 실현 하 는 것 입 니 다.여러분 에 게 도움 이 되 기 를 바 랍 니 다.궁금 한 점 이 있 으 면 메 시 지 를 남 겨 주세요.소 편 은 바로 답 해 드 리 겠 습 니 다.여기 서도 저희 사이트 에 대한 여러분 의 지지 에 감 사 드 립 니 다!
만약 당신 이 본문 이 당신 에 게 도움 이 된다 고 생각한다 면,전 재 를 환영 합 니 다.번 거 로 우 시 겠 지만 출처 를 밝 혀 주 십시오.감사합니다!
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Kotlin의 기초 - 2부지난 글에서는 Kotlin이 무엇인지, Kotlin의 특징, Kotlin에서 변수 및 데이터 유형을 선언하는 방법과 같은 Kotlin의 기본 개념에 대해 배웠습니다. 유형 변환은 데이터 변수의 한 유형을 다른 데이터...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.