Android 부유 창 기능 구현

19754 단어 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 를 넘 긴 후에 다시 첫 번 째 작업 스 택 으로 돌 아 왔 다.
총결산
위 에서 말 한 것 은 소 편 이 소개 한 안 드 로 이 드 가 부상 창 기능 을 실현 하 는 것 입 니 다.여러분 에 게 도움 이 되 기 를 바 랍 니 다.궁금 한 점 이 있 으 면 메 시 지 를 남 겨 주세요.소 편 은 바로 답 해 드 리 겠 습 니 다.여기 서도 저희 사이트 에 대한 여러분 의 지지 에 감 사 드 립 니 다!
만약 당신 이 본문 이 당신 에 게 도움 이 된다 고 생각한다 면,전 재 를 환영 합 니 다.번 거 로 우 시 겠 지만 출처 를 밝 혀 주 십시오.감사합니다!

좋은 웹페이지 즐겨찾기