To learn Behavior, start with usage. Because usage is the easiest to learn, because its complex logic will be hidden inside, exposed to external use is very simple to understand the method, therefore, learning needs to start with usage, and then deep into the source code understanding ideas.
What are behaviors
First of all, Behavior is a subclass of CoordinatorLayout, and CoordinatorLayout, by its name, is a coordination layout, in other words, a controller, a moderator, a backer.
All it does is manipulate its child Views and let them interact. Interaction includes dependency (one view interacts with another view), measurement layout (control the measurement process and layout process of sub-views), touch events (freely specify a view to intercept events), nesting sliding (multiple layouts slide together), etc.
But it’s not possible to write all this stuff in CoordinatorLayout, because that would make it very bloated and not free enough to use the interactions that it defines. For this reason, CoordinatorLayout extracts these functions from each other and forms a separate module that defines standards and allows developers to customize the interactions based on those standards, which can then be loaded and operated by CoordinatorLayout.
So you know what Behavior is. It is the module extracted by CoordinatorLayout, or more appropriately, it should be a plug-in standard.
What can Behavior do
What Behavior can do is the function of CoordinatorLayout mentioned earlier. Before introducing these features, let’s look at how to use Behavior.
Use behaviors
To use a Behavior, you first construct a Behavior and then set it to the View. So, how do we construct Behavior?
public Behavior(a) {}
public Behavior(Context context, AttributeSet attrs) {}
Copy the code
Behavior has two constructors, an empty parameter constructor and a two-parameter constructor. For those of us familiar with custom views, the two-parameter constructor looks like it should be instantiated from XML.
Indeed, empty arguments are typically constructed by manually creating instances and setting them, while two-argument arguments are typically set directly in XML. Notice that you can only set one Behavior per child View.
-
1. Settings in XML
Set the Behavior of a child view directly in XML via app:layout_behavior=” XXX “, where XXX stands for the class name of the Behavior. It can be abbreviated as. MyBehavior can also be complete like com.xx.test.myBehavior. Full package names are recommended, because using abbreviated names can cause errors when redefining a package in XML.
-
2. Create instance Settings directly
To create a Behavior manually, fetch the child’s params and call setBehavior to set it. Namely CoordinateLayout. LayoutParams# setBehavior
-
3. Set through annotations
You can also set the Behavior using annotations, but you need to customize the View and bind it to the class name by adding @defaultBehavior (myBehavior.class). This approach requires a custom view, which is cumbersome and rarely used.
Generally speaking, the first method is the most used, because it is not too easy to use. Also, the first approach takes the Behavior’s two-parameter constructor, and we can even customize the property Settings in the XML and hand them to the Behavior to read. As follows:
- 1, in
attrs.xml
To define attributes and names, and customview
Be consistent with
<resources>
<declare-styleable name="MovableButton_Behavior">
<attr name="sex" format="string" />
</declare-styleable>
</resources>
Copy the code
- 2, in
xml
Set this property in
<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"
tools:context=".MainActivity">
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_behavior=".behavior.AttrBehavior"
app:sex="aaaa" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Copy the code
- 3, in
Behavior
Read attribute in
class AttrBehavior(
context: Context,
attributeSet: AttributeSet
) : CoordinatorLayout.Behavior<View>(context, attributeSet) {
init {
context.obtainStyledAttributes(attributeSet, R.styleable.MovableButton_Behavior).use {
val sex = it.getString(R.styleable.MovableButton_Behavior_sex)
println("The app:sex property set in XML is:$sex")}}}Copy the code
It’s exactly the same as the custom properties of a custom View, very simple. Note that the generic parameters of the Behavior represent the controls on which the Behavior can be set. In the example above, all views can set the Behavior. If set to ImageView, only ImageView and its subclasses can use this Behavior.
Measurement and layout of Behavior capacity
The display of a View usually goes through three steps: measurement, layout, and drawing. The first two steps can also be implemented in the Behavior because CoordinatorLayout puts the process of measuring and layout of its subviews into the Behavior. If you choose to customize the measurement layout in the Behavior, the CoordinatorLayout doesn’t measure it anymore.
So what is left of CoordinatorLayout that extracts measurements and layouts?
CoordinatorLayout is a super-powered FrameLayout
That’s the first sentence of a CoordinatorLayout annotation, which means there’s nothing special about it, it’s just an ordinary FrameLayout. The common place is that it does not have other layout ability, just like FrameLayout layer by layer. However, it has the expansion ability that FrameLayout does not have, that is, it can expand various abilities for itself through Behavior, including but not limited to the layout and measurement of sub-views.
Corresponding method
The measurement and layout method names are very similar to the View method names. Override the two methods in Behavior to implement custom measurement and layout, and return true. Note that you must return true, otherwise the current customization will not take effect.
boolean onMeasureChild(
@NonNull CoordinatorLayout parent,
@NonNull V child,
int parentWidthMeasureSpec,
int widthUsed,
int parentHeightMeasureSpec,
int heightUsed
)
boolean onLayoutChild(
@NonNull CoordinatorLayout parent,
@NonNull V child,
int layoutDirection
)
Copy the code
Custom measurements of onMeasureChild are relatively rare, because by default the parent layout measures child Views like FrameLayout, which is mostly sufficient, and onLayoutChild measures more.
Small example
Here’s an example of how to use onLayoutChild:
class LayoutBehavior(
context: Context,
attr: AttributeSet
) : CoordinatorLayout.Behavior<View>(context, attr) {
override fun onLayoutChild(
parent: CoordinatorLayout,
child: View,
layoutDirection: Int
): Boolean {
// Check if there is a Button
var target: Button? = null
for (i in 0 until parent.childCount) {
if (parent.getChildAt(i) is Button) {
target = parent.getChildAt(i) as Button
break} } target ? :return false
// Place the child at the bottom right of the Button
child.layout(
target.right,
target.bottom,
target.right + child.measuredWidth,
target.bottom + child.measuredHeight
)
return true}}Copy the code
Then in the XML, use this Behavior:
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.LayoutActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Button"
tools:ignore="HardcodedText" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/bg"
app:layout_behavior=".behavior.LayoutBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Copy the code
Then run and see that the ImageView is placed in the lower right corner of the Button.
Behavior dependence
Behavior can realize the dependency relationship between two sub-views. One View depends on the other View, so when the position and size of the dependent View changes or is removed, the other View will also trigger the corresponding operation.
Corresponding method
public boolean layoutDependsOn(
@NonNull CoordinatorLayout parent,
@NonNull V child,
@NonNull View dependency
)
Copy the code
Firstly, the layoutDependsOn method is used to determine the dependency relationship in Behavior. This method takes three arguments. The first argument is the parent layout; The second argument is the View with the Behavior set, called child; The third parameter is the dependent View. All we need to do in this method is determine whether the child and view need to have a dependency, return true if there is a dependency, return false otherwise. Behavior iterates over all views except child, and then passes it into this method to determine if it is a dependent object.
If parent has three child views and one of them has Behavior, the layoutDependsOn method is called twice with parent and child parameters unchanged and dependency parameters changed each time. Therefore, the dependencies are one-to-many.
Note that in the Behavior, parent represents the parent layout and child represents the child View of the Behavior
public boolean onDependentViewChanged(
@NonNull CoordinatorLayout parent,
@NonNull V child,
@NonNull View dependency
)
Copy the code
OnDependentViewChanged is the method that will be invoked when the dependency occurs, in which the child can be declared to respond to the dependency. Also, if the size or position of the child is changed in this method, return true.
public void onDependentViewRemoved(
@NonNull CoordinatorLayout parent,
@NonNull V child,
@NonNull View dependency
)
Copy the code
The onDependentViewRemoved method is called when the dependent View is removed from the parent layout, which is when the child loses a dependency.
Small example
For example, you define two views in CoordinatorLayout, one of which is dependent on the other and always below the dependent View.
Define a moveable button MovableButton, because dependency events can occur only if the position or size of the dependent View changes, so we need to have a moveable View. Let’s define a MovableButton without any real content.
// A simple view that can be followed by a finger
class MovableButton @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null
) : AppCompatButton(context, attributeSet) {
private var mInitX = 0F
private var mInitY = 0F
private var mEventX = 0F
private var mEventY = 0F
override fun onTouchEvent(event: MotionEvent?).: Boolean {
when(event? .actionMasked) { MotionEvent.ACTION_DOWN -> { mInitX = x mInitY = y mEventX = event.rawX mEventY = event.rawY } MotionEvent.ACTION_MOVE -> { x = mInitX + event.rawX - mEventX y = mInitY + event.rawY - mEventY } }return super.onTouchEvent(event)
}
}
Copy the code
Then start writing behaviors, where you determine the dependencies and define the corresponding operations.
class BelowBehavior(
context: Context,
attributeSet: AttributeSet
) : CoordinatorLayout.Behavior<View>(context, attributeSet) {
override fun layoutDependsOn(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
// Only MovableButton can be used as a dependent View
return dependency is MovableButton
}
override fun onDependentViewChanged(
parent: CoordinatorLayout,
child: View,
dependency: View
): Boolean {
// Keep the child under the dependent View
child.y = dependency.height + dependency.translationY
return true
}
override fun onDependentViewRemoved(
parent: CoordinatorLayout, child: View,
dependency: View
) {
// When the dependent view is removed, reset the position of the child at the top of the interface
child.y = 0F}}Copy the code
Note that determining a dependency condition in a layoutDependsOn is easy as long as the View is a MovableButton. The actual conditions should be more complex, because simple conditions can easily form multiple dependent views.
Then use Behavior in XML. Note that it is easier to use Behavior in XML:
<androidx.coordinatorlayout.widget.CoordinatorLayout
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/parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.study.androidbehavior.widget.MovableButton
android:id="@+id/movable_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/bg"
app:layout_behavior=".behavior.BelowBehavior" />
<Button
android:id="@+id/button_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="Remove button" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Copy the code
There are three views defined in XML, and the MovableButton is treated as a dependent View and can be moved at will. The ImageView is the child View that sets the Behavior, all the way underneath the MovableButton. Button one click removes the MovableButton from the parent layout and is used to trigger the dependency disappearance event.
Finally, we write random click events in the activity:
class BelowActivity : AppCompatActivity() {
private lateinit var mParent: CoordinatorLayout
private lateinit var mButtonMovable: MovableButton
private lateinit var mButtonRemove: Button
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState) setContentView(R.layout.activity_below) mParent = findViewById(R.id.parent) mButtonMovable = findViewById(R.id.movable_button) mButtonRemove = findViewById(R.id.button_remove) mButtonRemove.setOnClickListener { mParent.removeView(mButtonMovable) } } }Copy the code
The final effect is as follows:
Behavior ability touch event
In View event distribution, events are first distributed to the innermost child View. When the child View decides not to handle the touch event, the outer parent layout will be given the opportunity to handle the event. This is not always the case, however, because the parent layout has a method for intercepting events so that they are passed directly to the parent layout for processing, rather than to the child View.
CoordinatorLayout as a parent (ViewGroup) must have this interception function, but again, it doesn’t implement the interception mechanism itself, it extracts this function into the Behavior, Behavior decides whether or not the CoordinatorLayout intercepts the event. When you decide to intercept events, the same CoordinatorLayout doesn’t process those events, it passes those events to the behaviors that decide to intercept events, and the behaviors process them.
Corresponding method
public boolean onInterceptTouchEvent(
@NonNull CoordinatorLayout parent,
@NonNull V child,
@NonNull MotionEvent ev
)
public boolean onTouchEvent(
@NonNull CoordinatorLayout parent,
@NonNull V child,
@NonNull MotionEvent ev
)
Copy the code
As with ViewGroup, onInterceptTouchEvent intercepts it and handles it in onTouchEvent. Notice that all of the capabilities in Behavior are derived from CoordinatorLayout, that is, CoordinatorLayout transfers work that it should have done itself to the Behavior. Therefore, the event distribution process is first to the CoordinatorLayout, and then the sub-view is distributed in the order of topMost. So the topMost order is going from top to bottom, and the child View is going from back to front, so the last child View is the top one.
Small example
There are no small examples because dealing with events directly is too cumbersome.
Behavior ability for nested sliding
When two cosliding views nested together, such as ScrollView has a RecyclerView child, then when rolling ScrollView or RecyclerView first? If it is a rolling RecyclerView, then when RecyclerView is fast rolling to the bottom, this time to a long rolling, then when RecyclerView is rolling to the bottom, the remaining rolling events are not reactive, and will not become ScrollView and then rolling. This is due to the event distribution mechanism of the View. In View event distribution, when one View processes an event, the event stream is handed over to it and not to another View.
Nested sliding is used to handle this kind of conflict, especially with the ability to distribute a stream of events to multiple Views, which makes sliding smoother and more desirable. It does this using two sets of interfaces, NestedScrollingChild3 and NestedScrollingParent3, which correspond to the child View and the parent View, but you can also implement both interfaces at the same time, so you can do whatever you want.
CoordinatorLayout implements the NestedScrollingParent3 interface, but instead of dealing with slide events like traditional nested slides, it delegates slide events to Behavior, so in nested slides, Behavior actually handles the sliding event as parent. In other words, nesting slides in CoordinatorLayout do not require nesting, and since the parent is the Behavior in nesting slides, the child View that wants to be the parent only needs to set the Behavior. Instead of implementing the NestedScrollingParent3 interface.
In summary, CoordinatorLayout uses nested slide logic to implement a nested slide that is not nested.
Note that nested sliding must be initiated by NestedScrollingChild3, and RecyclerView implements this interface.
Corresponding method
public boolean onStartNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View directTargetChild,
@NonNull View target,
@ScrollAxis int axes,
@NestedScrollType int type
)
Copy the code
When nested sliding occurs, this method will be called first to determine whether the Behavior needs to participate in the sliding. Return true to indicate that the Behavior needs to participate in the sliding, and then there will be subsequent method calls. Otherwise, subsequent events will not be called back to the Behavior.
The first two parameters are not mentioned. The third parameter directTargetChild is the direct child layout of the sliding View in CoordinatorLayout, while target represents the sliding View. When a target appears directly in CoordinatorLayout, the directTargetChild and Target are the same object. In the following layout, directTargetChild is FrameLayout and Target is RecyclerView.
<androidx.coordinatorlayout.widget.CoordinatorLayout...>
<FrameLayout...>
<androidx.recyclerview.widget.RecyclerView... />
</FrameLayout>.</androidx.coordinatorlayout.widget.CoordinatorLayout>
Copy the code
The last two parameters are the direction of the scroll and the type of scroll. Axes indicates the direction of sliding, which can be horizontal or vertical and can be ViewCompat#SCROLL_AXIS_HORIZONTAL or ViewCompat#SCROLL_AXIS_VERTICAL. Type indicates the type of sliding, which can be touch scrolling or non-touch scrolling (inertial scrolling). The value can be ViewCompat#TYPE_TOUCH or ViewCompat#TYPE_NON_TOUCH.
Return true when the Behavior wants to participate in the nested slide.
public void onNestedScrollAccepted(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View directTargetChild,
@NonNull View target,
@ScrollAxis int axes,
@NestedScrollType int type
)
Copy the code
OnNestedScrollAccepted has the same parameter as onStartNestedScroll. This method is called when onStartNestedScroll returns true and is bound to onStartNestedScroll, This method will be called every time onStartNestedScroll returns true. You can use this method to handle pre-preparations for nested slides, such as initialization states.
public void onNestedPreScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
int dx,
int dy,
@NonNull int[] consumed,
@NestedScrollType int type
)
public void onNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
int dxConsumed,
int dyConsumed,
int dxUnconsumed,
int dyUnconsumed,
@NestedScrollType int type,
@NonNull int[] consumed
)
Copy the code
The above two methods are the ones that actually slide back. After a nested slide occurs and the Behavior accepts the slide, the onNestedPreScroll method is called first. Note that this method occurs in the Behavior, that is, when there is a sliding event, it is passed to the Behavior first. The parameters dx and dy represent the sliding distance, while the length of array consumed is 2, which represents the sliding distance consumed by Behavior. Consumed [0] is the consumption of DX, and consumed[1] is the consumption of DY. After the Behavior consumption slides, it needs to fill the consumed amount into the array manually.
When the Behavior finishes processing, the remaining events are passed to Target for processing. Of course, the processing at this time has nothing to do with us, but is handled by target’s own logic. When target finishes scrolling, the remaining events are passed to the parent and then to the Behavior’s onNestedScroll method. The middle four parameters of this method are nothing to be said for, as their names suggest, are the consumed and unconsumed values of dx and dy. The Consumed array is the same, holding consumed events. Note that at this point the Consumed data may already have a value, so we need to overlay instead of assign. For example, consumed deltaY, it should consumed[1] += deltaY.
public boolean onNestedPreFling(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
float velocityX,
float velocityY
)
public boolean onNestedFling(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
float velocityX,
float velocityY,
boolean consumed
)
Copy the code
When the View is inertia sliding, it is also transferred to the Behavior first. The onNestedPreFling method is used to determine whether the Behavior needs to consume the inertia slide and return true if it does. It then returns the overScroll to the child View to determine whether it needs to display the overScroll and then passes it back to the Behavior through onNestedFling for real inertial sliding. Parameter consumed indicates whether the nested child View(the child View that initiated the scroll) consumes the inertial slide. If the Behavior consumes the scroll, it needs to return true.
public void onStopNestedScroll(
@NonNull CoordinatorLayout coordinatorLayout,
@NonNull V child,
@NonNull View target,
@NestedScrollType int type
)
Copy the code
After scrolling, the onStopNestedScroll method is called, and you can do some finishing work in that method. So nested sliding involves seven methods altogether, two methods at the beginning of the slide, two methods during the slide, two methods at the inertial slide, and one method at the end. And the order of sliding events is child View->Behavior-> child View->Behavior. So the sliding process and the inertial sliding process are both two methods, one is to do the first time, and the other is to do the sub-view and then do the rest.
Small example
Use Behavior to implement a nested sliding effect that follows a scroll
Full code: Click the jump to Github link
conclusion
First of all, Behavior is a plug-in, which is a standard that CoordinatorLayout extracts. CoordinatorLayout extracts all of its capabilities into the Behavior and loads it when needed to achieve some interaction.
Second, Behavior has four capabilities: measurement layout, dependency, Touch, and nested sliding. Dependency and nested sliding are the most popular, followed by layout and measurement, and finally touch.
Once you have learned how to use the various methods of Behavior, you can design all kinds of fancy operations, and then you can learn the Behavior.