finished conversation info app bar animation

This commit is contained in:
Mariotaku Lee 2017-02-21 15:42:07 +08:00
parent a8c5fca745
commit 81bf409cca
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
15 changed files with 700 additions and 257 deletions

View File

@ -69,6 +69,7 @@
-keep class * extends org.mariotaku.twidere.util.TwitterCardFragmentFactory
-keep class * extends org.mariotaku.twidere.util.Analyzer
-keep class * extends org.mariotaku.twidere.util.premium.ExtraFeaturesService
-keep class * extends org.mariotaku.twidere.util.view.AppBarChildBehavior.ChildTransformation
-keepclassmembers class * {
private <fields>;

View File

@ -27,7 +27,7 @@ import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.view.holder.SimpleUserViewHolder
class SimpleParcelableUsersAdapter @JvmOverloads constructor(
class SimpleParcelableUsersAdapter(
context: Context,
layoutRes: Int = R.layout.list_item_simple_user
) : BaseArrayAdapter<ParcelableUser>(context, layoutRes) {
@ -54,7 +54,7 @@ class SimpleParcelableUsersAdapter @JvmOverloads constructor(
return view
}
@JvmOverloads fun setData(data: List<ParcelableUser>?, clearOld: Boolean = false) {
fun setData(data: List<ParcelableUser>?, clearOld: Boolean = false) {
if (clearOld) {
clear()
}

View File

@ -21,9 +21,58 @@ package org.mariotaku.twidere.extension
import android.graphics.Point
import android.graphics.Rect
import android.support.v4.view.ViewCompat
/**
* Created by mariotaku on 2017/2/20.
*/
val Rect.origin: Point get() = Point(left, top)
fun Rect.offsetTopTo(y: Int) {
offsetTo(left, y)
}
fun Rect.offsetBottomTo(y: Int) {
offsetTo(left, y - height())
}
fun Rect.offsetLeftTo(x: Int) {
offsetTo(x, top)
}
fun Rect.offsetRightTo(x: Int) {
offsetTo(x - width(), top)
}
fun Rect.offsetStartTo(x: Int, layoutDirection: Int) {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
offsetRightTo(x)
} else {
offsetLeftTo(x)
}
}
fun Rect.offsetEndTo(x: Int, layoutDirection: Int) {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
offsetLeftTo(x)
} else {
offsetRightTo(x)
}
}
fun Rect.getStart(layoutDirection: Int): Int {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
return this.right
} else {
return this.left
}
}
fun Rect.getEnd(layoutDirection: Int): Int {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
return this.left
} else {
return this.right
}
}

View File

@ -21,31 +21,33 @@ package org.mariotaku.twidere.extension
import android.graphics.Rect
import android.graphics.RectF
import android.support.annotation.MainThread
import android.support.annotation.UiThread
import android.view.View
private val tempLocation = IntArray(2)
private val tempRect = Rect()
@MainThread
@UiThread
fun View.getBounds(rect: RectF) {
rect.set(x, y, x + width, y + height)
}
@MainThread
@UiThread
fun View.getFrame(rect: Rect) {
rect.set(left, top, right, bottom)
}
@MainThread
fun View.getFrameRelatedTo(rect: Rect, view: View? = null) {
if (view != null) {
view.getFrame(tempRect)
offsetToRoot(view, tempRect)
}
@UiThread
fun View.getFrameRelatedTo(rect: Rect, other: View? = null) {
this.getFrame(rect)
offsetToRoot(this, rect)
if (view != null) {
if (other == null) {
offsetToRoot(this, rect)
} else if (other === this) {
rect.offsetTo(0, 0)
} else if (other !== parent) {
offsetToRoot(this, rect)
other.getFrame(tempRect)
offsetToRoot(other, tempRect)
rect.offset(-tempRect.left, -tempRect.top)
}
}

View File

@ -26,6 +26,9 @@ import android.support.v4.app.LoaderManager
import android.support.v4.content.AsyncTaskLoader
import android.support.v4.content.Loader
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.FixedLinearLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.Toolbar
import android.view.LayoutInflater
import android.view.View
@ -39,6 +42,8 @@ import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.useCursor
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.BaseRecyclerViewAdapter
import org.mariotaku.twidere.adapter.iface.IItemCountsAdapter
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_ACCOUNT_KEY
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_CONVERSATION_ID
import org.mariotaku.twidere.constant.nameFirstKey
@ -47,10 +52,12 @@ import org.mariotaku.twidere.extension.model.displayAvatarTo
import org.mariotaku.twidere.extension.model.getConversationName
import org.mariotaku.twidere.fragment.BaseFragment
import org.mariotaku.twidere.fragment.iface.IToolBarSupportFragment
import org.mariotaku.twidere.model.ItemCounts
import org.mariotaku.twidere.model.ParcelableMessageConversation
import org.mariotaku.twidere.model.ParcelableMessageConversationCursorIndices
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore.Messages.Conversations
import org.mariotaku.twidere.view.holder.SimpleUserViewHolder
/**
* Created by mariotaku on 2017/2/15.
@ -62,6 +69,8 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
private val accountKey: UserKey get() = arguments.getParcelable(EXTRA_ACCOUNT_KEY)
private val conversationId: String get() = arguments.getString(EXTRA_CONVERSATION_ID)
private lateinit var adapter: ConversationInfoAdapter
override val controlBarHeight: Int get() = toolbar.measuredHeight
override var controlBarOffset: Float = 0f
@ -76,20 +85,29 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
activity.supportActionBar?.setDisplayShowTitleEnabled(false)
}
adapter = ConversationInfoAdapter(context)
recyclerView.adapter = adapter
recyclerView.layoutManager = FixedLinearLayoutManager(context, LinearLayoutManager.VERTICAL,
false)
val theme = Chameleon.getOverrideTheme(context, activity)
val profileImageStyle = preferences[profileImageStyleKey]
appBarConversationAvatar.style = profileImageStyle
appBarIcon.style = profileImageStyle
conversationAvatar.style = profileImageStyle
val avatarBackground = ChameleonUtils.getColorDependent(theme.colorToolbar)
appBarConversationAvatar.setBackgroundColor(avatarBackground)
appBarConversationName.setTextColor(ChameleonUtils.getColorDependent(theme.colorToolbar))
appBarIcon.setBackgroundColor(avatarBackground)
appBarTitle.setTextColor(ChameleonUtils.getColorDependent(theme.colorToolbar))
appBarSubtitle.setTextColor(ChameleonUtils.getColorDependent(theme.colorToolbar))
conversationAvatar.setBackgroundColor(avatarBackground)
conversationName.setTextColor(ChameleonUtils.getColorDependent(theme.colorToolbar))
conversationSummary.setTextColor(ChameleonUtils.getColorDependent(theme.colorToolbar))
loaderManager.initLoader(0, null, this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -112,11 +130,17 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
activity?.finish()
return
}
adapter.conversation = data
val name = data.getConversationName(context, userColorNameManager, preferences[nameFirstKey]).first
val summary = resources.getQuantityString(R.plurals.N_users, data.participants.size, data.participants.size)
data.displayAvatarTo(mediaLoader, conversationAvatar)
data.displayAvatarTo(mediaLoader, appBarConversationAvatar)
appBarConversationName.text = name
data.displayAvatarTo(mediaLoader, appBarIcon)
appBarTitle.text = name
appBarSubtitle.text = summary
conversationName.text = name
conversationSummary.text = summary
}
class ConversationInfoLoader(
@ -140,4 +164,67 @@ class MessageConversationInfoFragment : BaseFragment(), IToolBarSupportFragment,
forceLoad()
}
}
class ConversationInfoAdapter(context: Context) : BaseRecyclerViewAdapter<RecyclerView.ViewHolder>(context),
IItemCountsAdapter {
private val inflater = LayoutInflater.from(context)
override val itemCounts: ItemCounts = ItemCounts(2)
var conversation: ParcelableMessageConversation? = null
set(value) {
field = value
notifyDataSetChanged()
}
override fun getItemCount(): Int {
val conversation = this.conversation ?: return 0
itemCounts[ITEM_INDEX_ITEM] = conversation.participants.size
return itemCounts.itemCount
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
VIEW_TYPE_USER -> {
val user = this.conversation!!.participants[position - itemCounts.getItemStartPosition(position)]
(holder as SimpleUserViewHolder).displayUser(user)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
VIEW_TYPE_HEADER -> {
val view = inflater.inflate(HeaderViewHolder.layoutResource, parent, false)
return HeaderViewHolder(view)
}
VIEW_TYPE_USER -> {
val view = inflater.inflate(SimpleUserViewHolder.layoutResource, parent, false)
return SimpleUserViewHolder(view, this)
}
}
throw UnsupportedOperationException()
}
override fun getItemViewType(position: Int): Int {
when (itemCounts.getItemCountIndex(position)) {
ITEM_INDEX_HEADER -> return VIEW_TYPE_HEADER
ITEM_INDEX_ITEM -> return VIEW_TYPE_USER
}
throw UnsupportedOperationException()
}
companion object {
private const val ITEM_INDEX_HEADER = 0
private const val ITEM_INDEX_ITEM = 1
private const val VIEW_TYPE_HEADER = 1
private const val VIEW_TYPE_USER = 2
}
}
internal class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
companion object {
const val layoutResource = R.layout.header_message_conversation_info
}
}
}

View File

@ -0,0 +1,350 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.view
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Rect
import android.support.annotation.StyleableRes
import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.v4.view.ViewCompat
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.widget.TextView
import org.mariotaku.microblog.library.annotation.NoObfuscate
import org.mariotaku.twidere.R
import org.mariotaku.twidere.extension.*
import org.mariotaku.twidere.util.DebugLog
/**
* Created by mariotaku on 2017/2/20.
*/
class AppBarChildBehavior(
context: Context,
attrs: AttributeSet? = null
) : CoordinatorLayout.Behavior<View>(context, attrs) {
private val appBarId: Int
private val toolbarId: Int
private val dependencyViewId: Int
private val targetViewId: Int
private val alignmentRule: Int
private val marginTop: Int
private val marginBottom: Int
private val marginLeft: Int
private val marginRight: Int
private val marginStart: Int
private val marginEnd: Int
private val transformation: ChildTransformation
private val dependencyRect = Rect()
private val thisRect = Rect()
private val thisInitRect = Rect()
private val targetRect = Rect()
private val tempRect = Rect()
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.AppBarChildBehavior)
appBarId = a.getResourceIdOrThrow(R.styleable.AppBarChildBehavior_behavior_appBarId, "appBarId")
toolbarId = a.getResourceIdOrThrow(R.styleable.AppBarChildBehavior_behavior_toolbarId, "toolbarId")
dependencyViewId = a.getResourceIdOrThrow(R.styleable.AppBarChildBehavior_behavior_dependencyViewId, "dependencyViewId")
targetViewId = a.getResourceIdOrThrow(R.styleable.AppBarChildBehavior_behavior_targetViewId, "targetViewId")
alignmentRule = a.getIntegerOrThrow(R.styleable.AppBarChildBehavior_behavior_alignmentRule, "alignmentRule")
marginTop = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginTop, 0)
marginBottom = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginBottom, 0)
marginLeft = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginLeft, 0)
marginRight = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginRight, 0)
marginStart = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginStart, 0)
marginEnd = a.getDimensionPixelSize(R.styleable.AppBarChildBehavior_behavior_marginEnd, 0)
transformation = a.getTransformation(R.styleable.AppBarChildBehavior_behavior_childTransformation)
a.recycle()
}
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
return dependency.id == dependencyViewId
}
override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean {
val target = parent.findViewById(targetViewId)
val dependency = parent.getDependencies(child).first()
dependency.getFrameRelatedTo(dependencyRect, parent)
child.layoutRelatedTo(dependencyRect, layoutDirection)
child.getFrameRelatedTo(thisRect, parent)
target.getFrameRelatedTo(targetRect, parent)
transformation.onChildLayoutChanged(child, dependency, target)
return true
}
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
val appBar = parent.findViewById(appBarId)
val target = parent.findViewById(targetViewId)
val toolbar = parent.findViewById(toolbarId)
val behavior = (appBar.layoutParams as CoordinatorLayout.LayoutParams).behavior as AppBarLayout.Behavior
toolbar.getLocationOnScreen(tempRect)
val offset = behavior.topAndBottomOffset
val percent = offset / (tempRect.bottom - appBar.height).toFloat()
transformation.onTargetChanged(child, thisRect, target, targetRect, percent, offset)
return true
}
internal fun View.layoutRelatedTo(frame: Rect, layoutDirection: Int) {
val verticalRule = alignmentRule and VERTICAL_MASK
val horizontalRule = alignmentRule and HORIZONTAL_MASK
tempRect.set(0, 0, measuredWidth, measuredHeight)
when (verticalRule) {
ALIGNMENT_CENTER_VERTICAL -> {
tempRect.offsetTopTo(frame.centerY() - measuredHeight / 2 + marginTop - marginBottom)
}
0, ALIGNMENT_TOP -> {
tempRect.offsetTopTo(frame.top + marginTop)
}
ALIGNMENT_BOTTOM -> {
tempRect.offsetBottomTo(frame.bottom - marginBottom)
}
ALIGNMENT_ABOVE -> {
tempRect.offsetBottomTo(frame.top + marginTop - marginBottom)
}
ALIGNMENT_BELOW -> {
tempRect.offsetTopTo(frame.bottom + marginTop - marginBottom)
}
ALIGNMENT_ABOVE_CENTER -> {
tempRect.offsetBottomTo(frame.centerY() + marginTop - marginBottom)
}
ALIGNMENT_BELOW_CENTER -> {
tempRect.offsetTopTo(frame.centerY() + marginTop - marginBottom)
}
else -> {
throw IllegalArgumentException("Illegal alignment flag ${Integer.toHexString(alignmentRule)}")
}
}
when (horizontalRule) {
ALIGNMENT_CENTER_HORIZONTAL -> {
tempRect.offsetLeftTo(frame.centerX() - measuredWidth / 2
+ absoluteMarginLeft(layoutDirection) - absoluteMarginRight(layoutDirection))
}
0, ALIGNMENT_LEFT -> {
tempRect.offsetLeftTo(frame.left + absoluteMarginLeft(layoutDirection))
}
ALIGNMENT_RIGHT -> {
tempRect.offsetRightTo(frame.right - absoluteMarginRight(layoutDirection))
}
ALIGNMENT_TO_LEFT_OF -> {
tempRect.offsetRightTo(frame.left + absoluteMarginLeft(layoutDirection)
- absoluteMarginRight(layoutDirection))
}
ALIGNMENT_TO_RIGHT_OF -> {
tempRect.offsetLeftTo(frame.right + absoluteMarginLeft(layoutDirection)
- absoluteMarginRight(layoutDirection))
}
ALIGNMENT_TO_LEFT_OF_CENTER -> {
tempRect.offsetRightTo(frame.centerX() + absoluteMarginLeft(layoutDirection)
- absoluteMarginRight(layoutDirection))
}
ALIGNMENT_TO_RIGHT_OF_CENTER -> {
tempRect.offsetLeftTo(frame.centerX() + absoluteMarginLeft(layoutDirection)
- absoluteMarginRight(layoutDirection))
}
ALIGNMENT_START -> {
tempRect.offsetStartTo(frame.getStart(layoutDirection)
+ relativeMarginStart(layoutDirection), layoutDirection)
}
ALIGNMENT_END -> {
tempRect.offsetEndTo(frame.getEnd(layoutDirection)
- relativeMarginEnd(layoutDirection), layoutDirection)
}
ALIGNMENT_TO_START_OF -> {
tempRect.offsetEndTo(frame.getStart(layoutDirection)
+ relativeMarginStart(layoutDirection) - relativeMarginEnd(layoutDirection),
layoutDirection)
}
ALIGNMENT_TO_END_OF -> {
tempRect.offsetStartTo(frame.getEnd(layoutDirection)
+ relativeMarginStart(layoutDirection) - relativeMarginEnd(layoutDirection),
layoutDirection)
}
ALIGNMENT_TO_START_OF_CENTER -> {
tempRect.offsetEndTo(frame.centerX() + relativeMarginStart(layoutDirection)
- relativeMarginEnd(layoutDirection), layoutDirection)
}
ALIGNMENT_TO_END_OF_CENTER -> {
tempRect.offsetStartTo(frame.centerX() + relativeMarginStart(layoutDirection)
- relativeMarginEnd(layoutDirection), layoutDirection)
}
else -> {
throw IllegalArgumentException("Illegal alignment flag ${Integer.toHexString(alignmentRule)}")
}
}
this.layout(tempRect.left, tempRect.top, tempRect.right, tempRect.bottom)
}
private fun absoluteMarginLeft(layoutDirection: Int): Int {
if (marginStart == 0 && marginEnd == 0) return marginLeft
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
return marginEnd
} else {
return marginStart
}
}
private fun absoluteMarginRight(layoutDirection: Int): Int {
if (marginStart == 0 && marginEnd == 0) return marginRight
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
return marginStart
} else {
return marginEnd
}
}
private fun relativeMarginStart(layoutDirection: Int): Int {
if (marginStart != 0) {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
return -marginStart
} else {
return marginStart
}
} else {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
return marginRight
} else {
return marginLeft
}
}
}
private fun relativeMarginEnd(layoutDirection: Int): Int {
if (marginEnd != 0) {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
return -marginEnd
} else {
return marginEnd
}
} else {
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
return marginLeft
} else {
return marginRight
}
}
}
interface ChildTransformation {
fun onChildLayoutChanged(child: View, dependency: View, target: View) {}
fun onTargetChanged(child: View, frame: Rect, target: View, targetFrame: Rect, percent: Float, offset: Int)
}
open class ScaleTransformation : ChildTransformation {
override fun onTargetChanged(child: View, frame: Rect, target: View, targetFrame: Rect, percent: Float, offset: Int) {
child.pivotX = child.width.toFloat()
child.pivotY = child.height.toFloat()
child.scaleX = 1 - (frame.width() - targetFrame.width()) * percent / frame.width()
child.scaleY = 1 - (frame.height() - targetFrame.height()) * percent / frame.height()
child.translationX = (targetFrame.right - frame.right) * percent
child.translationY = -offset - (frame.bottom - offset - targetFrame.bottom) * percent
DebugLog.d(msg = "bot:${frame.bottom}")
}
}
@NoObfuscate
open class TextViewTransformation : ChildTransformation {
private var sourceSize: Float = Float.NaN
private var destSize: Float = Float.NaN
private var viewLaidOut: Boolean = false
override fun onChildLayoutChanged(child: View, dependency: View, target: View) {
if (viewLaidOut) return
child as TextView
target as TextView
sourceSize = child.textSize
destSize = target.textSize
viewLaidOut = true
}
override fun onTargetChanged(child: View, frame: Rect, target: View, targetFrame: Rect, percent: Float, offset: Int) {
child as TextView
child.pivotX = child.width.toFloat()
child.pivotY = child.height.toFloat()
child.translationX = (targetFrame.left - frame.left) * percent
child.translationY = -offset - (frame.bottom - offset - targetFrame.bottom) * percent
child.setTextSize(TypedValue.COMPLEX_UNIT_PX, sourceSize + (destSize - sourceSize) * percent)
}
}
companion object {
private const val HORIZONTAL_MASK: Int = 0x0000FFFF
private const val VERTICAL_MASK: Int = 0xFFFF0000.toInt()
private const val HORIZONTAL_RELATIVE_FLAG: Int = 0x00001000
const val ALIGNMENT_LEFT: Int = 0x00000001
const val ALIGNMENT_RIGHT: Int = 0x00000002
const val ALIGNMENT_TO_LEFT_OF: Int = 0x00000004
const val ALIGNMENT_TO_RIGHT_OF: Int = 0x00000008
const val ALIGNMENT_TO_LEFT_OF_CENTER: Int = 0x00000010
const val ALIGNMENT_TO_RIGHT_OF_CENTER: Int = 0x00000020
const val ALIGNMENT_CENTER_HORIZONTAL: Int = ALIGNMENT_LEFT or ALIGNMENT_RIGHT
const val ALIGNMENT_START: Int = ALIGNMENT_LEFT or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_END: Int = ALIGNMENT_RIGHT or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TO_START_OF: Int = ALIGNMENT_TO_LEFT_OF or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TO_END_OF: Int = ALIGNMENT_TO_RIGHT_OF or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TO_START_OF_CENTER: Int = ALIGNMENT_TO_LEFT_OF_CENTER or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TO_END_OF_CENTER: Int = ALIGNMENT_TO_RIGHT_OF_CENTER or HORIZONTAL_RELATIVE_FLAG
const val ALIGNMENT_TOP: Int = 0x00010000
const val ALIGNMENT_BOTTOM: Int = 0x00020000
const val ALIGNMENT_ABOVE: Int = 0x00040000
const val ALIGNMENT_BELOW: Int = 0x00080000
const val ALIGNMENT_ABOVE_CENTER: Int = 0x00100000
const val ALIGNMENT_BELOW_CENTER: Int = 0x00200000
const val ALIGNMENT_CENTER_VERTICAL: Int = ALIGNMENT_TOP or ALIGNMENT_BOTTOM
private fun TypedArray.getIntegerOrThrow(@StyleableRes index: Int, name: String): Int {
if (!hasValue(index)) {
throw IllegalArgumentException("$name required")
}
return getInteger(index, 0)
}
private fun TypedArray.getResourceIdOrThrow(@StyleableRes index: Int, name: String): Int {
if (!hasValue(index)) {
throw IllegalArgumentException("$name required")
}
return getResourceId(index, 0)
}
private fun TypedArray.getTransformation(@StyleableRes index: Int): ChildTransformation {
val className = getString(index) ?: return ScaleTransformation()
return Class.forName(className).newInstance() as ChildTransformation
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.view
import android.graphics.Rect
import android.view.View
/**
* Created by mariotaku on 2017/2/21.
*/
class ConversationAvatarTransformation : AppBarChildBehavior.ScaleTransformation() {
override fun onTargetChanged(child: View, frame: Rect, target: View, targetFrame: Rect, percent: Float, offset: Int) {
super.onTargetChanged(child, frame, target, targetFrame, percent, offset)
if (percent < 1) {
target.visibility = View.INVISIBLE
} else {
target.visibility = View.VISIBLE
}
}
}

View File

@ -1,122 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.view
import android.content.Context
import android.graphics.Rect
import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.v4.view.ViewCompat
import android.util.AttributeSet
import android.view.View
import kotlinx.android.synthetic.main.fragment_messages_conversation_info.view.*
import kotlinx.android.synthetic.main.layout_toolbar_message_conversation_title.view.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.extension.getFrame
import org.mariotaku.twidere.extension.getFrameRelatedTo
import org.mariotaku.twidere.extension.getLocationOnScreen
import org.mariotaku.twidere.extension.origin
/**
* Created by mariotaku on 2017/2/20.
*/
class ConversationInfoAvatarBehavior(
context: Context,
attrs: AttributeSet? = null
) : CoordinatorLayout.Behavior<View>(context, attrs) {
private val marginBottom: Float
private val marginStart: Float
private val appBarRect = Rect()
private val sourceRect = Rect()
private val destRect = Rect()
private val tempRect = Rect()
private var toolbarLayoutHeight = 0f
var dependentPercent: Float = 0f
private set
init {
val a = context.obtainStyledAttributes(attrs, R.styleable.ConversationInfoAvatarBehavior)
marginBottom = a.getDimension(R.styleable.ConversationInfoAvatarBehavior_behavior_avatarMarginBottom, 0f)
marginStart = a.getDimension(R.styleable.ConversationInfoAvatarBehavior_behavior_avatarMarginStart, 0f)
a.recycle()
}
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
return dependency is AppBarLayout
}
override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean {
val appBar = parent.getDependencies(child).first()
// Get AppBar frame (and reset origin to 0, 0)
appBar.getFrame(appBarRect)
val dependencyOrigin = appBarRect.origin
appBarRect.offsetTo(0, 0)
// Layout avatar
val left: Int
val right: Int
if (layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL) {
right = appBarRect.right - Math.round(marginStart)
left = right - child.measuredWidth
} else {
left = Math.round(marginStart)
right = left + child.measuredWidth
}
val bottom = Math.round(appBarRect.bottom - marginBottom)
val top = bottom - child.measuredHeight
child.layout(left, top, right, bottom)
val toolbarLayout = appBar.toolbarLayout
toolbarLayoutHeight = toolbarLayout.height.toFloat()
child.getFrameRelatedTo(sourceRect)
appBar.conversationAvatar.getFrameRelatedTo(destRect)
return true
}
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
val behavior = (dependency.layoutParams as CoordinatorLayout.LayoutParams).behavior as AppBarLayout.Behavior
val toolbar = dependency.toolbar
toolbar.getLocationOnScreen(tempRect)
val percent = behavior.topAndBottomOffset / (tempRect.bottom - toolbarLayoutHeight)
child.pivotX = child.width.toFloat()
child.pivotY = child.height.toFloat()
child.scaleX = 1 - (sourceRect.width() - destRect.width()) * percent / sourceRect.width()
child.scaleY = 1 - (sourceRect.height() - destRect.height()) * percent / sourceRect.height()
child.translationX = (destRect.right - sourceRect.right) * percent
child.translationY = (destRect.bottom - sourceRect.bottom) * percent
if (percent >= 1) {
child.visibility = View.INVISIBLE
dependency.conversationAvatar.visibility = View.VISIBLE
} else {
child.visibility = View.VISIBLE
dependency.conversationAvatar.visibility = View.INVISIBLE
}
dependentPercent = percent
return true
}
}

View File

@ -0,0 +1,38 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util.view
import android.graphics.Rect
import android.view.View
/**
* Created by mariotaku on 2017/2/21.
*/
class ConversationTitlesTransformation : AppBarChildBehavior.TextViewTransformation() {
override fun onTargetChanged(child: View, frame: Rect, target: View, targetFrame: Rect, percent: Float, offset: Int) {
super.onTargetChanged(child, frame, target, targetFrame, percent, offset)
if (percent < 1) {
target.visibility = View.INVISIBLE
} else {
target.visibility = View.VISIBLE
}
}
}

View File

@ -5,6 +5,7 @@ import android.view.View
import android.widget.CheckBox
import android.widget.TextView
import kotlinx.android.synthetic.main.list_item_simple_user.view.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.adapter.iface.IContentAdapter
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.view.ProfileImageView
@ -35,4 +36,8 @@ open class SimpleUserViewHolder(itemView: View, val adapter: IContentAdapter) :
profileImageView.visibility = View.GONE
}
}
companion object {
const val layoutResource = R.layout.list_item_simple_user
}
}

View File

@ -40,20 +40,14 @@
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.widget.NestedScrollView
<org.mariotaku.twidere.view.ExtendedRecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:text="@string/large_text"/>
</android.support.v4.widget.NestedScrollView>
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:id="@+id/editButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/element_spacing_large"
@ -64,19 +58,25 @@
tools:tint="?android:colorForeground"/>
<org.mariotaku.twidere.view.ProfileImageView
android:id="@+id/appBarConversationAvatar"
android:id="@+id/appBarIcon"
android:layout_width="@dimen/icon_size_conversation_info"
android:layout_height="@dimen/icon_size_conversation_info"
app:behavior_avatarMarginBottom="@dimen/element_spacing_large"
app:behavior_avatarMarginStart="@dimen/element_spacing_large"
app:layout_behavior="org.mariotaku.twidere.util.view.ConversationInfoAvatarBehavior"
app:behavior_alignmentRule="bottom|start"
app:behavior_appBarId="@+id/appBar"
app:behavior_childTransformation="org.mariotaku.twidere.util.view.ConversationAvatarTransformation"
app:behavior_dependencyViewId="@+id/appBar"
app:behavior_marginBottom="@dimen/element_spacing_xlarge"
app:behavior_marginStart="@dimen/element_spacing_xlarge"
app:behavior_targetViewId="@+id/conversationAvatar"
app:behavior_toolbarId="@+id/toolbar"
app:layout_behavior="org.mariotaku.twidere.util.view.AppBarChildBehavior"
app:sivDrawShadow="false"
app:sivElevation="8dp"
app:sivShape="circle"
tools:src="@drawable/ic_profile_image_default_group"/>
<org.mariotaku.twidere.view.FixedTextView
android:id="@+id/appBarConversationName"
android:id="@+id/appBarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="8dp"
@ -85,15 +85,35 @@
android:textAppearance="?android:textAppearanceLarge"
android:textColor="?android:textColorPrimary"
android:textStyle="normal"
app:behavior_titleMarginStart="@dimen/element_spacing_normal"
app:layout_behavior="org.mariotaku.twidere.util.view.ConversationInfoTitleBehavior"
app:behavior_alignmentRule="toEndOf|aboveCenter"
app:behavior_appBarId="@+id/appBar"
app:behavior_childTransformation="org.mariotaku.twidere.util.view.ConversationTitlesTransformation"
app:behavior_dependencyViewId="@+id/appBarIcon"
app:behavior_marginStart="@dimen/element_spacing_normal"
app:behavior_targetViewId="@+id/conversationName"
app:behavior_toolbarId="@+id/toolbar"
app:layout_behavior="org.mariotaku.twidere.util.view.AppBarChildBehavior"
tools:text="Title"/>
<org.mariotaku.twidere.view.FixedTextView
android:id="@+id/appBarConversationSummary"
android:id="@+id/appBarSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="8dp"
android:ellipsize="end"
android:minLines="1"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="?android:textColorSecondary"
android:textStyle="normal"
app:behavior_alignmentRule="toEndOf|belowCenter"
app:behavior_appBarId="@+id/appBar"
app:behavior_childTransformation="org.mariotaku.twidere.util.view.ConversationTitlesTransformation"
app:behavior_dependencyViewId="@+id/appBarIcon"
app:behavior_marginStart="@dimen/element_spacing_normal"
app:behavior_marginTop="@dimen/element_spacing_small"
app:behavior_targetViewId="@+id/conversationSummary"
app:behavior_toolbarId="@+id/toolbar"
app:layout_behavior="org.mariotaku.twidere.util.view.AppBarChildBehavior"
tools:text="Subtitle"/>
</android.support.design.widget.CoordinatorLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Twidere - Twitter client for Android
~
~ Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.SwitchCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/element_size_normal"
android:padding="@dimen/element_spacing_normal"
android:text="@string/action_mute_notifications"/>
</LinearLayout>

View File

@ -145,14 +145,46 @@
<declare-styleable name="ProfileBannerImageView">
<attr name="bannerAspectRatio" format="fraction"/>
</declare-styleable>
<declare-styleable name="ConversationInfoAvatarBehavior">
<attr name="behavior_avatarMarginBottom" format="dimension"/>
<attr name="behavior_avatarMarginStart" format="dimension"/>
</declare-styleable>
<declare-styleable name="ConversationInfoTitleBehavior">
<attr name="behavior_titleMarginBottom" format="dimension"/>
<attr name="behavior_titleMarginStart" format="dimension"/>
<declare-styleable name="AppBarChildBehavior">
<attr name="behavior_appBarId" format="reference"/>
<attr name="behavior_toolbarId" format="reference"/>
<attr name="behavior_dependencyViewId" format="reference"/>
<attr name="behavior_targetViewId" format="reference"/>
<attr name="behavior_marginLeft" format="dimension"/>
<attr name="behavior_marginRight" format="dimension"/>
<attr name="behavior_marginStart" format="dimension"/>
<attr name="behavior_marginEnd" format="dimension"/>
<attr name="behavior_marginTop" format="dimension"/>
<attr name="behavior_marginBottom" format="dimension"/>
<attr name="behavior_alignmentRule"/>
<attr name="behavior_childTransformation" format="string"/>
</declare-styleable>
<attr name="behavior_alignmentRule">
<flag name="left" value="0x00000001"/>
<flag name="right" value="0x00000002"/>
<flag name="toLeftOf" value="0x00000004"/>
<flag name="toRightOf" value="0x00000008"/>
<flag name="toLeftOfCenter" value="0x00000010"/>
<flag name="toRightOfCenter" value="0x00000020"/>
<flag name="centerHorizontal" value="0x00000003"/>
<flag name="start" value="0x00001001"/>
<flag name="end" value="0x00001002"/>
<flag name="toStartOf" value="0x00001004"/>
<flag name="toEndOf" value="0x00001008"/>
<flag name="toStartOfCenter" value="0x00001010"/>
<flag name="toEndOfCenter" value="0x00001020"/>
<flag name="top" value="0x00010000"/>
<flag name="bottom" value="0x00020000"/>
<flag name="above" value="0x00040000"/>
<flag name="below" value="0x00080000"/>
<flag name="aboveCenter" value="0x00100000"/>
<flag name="belowCenter" value="0x00200000"/>
<flag name="centerVertical" value="0x00030000"/>
<flag name="center" value="0x00030003"/>
</attr>
<attr name="notificationType">
<flag name="none" value="0"/>
<flag name="ringtone" value="1"/>

View File

@ -111,5 +111,4 @@
<dimen name="toolbar_elevation">8dp</dimen>
<dimen name="unread_indicator_size">16dp</dimen>
<dimen name="text_margin">16dp</dimen>
</resources>

View File

@ -65,6 +65,7 @@
<string name="action_liking">liking</string>
<string name="action_modifying_lists">modifying lists</string>
<string name="action_mute">Mute</string>
<string name="action_mute_notifications">Mute notifications</string>
<string name="action_muting">muting</string>
<string name="action_name_saved_at_time"><xliff:g id="action">%1$s</xliff:g>, saved at <xliff:g id="time">%2$s</xliff:g></string>
<string name="action_open_in_browser">Open in browser</string>
@ -1220,94 +1221,4 @@
<string name="users_statuses">User\'s tweets</string>
<string name="vibration">Vibration</string>
<string name="title_activity_scrolling">ScrollingActivity</string>
<string name="large_text">
"Material is the metaphor.\n\n"
"A material metaphor is the unifying theory of a rationalized space and a system of motion."
"The material is grounded in tactile reality, inspired by the study of paper and ink, yet "
"technologically advanced and open to imagination and magic.\n"
"Surfaces and edges of the material provide visual cues that are grounded in reality. The "
"use of familiar tactile attributes helps users quickly understand affordances. Yet the "
"flexibility of the material creates new affordances that supercede those in the physical "
"world, without breaking the rules of physics.\n"
"The fundamentals of light, surface, and movement are key to conveying how objects move, "
"interact, and exist in space and in relation to each other. Realistic lighting shows "
"seams, divides space, and indicates moving parts.\n\n"
"Bold, graphic, intentional.\n\n"
"The foundational elements of print based design typography, grids, space, scale, color, "
"and use of imagery guide visual treatments. These elements do far more than please the "
"eye. They create hierarchy, meaning, and focus. Deliberate color choices, edge to edge "
"imagery, large scale typography, and intentional white space create a bold and graphic "
"interface that immerse the user in the experience.\n"
"An emphasis on user actions makes core functionality immediately apparent and provides "
"waypoints for the user.\n\n"
"Motion provides meaning.\n\n"
"Motion respects and reinforces the user as the prime mover. Primary user actions are "
"inflection points that initiate motion, transforming the whole design.\n"
"All action takes place in a single environment. Objects are presented to the user without "
"breaking the continuity of experience even as they transform and reorganize.\n"
"Motion is meaningful and appropriate, serving to focus attention and maintain continuity. "
"Feedback is subtle yet clear. Transitions are efficient yet coherent.\n\n"
"3D world.\n\n"
"The material environment is a 3D space, which means all objects have x, y, and z "
"dimensions. The z-axis is perpendicularly aligned to the plane of the display, with the "
"positive z-axis extending towards the viewer. Every sheet of material occupies a single "
"position along the z-axis and has a standard 1dp thickness.\n"
"On the web, the z-axis is used for layering and not for perspective. The 3D world is "
"emulated by manipulating the y-axis.\n\n"
"Light and shadow.\n\n"
"Within the material environment, virtual lights illuminate the scene. Key lights create "
"directional shadows, while ambient light creates soft shadows from all angles.\n"
"Shadows in the material environment are cast by these two light sources. In Android "
"development, shadows occur when light sources are blocked by sheets of material at "
"various positions along the z-axis. On the web, shadows are depicted by manipulating the "
"y-axis only. The following example shows the card with a height of 6dp.\n\n"
"Resting elevation.\n\n"
"All material objects, regardless of size, have a resting elevation, or default elevation "
"that does not change. If an object changes elevation, it should return to its resting "
"elevation as soon as possible.\n\n"
"Component elevations.\n\n"
"The resting elevation for a component type is consistent across apps (e.g., FAB elevation "
"does not vary from 6dp in one app to 16dp in another app).\n"
"Components may have different resting elevations across platforms, depending on the depth "
"of the environment (e.g., TV has a greater depth than mobile or desktop).\n\n"
"Responsive elevation and dynamic elevation offsets.\n\n"
"Some component types have responsive elevation, meaning they change elevation in response "
"to user input (e.g., normal, focused, and pressed) or system events. These elevation "
"changes are consistently implemented using dynamic elevation offsets.\n"
"Dynamic elevation offsets are the goal elevation that a component moves towards, relative "
"to the components resting state. They ensure that elevation changes are consistent "
"across actions and component types. For example, all components that lift on press have "
"the same elevation change relative to their resting elevation.\n"
"Once the input event is completed or cancelled, the component will return to its resting "
"elevation.\n\n"
"Avoiding elevation interference.\n\n"
"Components with responsive elevations may encounter other components as they move between "
"their resting elevations and dynamic elevation offsets. Because material cannot pass "
"through other material, components avoid interfering with one another any number of ways, "
"whether on a per component basis or using the entire app layout.\n"
"On a component level, components can move or be removed before they cause interference. "
"For example, a floating action button (FAB) can disappear or move off screen before a "
"user picks up a card, or it can move if a snackbar appears.\n"
"On the layout level, design your app layout to minimize opportunities for interference. "
"For example, position the FAB to one side of stream of a cards so the FAB wont interfere "
"when a user tries to pick up one of cards.\n\n"
</string>
</resources>