[fenix] Fixes https://github.com/mozilla-mobile/fenix/issues/351 Create home screen component for multitasking
parent
8e2ddbefd0
commit
d6e79694f8
@ -0,0 +1,21 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.ext
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.TouchDelegate
|
||||
import android.view.View
|
||||
|
||||
fun View?.increaseTapArea(extraDps: Int) {
|
||||
this!!.post {
|
||||
val touchRect = Rect()
|
||||
getHitRect(touchRect)
|
||||
touchRect.top -= extraDps
|
||||
touchRect.left -= extraDps
|
||||
touchRect.right += extraDps
|
||||
touchRect.bottom += extraDps
|
||||
(parent as View).touchDelegate = TouchDelegate(touchRect, this)
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.tabs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.Observer
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.tab_list_row.*
|
||||
import mozilla.components.browser.session.Session
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.increaseTapArea
|
||||
|
||||
class TabsAdapter(private val actionEmitter: Observer<TabsAction>) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
var sessions = listOf<Session>()
|
||||
set(value) {
|
||||
val diffResult = DiffUtil.calculateDiff(TabsDiffCallback(field, value), true)
|
||||
field = value
|
||||
diffResult.dispatchUpdatesTo(this@TabsAdapter)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
return TabViewHolder(view, actionEmitter)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int) = TabViewHolder.LAYOUT_ID
|
||||
|
||||
override fun getItemCount(): Int = sessions.size
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is TabViewHolder -> {
|
||||
holder.bindSession(sessions[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TabViewHolder(
|
||||
view: View,
|
||||
actionEmitter: Observer<TabsAction>,
|
||||
override val containerView: View? = view
|
||||
) :
|
||||
RecyclerView.ViewHolder(view), LayoutContainer {
|
||||
|
||||
var session: Session? = null
|
||||
|
||||
init {
|
||||
item_tab.setOnClickListener {
|
||||
actionEmitter.onNext(TabsAction.Select(session!!))
|
||||
}
|
||||
|
||||
close_tab_button?.run {
|
||||
increaseTapArea(closeButtonIncreaseDps)
|
||||
setOnClickListener {
|
||||
actionEmitter.onNext(TabsAction.Close(session!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun bindSession(session: Session) {
|
||||
this.session = session
|
||||
text_url.text = session.url
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val closeButtonIncreaseDps = 12
|
||||
const val LAYOUT_ID = R.layout.tab_list_row
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TabsDiffCallback(
|
||||
private val oldList: List<Session>,
|
||||
private val newList: List<Session>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize(): Int = oldList.size
|
||||
override fun getNewListSize(): Int = newList.size
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldList[oldItemPosition].id == newList[newItemPosition].id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
return oldList[oldItemPosition] == newList[newItemPosition]
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
|
||||
val oldSession = oldList[oldItemPosition]
|
||||
val newSession = newList[newItemPosition]
|
||||
val diffBundle = Bundle()
|
||||
if (oldSession.url != newSession.url) {
|
||||
diffBundle.putString("url", newSession.url)
|
||||
}
|
||||
return if (diffBundle.size() == 0) null else diffBundle
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.tabs
|
||||
|
||||
import android.view.ViewGroup
|
||||
import mozilla.components.browser.session.Session
|
||||
import org.mozilla.fenix.mvi.Action
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
import org.mozilla.fenix.mvi.Change
|
||||
import org.mozilla.fenix.mvi.UIComponent
|
||||
import org.mozilla.fenix.mvi.ViewState
|
||||
|
||||
class TabsComponent(
|
||||
private val container: ViewGroup,
|
||||
bus: ActionBusFactory,
|
||||
override var initialState: TabsState = TabsState(listOf())
|
||||
) :
|
||||
UIComponent<TabsState, TabsAction, TabsChange>(
|
||||
bus.getManagedEmitter(TabsAction::class.java),
|
||||
bus.getSafeManagedObservable(TabsChange::class.java)
|
||||
) {
|
||||
|
||||
override val reducer: (TabsState, TabsChange) -> TabsState = { state, change ->
|
||||
when (change) {
|
||||
is TabsChange.Changed -> state.copy(sessions = change.sessions)
|
||||
}
|
||||
}
|
||||
|
||||
override fun initView() = TabsUIView(container, actionEmitter, changesObservable)
|
||||
|
||||
init {
|
||||
render(reducer)
|
||||
}
|
||||
}
|
||||
|
||||
data class TabsState(val sessions: List<Session>) : ViewState
|
||||
|
||||
sealed class TabsAction : Action {
|
||||
data class Select(val session: Session) : TabsAction()
|
||||
data class Close(val session: Session) : TabsAction()
|
||||
}
|
||||
|
||||
sealed class TabsChange : Change {
|
||||
data class Changed(val sessions: List<Session>) : TabsChange()
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.tabs
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.functions.Consumer
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.mvi.UIView
|
||||
|
||||
class TabsUIView(
|
||||
container: ViewGroup,
|
||||
actionEmitter: Observer<TabsAction>,
|
||||
changesObservable: Observable<TabsChange>
|
||||
) :
|
||||
UIView<TabsState, TabsAction, TabsChange>(container, actionEmitter, changesObservable) {
|
||||
|
||||
private val header: ConstraintLayout = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.tab_list_header, container, true)
|
||||
.findViewById(R.id.tabs_header)
|
||||
|
||||
override val view: RecyclerView = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_tabs, container, true)
|
||||
.findViewById(R.id.tabs_list)
|
||||
|
||||
private val tabsAdapter = TabsAdapter(actionEmitter)
|
||||
|
||||
init {
|
||||
view.apply {
|
||||
layoutManager = LinearLayoutManager(container.context)
|
||||
adapter = tabsAdapter
|
||||
itemAnimator = DefaultItemAnimator()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateView() = Consumer<TabsState> {
|
||||
tabsAdapter.sessions = it.sessions
|
||||
header.visibility = if (it.sessions.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
@ -1,12 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/session_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"/>
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"/>
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/tabs_list"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar_wrapper"/>
|
@ -0,0 +1,40 @@
|
||||
<?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:id="@+id/tabs_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
<TextView
|
||||
android:id="@+id/header_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tabs_header_title"
|
||||
android:textSize="24sp"
|
||||
android:textColor="@android:color/black"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
<ImageView
|
||||
android:id="@+id/add_tab_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/ic_add_black_24dp"
|
||||
android:baselineAlignBottom="true"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/header_text"
|
||||
app:layout_constraintEnd_toStartOf="@id/tabs_overflow_button"/>
|
||||
<ImageView
|
||||
android:id="@+id/tabs_overflow_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/ic_menu"
|
||||
android:baselineAlignBottom="true"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/header_text"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView
|
||||
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="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:padding="10dp"
|
||||
android:elevation="5dp"
|
||||
app:cardCornerRadius="10dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/item_tab"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="10dp">
|
||||
|
||||
<ImageView android:id="@+id/favicon_image"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_link"
|
||||
android:tint="@android:color/black"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:contentDescription="@string/favicon_content_description"/>
|
||||
|
||||
<TextView android:id="@+id/text_url"
|
||||
android:textSize="18sp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/favicon_image"
|
||||
app:layout_constraintEnd_toStartOf="@id/close_tab_button"/>
|
||||
|
||||
<ImageView android:id="@+id/close_tab_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_close_black_24dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:contentDescription="@string/close_tab"
|
||||
app:layout_constraintBottom_toBottomOf="@id/text_url"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
|
||||
<ImageView android:layout_width="0dp"
|
||||
android:layout_height="100dp"
|
||||
android:src="@color/photonBlue40"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_url"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:contentDescription="TODO"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
@ -1,5 +1,6 @@
|
||||
#Wed Feb 06 16:31:29 CST 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
|
||||
|
Loading…
Reference in New Issue