이번 포스팅에서는 앞서 포스팅에서 한 ViewPager2에 대해 Fragment를 이용한 방법을 소개하려 합니다.
기본적으로 ViewPager2는 RecyclerView를 토대로 구성되어 있기 때문에 RecyclerView.Adapter<ViewHolder> 형태로
어댑터를 구현하여 붙일 수 있다고 말씀드렸습니다.
하지만 이러한 어댑터를 FragmentStateAdapter를 이용해서 붙인다면, ViewPager2에 보이는 화면이
각각 Fragment로 구성되어 보여질 수 있습니다.
여기에 더해서 TabLayout을 하여 뷰페이징 되는 과정에 탭도 이동할 수 있도록 진행해보겠습니다.
[activity_main.xml]
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
다음과 같이 TabLayout을 설정하는데 ViewPager2가 Top_Bottom을 TabLayout 기준으로 두게하여 화면 구성을 진행합니다.
이제 MainActivity.kt를 볼텐데 우선 코드를 설명하기 전에 생각을 먼저 해봅시다.
1) ViewPager2에 FragmentStateAdapter를 adapter로 꼽는다.
2) TabLayoutMediator를 통해 tabLayout, viewPager를 인자로 넣는다.
3) 콜백으로 받는건 tab, position인데 position에 따라서 tab.text를 달리한다. (+ attach())를 하여 붙인다.
4) FragmentStateAdapter를 구현할 때 상태 변화(Position)에 따라 Fragment 생성부를 리턴하면 된다. (단! getItemCount는 여기 position 갯수와 동일하게 셋팅해야 한다!)
5) createFragment 함수를 구현해서 Fragment를 생성해야 하므로 실제 Fragment는 갯수만큼 만들어줘야 합니다.
(저는 구현할 Fragment 모두 WebView를 보여줄 것이라 WebView를 띄울 Fragment 하나를 만들고 position 값을 넘기면서
각 화면을 구현할 것입니다.)
다음 순서는 그대로 할 필요는 없지만, 실제 순서대로 하게 된다면 아래처럼 진행하면 됩니다.
1) Fragment를 보여줄 xml을 만든다.
[fragment_webview.xml]
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<WebView
android:id="@+id/webView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/backToLastButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/webView"
app:layout_constraintEnd_toEndOf="@id/webView"
app:layout_constraintStart_toStartOf="@id/webView"
app:layout_constraintTop_toTopOf="@id/webView" />
<Button
android:id="@+id/backToLastButton"
android:layout_width="0dp"
android:layout_height="50dp"
android:text="@string/backToLast"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/changeTabNameButton"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/changeTabNameButton"
android:layout_width="0dp"
android:layout_height="50dp"
android:text="@string/changeTabName"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/backToLastButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
사실 여기서 주목할 건 WebView와 ProgressBar 정도가 될 것 같습니다.
화면이 모두 로드되었을때 ProgressBar가 없어지도록 구현할 것이기 때문입니다.
2) 해당 xml을 관리할 Fragment 만들기
[WebViewFragment.kt]
package com.example.part2.chapter1
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.Toast
import androidx.core.content.edit
import androidx.fragment.app.Fragment
import com.example.part2.chapter1.databinding.FragmentWebviewBinding
// Fragment ID로 찾아가는거
class WebViewFragment(private val position: Int, private val webViewUrl: String) : Fragment() {
private lateinit var binding: FragmentWebviewBinding
var listener: OnTabLayoutNameChanged ?= null // WebViewFragment에 있는 리스너 -> 메인액티비티를 바라보도록 설정할 것
companion object {
const val SHARED_PREFERENCE = "WEB_HISTORY"
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// View 반환 -> inflate 진행
binding = FragmentWebviewBinding.inflate(inflater)
return binding.root
}
// onCreateView 다음 작업
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.webView.webViewClient = WebtoonWebViewClient(binding.progressBar) { url ->
activity?.getSharedPreferences(SHARED_PREFERENCE, Context.MODE_PRIVATE)?.edit {
Log.d("WebViewFragment", "tab$position")
putString("tab$position", url)
// commit() // 동기처리 (바로 저장)
}
}
binding.webView.settings.javaScriptEnabled = true
binding.webView.loadUrl(webViewUrl)
// Button 클릭
// 마지막 시점 기록해두기
// Activity에 Fragment가 붙어있음 = 내가 어디 액티비티에 붙어있는지 알 수 있음
binding.backToLastButton.setOnClickListener {
val sharedPreference = activity?.getSharedPreferences(SHARED_PREFERENCE, Context.MODE_PRIVATE)
val url = sharedPreference?.getString("tab$position", "")
if(url.isNullOrEmpty()){
Toast.makeText(context, "마지막 저장 시점이 없습니다.", Toast.LENGTH_SHORT).show()
} else {
binding.webView.loadUrl(url)
}
}
binding.changeTabNameButton.setOnClickListener {
val dialog = AlertDialog.Builder(context)
val editText = EditText(context)
dialog.setView(editText)
dialog.setPositiveButton("저장") { _, _ -> // dialogInterface, which 인데 안쓰니까
// TODO 저장기능
// Fragment도 액티비티에 붙어있는 녀석이기도 하고 확인버튼 눌렀을때 이름 변환 리스너에 대한 결과 처리는 Fragment가 붙어있는 액티비티에서 구현
activity?.getSharedPreferences(SHARED_PREFERENCE, Context.MODE_PRIVATE)?.edit {
putString("tab${position}_name", editText.text.toString())
listener?.nameChanged(position, "tab${position}_name") // 저장 누를때 현재 위치와 이름 보내기 (-> 메인액티비티로 보내기)
}
}
dialog.setNegativeButton("취소") { dialogInterface, _ ->
dialogInterface.cancel()
}
dialog.show()
}
}
fun canGoBack(): Boolean {
return binding.webView.canGoBack()
}
fun goBack() {
binding.webView.goBack()
}
}
interface OnTabLayoutNameChanged {
fun nameChanged(position: Int, name: String)
}
여기서 볼 것은 onCreateView가 onViewCreated보다 먼저 수행되는 함수로, 여기서 우리가 잘 알고 있는 바인딩 작업을 하는 것입니다.
WebViewFragment라는 클래스가 생성될 때 여기서 구현될 inflater를 xml이 되는 FragmentWebViewBinding에 인플레이팅
하는 과정을 거치고 그 바인딩의 부모 뷰를 리턴해주면 됩니다. (결국 Fragment도 액티비티 위에서 붙여지는 것이라 binding.root 반환)
그리고 onViewCreated() 에서 binding 한 위젯을 가져오면 됩니다.
WebView를 이용한다고 했으니 WebViewClient를 만들어야 하는데, 이를 상속해 만든 클래스에 연결해 호출합니다.
WebViewClient의 상태중 onPageFinished, onPageStarted 라는 구현해야 할 함수가 있는데
여기서 ProgressBar를 보이게 하거나 보이지 않게 하도록 구현해두면 됩니다.
따라서 인자로 보내야하고, shouldOverrideUrlLoading 구현 함수에 대해서는 요청 상태에 따라 웹뷰에서 계속 로드를
시도할 지 말 지를 Boolean 값으로 구현해주면 됩니다.
다음 내용에서 SharedPreference를 사용한 것은 현재 상태에 대해서 기억해두고 웹뷰를 로드하기 위해서 쓴 것이므로
이번 포스팅에서 집중하고자 하는 WebView와 ViewPager2, Fragment에 좀 더 포커스를 두겠습니다.
[WebtoonWebViewClient.kt]
package com.example.part2.chapter1
import android.graphics.Bitmap
import android.util.Log
import android.view.View.GONE
import android.view.View.VISIBLE
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ProgressBar
// Unit -> 무엇이 들어와도 됨 (함수) : 리턴형 Void (Unit)
class WebtoonWebViewClient(
private val progressBar: ProgressBar,
private val saveData: (String) -> Unit
): WebViewClient() {
// 이 때 progressBar 끄면 될 듯?
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
// progressBar를 가져오거나 callback으로 처리하거나
progressBar.visibility = GONE
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
progressBar.visibility = VISIBLE
}
// shouldOverrideUrlLoading
// request를 보고 이걸 로드할지 말지 결정하는 함수
// true => page load X
// false => page load 계속 시도
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
Log.d("WebtoonWebViewClient", request?.url.toString())
saveData.invoke(request?.url.toString()) // request의 Url 보내서 저장시킬것
return !(request != null && request.url.toString().contains("google"))
}
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError?
) {
super.onReceivedError(view, request, error)
}
}
[ViewPagerAdapter.kt]
package com.example.part2.chapter1.adapters
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.example.part2.chapter1.MainActivity
import com.example.part2.chapter1.OnTabLayoutNameChanged
import com.example.part2.chapter1.WebViewFragment
// 인자 : FragmentManager, LifeCycle
/*
FragmentActivity를 보낼수도 있고 Fragment 안에 Fragment를 만들어서 보낼 수도 있음
*/
class ViewPagerAdapter(private val mActivity: MainActivity) :
FragmentStateAdapter(mActivity) {
override fun getItemCount(): Int {
return 3
}
// 이 때 리스너를 달아줄 것
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> {
return WebViewFragment(position, "https://www.google.com").apply {
listener = mActivity
}
}
1 -> {
return WebViewFragment(position, "https://search.naver.com/search.naver?sm=tab_hty.top&where=nexearch&ssc=tab.nx.all&query=%EA%B7%B8%EB%A6%B0%EB%A6%AC%EC%86%8C%EC%8A%A4&oquery=%EA%B7%B8%EB%A6%B0%EB%A6%AC%EC%86%8C%EC%8A%A4&tqi=irw3Awqo1Sossf%2F09f0ssssssoZ-362163").apply {
listener = mActivity
}
}
else -> {
return WebViewFragment(position, "https://haams704.tistory.com/").apply {
listener = mActivity
}
}
}
}
}
이처럼 어댑터에서는 FragmentStateAdapter를 상속하는 어댑터를 만들고 포지션에 따라 Fragment를 만들어주는데,
앞서 설명한대로 포지션 값에 따라 리스너를 달리 두어 처리하도록 하고자 다음과 같이 구현했습니다.
이에 더해서 FragmentStateAdapter에 대해서 조금 더 알아보자면, 인자로 3가지를 받을 수 있습니다.
1) 액티비티를 받아서 처리하는 것
2) Fragment를 받아서 처리하는 것
3) FragmentManager와 LifeCycle을 받아서 처리하는 것
제가 사용한 방법은 1번 방식으로 Adapter가 구현될 곳이 액티비티(MainActivity)이기에 다음과 같이 했습니다.
만약, Fragment안에서 Fragment의 어댑터를 꽂아 구현할 경우가 생긴다하면 저 인자는 Fragment가 될 것입니다.
또한 OnTabLayoutNameChanged 라는 인터페이스는 MainActivity에서 구현될 것이기 때문에
WebViewFragment를 리턴할 때 listener = mActivity 이렇게 연결해 둔 것입니다.
즉, WebViewFragment에 인자로 OnTabLayoutNameChanged라는 인터페이스가 있을 것이고 이는 listener라는
변수명으로 선언되어 있을 것입니다.
탭 이름 변경에 대해서 사실 Fragment에서는 웹뷰만 띄우는 상황이지, TabLayout은 MainActivity에 있으므로
listener와 mActivity를 연결하여 OnTabLayoutNameChanged라는 인터페이스 구현은 실질적으로
MainActivity에서 진행될 것임을 고지해야한다.
따라서 MainActivity는 다음과 같아질 것입니다.
[MainActivity.kt]
package com.example.part2.chapter1
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.Gravity
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.part2.chapter1.adapters.ViewPagerAdapter
import com.example.part2.chapter1.databinding.ActivityMainBinding
import com.google.android.material.tabs.TabLayoutMediator
class MainActivity : AppCompatActivity(), OnTabLayoutNameChanged {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
initViews()
}
@SuppressLint("SetJavaScriptEnabled")
private fun initViews() {
val sharedPreference = getSharedPreferences(WebViewFragment.Companion.SHARED_PREFERENCE, Context.MODE_PRIVATE)
val tab0 = sharedPreference.getString("tab0_name","월요웹툰")
val tab1 = sharedPreference.getString("tab1_name","화요웹툰")
val tab2 = sharedPreference.getString("tab2_name","수요웹툰")
binding.viewPager.adapter = ViewPagerAdapter(this)
TabLayoutMediator(binding.tabLayout, binding.viewPager){ tab, position ->
run {
// val textView = TextView(this@MainActivity)
// textView.text = "position $position"
// textView.gravity = Gravity.CENTER
// tab.customView = textView // customView -> 만든거 넣을 수 있음
tab.text = when(position){
0 -> tab0
1 -> tab1
else -> tab2
}
}
}.attach()
}
@Deprecated(
"Deprecated in Java",
ReplaceWith("super.onBackPressed()", "androidx.appcompat.app.AppCompatActivity")
)
override fun onBackPressed() {
// supportFragmentManager에 fragment들을 리스트로 가지고 있음
// viewPager의 순서를 fragments의 요소로 넣어서 supportFragmentManager로부터 찾음
val currentFragment = supportFragmentManager.fragments[binding.viewPager.currentItem]// fragment를 ViewPager에서 가져와야함
if (currentFragment is WebViewFragment) {
if (currentFragment.canGoBack()) {
currentFragment.goBack()
} else {
super.onBackPressed()
}
} else {
super.onBackPressed()
}
}
override fun nameChanged(position: Int, name: String) {
val tab = binding.tabLayout.getTabAt(position) // 현재 탭의 위치를 통해 탭을 가져옴
tab?.text = name
}
}
뒤로가기 버튼을 선택했을때에도 현재 FragmentManager인 SupportFragmentManager에서
Fragments를 접근하는데, 현 viewPager2의 선택된 현 위치로 가져오는 것이 현재의 Fragment가 될 것입니다.
즉, "Fragment0 = ViewPager2에서 첫 번째 페이지 / Fragment1 = ViewPager2에서 두 번째 페이지" 이런 개념입니다.
전체 코드는 아래 링크에서 확인 가능합니다. 다음 포스팅에서 뵙겠습니다. 감사합니다!
https://github.com/Haamseongho/Kotlin_Jetpack/tree/main/part2Chapter1
'Android' 카테고리의 다른 글
Network / Service (feat. Retrofit) (11) | 2024.08.29 |
---|---|
Network - OkHttpClient / Socket 통신 (0) | 2024.08.25 |
ViewPager2와 TabLayout (0) | 2024.08.14 |
Permission 처리와 ListAdapter 활용법 (0) | 2024.08.01 |
RoomDB 활용 방법과 Coroutine 사용 (0) | 2024.07.29 |