SystemUI 下拉状态栏的motionlayout
- motion 3 这里设置mUseLargeScreenShadeHeader
- NotificationPanelViewController
-
public void updateResources() { final boolean newSplitShadeEnabled = LargeScreenUtils.shouldUseSplitNotificationShade(mResources); final boolean splitShadeChanged = mSplitShadeEnabled != newSplitShadeEnabled; mSplitShadeEnabled = newSplitShadeEnabled; mQsController.updateResources(); mNotificationsQSContainerController.updateResources(); //zsg updateKeyguardStatusViewAlignment(/* animate= */false); mKeyguardMediaController.refreshMediaPosition(); if (splitShadeChanged) { onSplitShadeEnabledChanged(); } mSplitShadeFullTransitionDistance = mResources.getDimensionPixelSize(R.dimen.split_shade_full_transition_distance); }
- LargeScreenUtils 中的 config_use_large_screen_shade_header 用来判断是否用motionlayout,
- 如果是大屏幕就用则没有不开启motionlayout
-
@JvmStatic fun shouldUseLargeScreenShadeHeader(resources: Resources): Boolean { return resources.getBoolean(R.bool.config_use_large_screen_shade_header) }
- motion 5 如果是用的CombinedQSHeader 则这里不监听触摸事件
QuickStatusBarHeader
@Override
public boolean onTouchEvent(MotionEvent event) {
// Only react to touches inside QuickQSPanel
if (event.getY() > mHeaderQsPanel.getTop()) {
return super.onTouchEvent(event);
} else {
return false;
}
}
- motion 7 这里通过privacyItem是否可见去判断是否让右边的sim卡信息和电池信息显示
- HeaderPrivacyIconsController.kt
-
private val picCallback: PrivacyItemController.Callback = object : PrivacyItemController.Callback { override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) { privacyChip.privacyList = privacyItems setChipVisibility(privacyItems.isNotEmpty()) }
- motion 8 这里添加的回调
-
fun startListening() { listening = true // Get the most up to date info micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable locationIndicatorsEnabled = privacyItemController.locationAvailable privacyItemController.addCallback(picCallback) }
- motion 9 在这里startListening里设置的监听回调,这里的Monitors就是AppOpsPrivacyItemMonitor
PrivacyItemController
private fun setListeningState() {
val listen = callbacks.isNotEmpty()
if (listening == listen) return
listening = listen
if (listening) {
privacyItemMonitors.forEach { it.startListening(privacyItemMonitorCallback) }
update()
} else {
privacyItemMonitors.forEach { it.stopListening() }
// Make sure that we remove all indicators and notify listeners if we are not
// listening anymore due to indicators being disabled
update()
}
}
- motion 10 在这里添加回调appOpsCallback,有打开camera等就回调
AppOpsPrivacyItemMonitor
@GuardedBy("lock")
private fun setListeningStateLocked() {
val shouldListen = callback != null && (micCameraAvailable || locationAvailable)
if (listening == shouldListen) {
return
}
listening = shouldListen
if (shouldListen) {
appOpsController.addCallback(OPS, appOpsCallback)
userTracker.addCallback(userTrackerCallback, bgExecutor)
onCurrentProfilesChanged()
} else {
appOpsController.removeCallback(OPS, appOpsCallback)
userTracker.removeCallback(userTrackerCallback)
}
}
A14 motionLayout in SystemUI
combined_qs_header.xml
MotionLayout 1 在这里设置的Transition 布局
<com.android.systemui.util.NoRemeasureMotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/split_shade_status_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_header_height"
android:minHeight="@dimen/large_screen_shade_header_min_height"
android:clickable="false"
android:focusable="true"
android:paddingLeft="@dimen/qs_panel_padding"
android:paddingRight="@dimen/qs_panel_padding"
android:visibility="gone"
android:theme="@style/Theme.SystemUI.QuickSettings.Header"
app:layoutDescription="@xml/combined_qs_header_scene"><!--zsg here-->
combined_qs_header_scene.xml
MotionLayout 2 注意这里有两个transition,一个header_transition,一个large_screen_header_transition.
在每一个transition中可以设置开始和结束时的两种布局,以及用KeyFrameSet描述过渡动画。
MotionLayout 3 qqs_header 是第一个transition的开始界面布局,qs_header是第一个的结束界面布局
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<!--zsg here 2-->
<Transition
android:id="@+id/header_transition"
app:constraintSetEnd="@id/qs_header_constraint"
app:constraintSetStart="@id/qqs_header_constraint"
motion:layoutDuringTransition="honorRequest">
<KeyFrameSet>
<!-- These positions are to prevent visual movement of @id/date -->
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="0"
app:percentY="@dimen/percent_displacement_at_fade_out"
app:framePosition="@integer/fade_out_complete_frame"
app:sizePercent="0"
app:curveFit="linear"
app:motionTarget="@id/date" />
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="1"
app:percentY="0.5"
app:sizePercent="1"
app:framePosition="50"
app:curveFit="linear"
app:motionTarget="@id/date" />
<KeyAttribute
app:motionTarget="@id/date"
app:framePosition="14"
android:alpha="0"
/>
<KeyAttribute
app:motionTarget="@id/date"
app:framePosition="@integer/fade_in_start_frame"
android:alpha="0"
/>
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="0"
app:percentY="@dimen/percent_displacement_at_fade_out"
app:framePosition="@integer/fade_out_complete_frame"
app:sizePercent="0"
app:curveFit="linear"
app:motionTarget="@id/statusIcons" />
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="1"
app:percentY="0.5"
app:framePosition="50"
app:sizePercent="1"
app:curveFit="linear"
app:motionTarget="@id/statusIcons" />
<KeyAttribute
app:motionTarget="@id/statusIcons"
app:framePosition="@integer/fade_out_complete_frame"
android:alpha="0"
/>
<KeyAttribute
app:motionTarget="@id/statusIcons"
app:framePosition="@integer/fade_in_start_frame"
android:alpha="0"
/>
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="0"
app:percentY="@dimen/percent_displacement_at_fade_out"
app:framePosition="@integer/fade_out_complete_frame"
app:percentWidth="1"
app:percentHeight="1"
app:curveFit="linear"
app:motionTarget="@id/batteryRemainingIcon" />
<KeyPosition
app:keyPositionType="deltaRelative"
app:percentX="1"
app:percentY="0.5"
app:framePosition="50"
app:percentWidth="1"
app:percentHeight="1"
app:curveFit="linear"
app:motionTarget="@id/batteryRemainingIcon" />
<KeyAttribute
app:motionTarget="@id/batteryRemainingIcon"
app:framePosition="@integer/fade_out_complete_frame"
android:alpha="0"
/>
<KeyAttribute
app:motionTarget="@id/batteryRemainingIcon"
app:framePosition="@integer/fade_in_start_frame"
android:alpha="0"
/>
<KeyPosition
app:motionTarget="@id/carrier_group"
app:percentX="1"
app:percentY="0.5"
app:framePosition="50"
app:percentWidth="1"
app:percentHeight="1"
app:curveFit="linear"
app:keyPositionType="deltaRelative" />
<KeyAttribute
app:motionTarget="@id/carrier_group"
app:framePosition="0"
android:alpha="0" />
<KeyAttribute
app:motionTarget="@id/carrier_group"
app:framePosition="@integer/fade_in_start_frame"
android:alpha="0" />
</KeyFrameSet>
</Transition>
<Transition
android:id="@+id/large_screen_header_transition"
app:constraintSetStart="@id/large_screen_header_constraint"
app:constraintSetEnd="@id/large_screen_header_constraint"/>
<Include app:constraintSet="@xml/large_screen_shade_header"/>
<Include app:constraintSet="@xml/qs_header"/>
<Include app:constraintSet="@xml/qqs_header"/><!--zsg here 3-->
</MotionScene>
ShadeModule.kt
MotionLayout 4 在这里通过dagger 依赖注入给ShadeHeaderViewControler
// TODO(b/277762009): Only allow this view's controller to inject the view. See above.
@Provides
@SysUISingleton
@Named(SHADE_HEADER)
fun providesShadeHeaderView(
notificationShadeWindowView: NotificationShadeWindowView,
): MotionLayout {
val stub = notificationShadeWindowView.findViewById<ViewStub>(R.id.qs_header_stub)
val layoutId = R.layout.combined_qs_header
stub.layoutResource = layoutId
return stub.inflate() as MotionLayout
}
status_bar_expanded.xml
MotionLayout 5 qs_header_stub 在这个布局里,它的最外层view是NotificationPanelView
<!-- This view should be after qs_frame so touches are dispatched first to it. That gives
it a chance to capture clicks before the NonInterceptingScrollView disallows all
intercepts -->
<ViewStub
android:id="@+id/qs_header_stub"
android:layout_height="wrap_content"
android:layout_width="match_parent"
/>
NotificationPanelViewController.java
MotionLayout 6 shadeHeaderController 在这里注入,并在下面init
@Inject
public NotificationPanelViewController(NotificationPanelView view,
@Main Handler handler,
LayoutInflater layoutInflater,
..........................................
..........................................
..........................................
mView.setWillNotDraw(!DEBUG_DRAWABLE);
mShadeHeaderController = shadeHeaderController;//zsg here
mLayoutInflater = layoutInflater;
ShadeHeaderController.kt
MotionLayout 7 在这里注入view ,@Named(SHADE_HEADER) 用来区分相同的属性名 不同对象的情况
@SysUISingleton
class ShadeHeaderController
@Inject
constructor(
@Named(SHADE_HEADER) private val header: MotionLayout, //zsg
ShadeHeaderController.kt
MotionLayout 8 在这里通过largeScreenActive判断是用哪个transition
private fun updateTransition() {
if (largeScreenActive) {
logInstantEvent("Large screen constraints set")
header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
} else {
logInstantEvent("Small screen constraints set")
header.setTransition(HEADER_TRANSITION_ID)
}
header.jumpToState(header.startState)
updatePosition()
updateScrollY()
}
QuickSettingsController.java
MotionLayout 9 在这里监听的滑动,并且判断是否展开到Qs
private void onScroll(int scrollY) {
mShadeHeaderController.setQsScrollY(scrollY);
if (scrollY > 0 && !mFullyExpanded) {
// TODO (b/265193930): remove dependency on NPVC
// If we are scrolling QS, we should be fully expanded.
mPanelViewControllerLazy.get().expandToQs();
}
}
ShadeHeaderController.kt
MotionLayout 10 这里就是让motionlayout动起来的地方,设置header.progress
private fun updatePosition() {
if (!largeScreenActive && visible) {
logInstantEvent("updatePosition: $qsExpandedFraction")
header.progress = qsExpandedFraction
updateBatteryMode()
}
}
ShadeCarrierGroupController.java
MotionTwo 1 这里是控制wwan设备显示 无SIM卡 ,WLAN设备上carrierText是空就不显示
@MainThread
private void handleUpdateCarrierInfo(CarrierTextManager.CarrierTextCallbackInfo info) {
if (!mMainHandler.getLooper().isCurrentThread()) {
mMainHandler.obtainMessage(H.MSG_UPDATE_CARRIER_INFO, info).sendToTarget();
return;
}
mNoSimTextView.setVisibility(View.GONE);
if (!info.airplaneMode && info.anySimReady) {
boolean[] slotSeen = new boolean[SIM_SLOTS];
if (info.listOfCarriers.length == info.subscriptionIds.length) {
for (int i = 0; i < SIM_SLOTS && i < info.listOfCarriers.length; i++) {
int slot = getSlotIndex(info.subscriptionIds[i]);
if (slot >= SIM_SLOTS) {
Log.w(TAG, "updateInfoCarrier - slot: " + slot);
continue;
}
if (slot == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
Log.e(TAG,
"Invalid SIM slot index for subscription: "
+ info.subscriptionIds[i]);
continue;
}
String carrierText = info.listOfCarriers[i].toString().trim();
if (!TextUtils.isEmpty(carrierText)) {
mInfos[slot] = mInfos[slot].changeVisibility(true);
slotSeen[slot] = true;
mCarrierGroups[slot].setCarrierText(carrierText);
mCarrierGroups[slot].setVisibility(View.VISIBLE);
}
}
for (int i = 0; i < SIM_SLOTS; i++) {
if (!slotSeen[i]) {
mInfos[i] = mInfos[i].changeVisibility(false);
mCarrierGroups[i].setVisibility(View.GONE);
}
}
} else {
Log.e(TAG, "Carrier information arrays not of same length");
}
} else {
// No sims or airplane mode (but not WFC). Do not show ShadeCarrierGroup,
// instead just show info.carrierText in a different view.
for (int i = 0; i < SIM_SLOTS; i++) {
mInfos[i] = mInfos[i].changeVisibility(false);
mCarrierGroups[i].setCarrierText("");
mCarrierGroups[i].setVisibility(View.GONE);
}
mNoSimTextView.setText(info.carrierText);
if (!TextUtils.isEmpty(info.carrierText)) { //zsg here
mNoSimTextView.setVisibility(View.VISIBLE);
mNoSimTextView.setTextColor(Color.YELLOW);
Log.i(TAG, "info.carrierText: "+info.carrierText);
}
}
handleUpdateState(); // handleUpdateCarrierInfo is always called from main thread.
}
HeaderPrivacyIconsController.kt
MotionTwo 2 这里控制camera绿色图标的显示,下面onChipVisibilityRefreshed控制当camera图标显示时,原来的其他图标是否显示
private fun setChipVisibility(visible: Boolean) {
if (visible && getChipEnabled()) {
privacyLogger.logChipVisible(true)
// Makes sure that the chip is logged as viewed at most once each time QS is opened
// mListening makes sure that the callback didn't return after the user closed QS
if (!privacyChipLogged && listening) {
privacyChipLogged = true
uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW)
}
} else {
privacyLogger.logChipVisible(false)
}
privacyChip.visibility = if (visible) View.VISIBLE else View.GONE
chipVisibilityListener?.onChipVisibilityRefreshed(visible)
}
CombinedShadeHeadersConstraintManager.kt
MotionTwo 3 这里是用上面typealias定义的一个高阶函数,
方法体里是定义了这个数据类可以直接用+号合并,
所以之前其实就是构造了一个ConstraintsChanges ,传入了一个设置ConstraintSet的高阶函数
/**
* Contains all changes that need to be performed to the different [ConstraintSet] in
* [ShadeHeaderController].
*/
data class ConstraintsChanges(
val qqsConstraintsChanges: ConstraintChange? = null,
val qsConstraintsChanges: ConstraintChange? = null,
val largeScreenConstraintsChanges: ConstraintChange? = null
) {
operator fun plus(other: ConstraintsChanges) = ConstraintsChanges(
qqsConstraintsChanges + other.qqsConstraintsChanges,
qsConstraintsChanges + other.qsConstraintsChanges,
largeScreenConstraintsChanges + other.largeScreenConstraintsChanges
)
}
ShadeHeaderController.kt
MotionTwo 4 这里是一个扩展函数
private val chipVisibilityListener: ChipVisibilityListener =
object : ChipVisibilityListener {
override fun onChipVisibilityRefreshed(visible: Boolean) {
// If the privacy chip is visible, we hide the status icons and battery remaining
// icon, only in QQS.
val update =
combinedShadeHeadersConstraintManager.privacyChipVisibilityConstraints(visible)
header.updateAllConstraints(update)//zsg here
}
}
ShadeHeaderController.kt
MotionTwo 5 扩展函数,然后调用下面的扩展函数updateConstraints
MotionTwo 6 最后调用更新MotionLayout
private fun MotionLayout.updateConstraints(@IdRes state: Int, update: ConstraintChange) {//zsg 6
val constraints = getConstraintSet(state)
constraints.update()
updateState(state, constraints)
}
/**
* Updates the [ConstraintSet] for the case of combined headers.
*
* Only non-`null` changes are applied to reduce the number of rebuilding in the [MotionLayout].
*/
private fun MotionLayout.updateAllConstraints(updates: ConstraintsChanges) {
if (updates.qqsConstraintsChanges != null) {
updateConstraints(QQS_HEADER_CONSTRAINT, updates.qqsConstraintsChanges)
}
if (updates.qsConstraintsChanges != null) {
updateConstraints(QS_HEADER_CONSTRAINT, updates.qsConstraintsChanges)
}
if (updates.largeScreenConstraintsChanges != null) {
updateConstraints(LARGE_SCREEN_HEADER_CONSTRAINT, updates.largeScreenConstraintsChanges)
}
}
二、一个motionLayout的demo
https://developer.android.google.cn/develop/ui/views/animations/motionlayout/examples
CollapsingToolbar.kt
//zsg AppBarLayout 在这里监听关联的view的滑动
// When the AppBarLayout progress changes, snap MotionLayout to the current progress
val listener = AppBarLayout.OnOffsetChangedListener { appBar, verticalOffset ->
// convert offset into % scrolled
val seekPosition = -verticalOffset / appBar.totalScrollRange.toFloat()
// inform both both MotionLayout and CutoutImage of the animation progress.
binding.motionLayout.progress = seekPosition//zsg 设置motionLayout的进度
binding.background.translationProgress = (100 * seekPosition).toInt()
}
activity_collapsing_toolbar.xml
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!-- Use a CoordinatorLayout and AppBar to build a Collapsing Toolbar from MotionLayout -->
<!-- See: https://developer.android.com/reference/androidx/coordinatorlayout/widget/CoordinatorLayout -->
<!-- See: https://developer.android.com/reference/com/google/android/material/appbar/AppBarLayout-->
<androidx.coordinatorlayout.widget.CoordinatorLayout 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">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<include layout="@layout/lots_of_cards" />
</androidx.core.widget.NestedScrollView>
<!--AppBarLayout is a vertical LinearLayout which implements many of the features of material designs app bar concept, namely scrolling gestures.
Children should provide their desired scrolling behavior through AppBarLayout.LayoutParams.setScrollFlags(int) and the associated layout xml attribute: app:layout_scrollFlags.
This view depends heavily on being used as a direct child within a CoordinatorLayout. If you use AppBarLayout within a different ViewGroup, most of its functionality will not work.
AppBarLayout also requires a separate scrolling sibling in order to know when to scroll. The binding is done through the AppBarLayout.ScrollingViewBehavior behavior class, meaning that you should set your scrolling view's behavior to be an instance of AppBarLayout.ScrollingViewBehavior. A string resource containing the full class name is available.-->
<!--zsg AppBarLayout 一般只在CoordinatorLayout 里使用,它会和设置layout_behavior 的同级可滑动view关联上-->
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_layout"
android:background="#00000000"
android:layout_width="match_parent"
android:layout_height="200dp">
<!--zsg 这里设置trasition activity_collapsing_toolbar_scene-->
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/motion_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="100dp"
app:layoutDescription="@xml/activity_collapsing_toolbar_scene"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<com.example.androidstudio.motionlayoutintegrations.CutoutImage
android:id="@+id/background"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="centerCrop"
android:src="@drawable/star_parallax"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/expanded_bottom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:bottomCut="120dp"
app:cutoutColor="@color/white"
app:layout_constraintVertical_bias="0.52" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:textColor="@color/white"
android:text="@string/ringed_planet"
android:textAppearance="?attr/textAppearanceHeadline5"
app:layout_constraintBottom_toTopOf="@id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.24000001" />
<TextView
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/white"
android:text="@string/added_in_2019_in_unicode_12_0"
android:textAppearance="?attr/textAppearanceBody2"
android:visibility="invisible"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/collapsed_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_end="100dp"
/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/expanded_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintGuide_end="60dp"
android:orientation="horizontal"
/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/inset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="0dp"
/>
<TextView
android:id="@+id/icon"
android:layout_width="100dp"
android:layout_height="100dp"
android:gravity="center"
android:text="@string/ringed_planet_emoji"
app:autoSizeTextType="uniform" />
<View
android:id="@+id/systembar_overlay"
android:layout_width="0dp"
android:layout_height="100dp"
android:background="#0E328F"
app:layout_constraintTop_toTopOf="@id/collapsed_top"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:alpha="0.5"
/>
</androidx.constraintlayout.motion.widget.MotionLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
activity_collapsing_toolbar_scene.xml
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<MotionScene xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<Transition
app:constraintSetEnd="@+id/collapsed"
app:constraintSetStart="@id/expanded"
>
<!--zsg KeyPosition 是view在对应某个时刻的位置 framePosition="25" 表示当到达整个过程的25%时-->
<!--zsg KeyAttribute是view在对应某个时刻的大小形状-->
<!--zsg percentX percentY 的方向是从起始位置到结束位置,距离就是设置的分数 1表示到达最后的位置处 0 表示不动-->
<KeyFrameSet >
<!-- Hold title in place -->
<KeyPosition
app:motionTarget="@+id/title"
app:framePosition="25"
app:percentX="0.0"
app:percentY=".2" />
<!-- At 55, title should still be in place -->
<KeyPosition
app:motionTarget="@+id/title"
app:framePosition="55"
app:percentX="0" />
<!-- at 75, title is 50% to destination -->
<KeyPosition
app:motionTarget="@+id/title"
app:framePosition="75"
app:percentX=".5" />
<!-- Hide subtitle until 55 -->
<KeyAttribute
app:motionTarget="@+id/subtitle"
app:framePosition="55"
android:alpha="0" />
<!-- Hold subtitle in place until 55 -->
<KeyPosition
app:motionTarget="@+id/subtitle"
app:framePosition="55"
app:percentX="0" />
<!-- at 55, subtitle is 50% to destination -->
<KeyPosition
app:motionTarget="@+id/subtitle"
app:framePosition="75"
app:percentX=".5" />
<!-- "pop" icon out at 35, with some translation to make it visually stay in place -->
<KeyAttribute
app:motionTarget="@+id/icon"
app:framePosition="35"
android:scaleX="0"
android:scaleY="0"
android:translationY="30dp"
android:translationX="0dp"
app:curveFit="linear" />
<!-- Don't move icon along curve until 35 -->
<KeyPosition
app:motionTarget="@+id/icon"
app:framePosition="35"
app:percentX="0"
app:percentY="0"
app:curveFit="linear" />
<!-- Between 35 and 36 while icon is not visible, jump icon to it's final location -->
<KeyPosition
app:motionTarget="@+id/icon"
app:framePosition="36"
app:percentX="1"
app:percentY="1"
app:curveFit="linear" />
<!-- Translate icon a bit to make it "fill in" after jump -->
<KeyAttribute
app:motionTarget="@+id/icon"
app:framePosition="36"
android:translationY="-30dp"
android:translationX="20dp"
app:curveFit="linear" />
<!-- Animate the bottomCutSize to 0 -->
<KeyAttribute
app:motionTarget="@id/background"
app:framePosition="60">
<CustomAttribute app:attributeName="bottomCutSize" app:customDimension="0dp" />
</KeyAttribute>
<!-- overshoot endCutSize -->
<KeyAttribute
app:motionTarget="@id/background"
app:framePosition="80">
<CustomAttribute app:attributeName="endCutSize" app:customDimension="220dp" />
</KeyAttribute>
<!-- bring the system bar overlay in slightly early (numbers decided visually) -->
<KeyPosition
app:motionTarget="@+id/systembar_overlay"
app:framePosition="45"
app:percentY=".50"
app:sizePercent="0.50" />
</KeyFrameSet>
</Transition>
<!-- Start constraint set uses expanded_bottom and parent as bottom and top bounds -->
<ConstraintSet android:id="@+id/expanded">
<Constraint
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="@id/parent"
app:layout_constraintBottom_toTopOf="@id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="16dp"
android:layout_marginTop="0dp"
/>
<Constraint
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="@id/title"
android:visibility="invisible"
android:layout_marginTop="8dp"
android:alpha="0" />
<Constraint
android:id="@+id/icon"
app:layout_constraintTop_toTopOf="@id/expanded_bottom"
app:layout_constraintBottom_toTopOf="@id/expanded_bottom"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="100dp"
android:layout_height="100dp"
/>
<Constraint
android:id="@+id/background"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/expanded_bottom"
app:layout_constraintStart_toStartOf="parent">
<CustomAttribute app:attributeName="bottomCutSize" app:customDimension="120dp" />
<CustomAttribute app:attributeName="endCutSize" app:customDimension="0dp" />
</Constraint>
<Constraint
android:id="@id/systembar_overlay"
android:layout_height="0dp"
android:layout_width="0dp"
android:alpha="0.5"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</ConstraintSet>
<!-- end constraint set uses collapsed_top and parent as top and bottom, with inset as
guideline -->
<ConstraintSet android:id="@+id/collapsed" >
<Constraint
android:id="@+id/title"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginBottom="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
app:layout_constraintTop_toTopOf="@+id/inset"
app:layout_constraintBottom_toTopOf="@id/subtitle"
app:layout_constraintStart_toStartOf="parent"
/>
<Constraint
android:id="@+id/subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_marginBottom="0dp"
android:layout_marginTop="0dp" />
<Constraint
android:id="@+id/icon"
android:layout_width="50dp"
android:layout_height="50dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="@id/inset"
android:layout_marginEnd="16dp" />
<Constraint
android:id="@+id/background"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="@id/collapsed_top"
app:layout_constraintStart_toStartOf="parent">
<CustomAttribute app:attributeName="bottomCutSize" app:customDimension="0dp" />
<CustomAttribute app:attributeName="endCutSize" app:customDimension="180dp" />
</Constraint>
<Constraint
android:id="@id/systembar_overlay"
android:layout_height="0dp"
android:layout_width="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/inset"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:alpha="0.5"
/>
</ConstraintSet>
</MotionScene>