RankingsListView
RankingsListView
(heir of the ScrollView
). He manages a scroll (unexpectedly, yes?) And zoom, and also creates a list from the RankingView
.RankingView
RankingView
displays rank (left) and UsersView
(right).UsersView
UsersView
, as you might have guessed, is engaged in displaying users and showing animations of combining and disconnecting users into groups.GroupView
GroupView
. Only in the case when one user is displayed, and not a group, there will be no green circle (within which the number of users in the group is displayed). Well, on the right there is an indicator of user / group points with a “%” sign.View
using DataBinding
DataBinding
with its code generation works wonders: WidgetGroupViewBinding binding; … binding = DataBindingUtil.setContentView(this, R.layout.activity_main); // binding.title
binding
are available on id
all the twists indicated in the markup, no matter how complex this markup is. No more: @ BindView (R.id.tb_progress) View loadingView; @BindView (R.id.tb_user) View userView; @BindView (R.id.iv_avatar) ImageView avatarView; @BindView (R.id.tv_name) TextView nameView; @BindView (R.id.l_error) View errorView; @BindView (R.id.l_container) ViewGroup containerLayout;
ButterKnife
. But wait! setContentView()
is the Activity
method. And what about viewsinflate(getContext(), R.layout.my_view_layout, this)
method, for example, inside the constructor. The last flag is interesting. It adds a view created by markup inside the current view. This means that if you have a root tag in your markup, for example, LinearLayout
, and you try to use inflate(…)
inside your view inherited from LinearLayout
, then you will get two LinearLayout in the hierarchy ...LinearLayout
's is redundant. What to do? To get around this is quite simple. You need to use the <merge>
inside the markup, wrapping everything in it that should be inside your view, as described here .DataBinding
does not support <merge>
. Its root tag must be a <layout>
, inside of which there must be a single child element — not- <merge>
(in fact, there may be another <data>
, but this is a completely different story).DataBinding
in your own views without generating additional Layouts, which will not have the best effect on performance. ButterKnife is still our everything.onMeasure()
couple of times to determine the size;onLayout()
is called to position the element inside the container;onDraw()
is called to draw.onMeasure()
is for sizing only. It makes absolutely no sense to put anything else in it. The reason is not only that the method is called several times, but also that it is impossible to say with certainty whether it will be called again before onLayout()
or whether the current calculated size is final;onSizeChanged()
, which for some incredible reason is often not mentioned in the articles on custom views, is called before onLayout()
, but inside the layout()
method called by the parent. The bottom line is that layout()
calls setFrame()
, which calls onSizeChanged()
, and only then (after exiting setFrame()
) onLayout()
called. This means that inside the onSizeChanged()
method you still cannot rely on the fact that all the child views within your view are arranged as needed. Moreover, they have not yet called their onSizeChanged()
, not to mention onLayout()
;onLayout()
you can call measureChildren()
yourself. In other words, if I wish to twist it, he may drive the measurement pass a few more times;onLayout()
better to leave super.onLayout()
. Otherwise, you can create a situation in which the child view will be called onSizeChanged()
twice with onLayout()
. And if you are unlucky, you can get stuck in such a way that every 16ms (60 fps after all) requestLayout()
will be called (spoiler: the author shot himself in the leg, but more on that later).onLayout()
UsersView.onLayout()
, the Y-coordinates of the views change, which causes some views to become visible, while others hide, and this leads to ... (attention to the bottom right):Bitmap drawingCache;
. Then that additional measurement pass in the form of measureChildren()
inside onLayout()
came to the rescue, forcing the view to reconsider its caches after changing its Y-coordinate.ScrollView
does not allow ScrollView
childScrollView
element layout_height=match_parent
, and then instantly failed, because the result was just the same as with wrap_content
, and then found an article like this with the description of the fillViewport
flag, guessed? And now the question: how to achieve the same result as with the fillViewport
flag, but at the same time dynamically change the height of the child element?LayoutParams.height
course, no more. Problem solved? Not. Height remained unchanged. What happened? After examining onMeasure()
in a child view, I came to the conclusion that ScrollView
simply ignored the set height
in the parameters, sending it onMeasure()
with mode
equal to “ UNSPECIFIED
” and then onMeasure()
with “ EXACTLY
” and the value of height
', equal to the size of the ScrollView
(if fillViewport
set). And since the only way to change the height
view is to change its LayoutParams
, the child doesn't change.ScrollView
so LayoutParams
, you can simply rewrite the onMeasure()
method in the child and add this to the local super.onMeasure()
: if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED && getLayoutParams().height > 0) { heightMeasureSpec = MeasureSpec.makeMeasureSpec( getLayoutParams().height, MeasureSpec.EXACTLY); }
ScrollView
work for it. But it's natural to create a subclass every time just for this - not a good idea. FrameLayout
what class should be a child FrameLayout
: FrameLayout
, LinearLayout
, RelativeLayout
, etc. To do for every possible Layout
'and its subclass - just litter. Therefore, here is your solution number 2.FrameLayout
and resize this FrameLayout
's child.FrameLayout
LayoutParams.height
, but because inside ScrollView.onLayout()
ScrollView.onLayout()
ScrollView
has a problem with the bug tracker , the problem with the unchangeable size of the child, but as is usually the case with Android, it is still in New status from 2014.background
rounded cornersdrawable
with the <shape>
, set the background
'and it's ready ... but no. The task is such that the color changes programmatically, and the rounded edges are added only to the first and last elements.ShapeDrawable
class ShapeDrawable
not have methods for working with rounded corners, as one would expect. But fortunately, there is a successor to RoundRectShape
and PaintDrawable
zoom in
can be of any kind, which means that the view with its background
’will stretch greatly, which leads to ...Logcat: W/OpenGLRenderer: Path too large to be rendered into a texture
background
just ceases to be displayed. As you can see from the warning, a certain Path
too large to be able to draw it into a texture. Having rummaged a bit in the source code, I came to the conclusion that this comrade was to blame: canvas.drawPath(mPath, paint);
mPath
place a rounded rectangle: mPath.addRoundRect(mInnerRect, mInnerRadii, Path.Direction.CCW);
ColorDrawable
and in its draw()
method by calling not drawPath()
, but: canvas.clipPath(roundedPath);
canvas.clipPath()
not affected by antialias . However, in the absence of another way to do this, you have to be content with this.View
inside FrameLayout
UsersView
, within which GroupView could be anywhere along the OY axis.FrameLayout
) was to use MarginLayoutParams
and change the topMargin
parameter. This is also indicated by the majority of responses to stackoverflow ( one , two , three ).LayoutParams
causes a requestLayout()
, and this is an extremely expensive operation, especially if it is called for all GroupView
inside UsersView
, even taking into account that the layout pass itself is postponed until better times (next 16ms-frame) .View.setY()
(the answer to stackoverflow ). Ideal for animations and for children inside unchanging Layout
. It does not call requestLayout()
, but uses only the fields of the view itself and affects only the phase (pass) of the layout. And since the miscalculation of new positions of the views occurs directly in onLayout()
before calling super.onLayout()
, the requestLayout()
is not needed at all.includeFontPadding
TextView
parameter includeFontPadding=false
, the problem disappeared completely.ViewPropertyAnimator
does not have a reverse()
methodview.animate().setDuration(fadeDuration).alpha(0 or 1)
.fade
animations. However, if the fade
is slow, then with a sharp zoom in
after zoom out
, the alpha channel of the view will not be 1 or 0, but, for example, 0.5. Because of this, the animation will play from 0.5 to 0 for the same fadeDuration
. It will look like the animation slowed down 2 times. Adding something like view.setAlpha(0 or 1)
before calling view.animate()
is not a good solution. The view will begin to flicker during fast zoom.setReverseDuration()
(without parameters) that would understand that, “yeah, I played the fade
animation for 500ms, therefore reverse animation will play the same amount”. But this is not, alas. The only way out that I managed to find is to make such handles. In my case, the animation was pretty simple, so it was enough for me to hide it: final float realDuration = iconView.getAlpha() * animationDuration;
... and this to show: final float realDuration = (1 - iconView.getAlpha()) * animationDuration;
view.animate().setDuration((long) realDuration)
- and everything is in openwork.ScaleGestureDetector
(aka "pinch", aka "zoom")ScaleGestureDetector
API ScaleGestureDetector
pretty good - hung up the listener
and wait for yourself events, not forgetting to transfer all onTouchEvent()
's into the detector itself. However, not everything is so rosy.onTouchEvent()
events between the ScaleGestureDetector
and ScrollView
(after all, I remind you, it happens inside the RankingsListView
, which is the heir of the ScrollView
). As a result, the method looks like this: @Override public boolean onTouchEvent(MotionEvent ev) { scaleDetector.onTouchEvent(ev); super.onTouchEvent(ev); return true; }
super.onTouchEvent()
and scaleDetector.onTouchEvent()
... and I was really looking for ... But as it turned out, the solution was terribly simple: @Override public boolean onTouchEvent(MotionEvent ev) { scaleDetector.onTouchEvent(ev); if (ev.getPointerCount() == 1) { super.onTouchEvent(ev); } return true; }
super.onTouchEvent()
does not track the id
finger that was used to scroll for the first time, so even if you started scrolling with finger # 1, and finished with finger # 2 - it is ok with him. Unfortunately, I was so sure that the Android SDK would once again put a stick in the wheels, that I bothered to try this only after: googling and studying the sources. What can I say, the Android SDK can ScrollView
. This child is a LinearLayout
whose children are layout_weight=1
. In other words, they are all the same height ... although no.LinearLayout
has a height of 1001 and it has 2 children, then one of them will be 501 and the other 500. Notice this by sight is almost impossible, but there may be indirect consequences.ViewPropertyAnimator
and reverse()
, I showed an animation of hiding the rank icon. The check itself is simple - we onLayout()
up the height of 2 TextView
and ImageView
in onLayout()
, and if they don’t get inside the current view at once, then hide the fade
ImageView
. It is also worth noting that this total height (“height threshold”, so to speak) does not change. As a result, if the threshold is 500 pixels, then in the described case, for one I twist the size of 500, the icon will hide, while the second, the size of 501, will not.getHeight()
to getHeight()
with a threshold. In onSizeChanged()
inside LinearLayout
find the smallest size of all child elements and notify everyone that they compare the threshold with this number. I called it shared height
and it looks like this for me: private void updateChildsSharedHeight() { int minChildHeight = Integer.MAX_VALUE; for (int i = 0; i < binding.lRankings.getChildCount(); ++i) { minChildHeight = Math.min(minChildHeight, binding.lRankings.getChildAt(i).getMeasuredHeight()); } for (int i = 0; i < binding.lRankings.getChildCount(); ++i) { RankingView child = (RankingView) binding.lRankings.getChildAt(i); child.setSharedHeight(minChildHeight); } }
int requiredHeight = binding.tvScore.getHeight() + binding.tvTitle.getHeight() + binding.ivIcon.getHeight(); boolean shouldHideIcon = requiredHeight > sharedHeight;
ScaleGestureDetector
at the ScaleGestureDetector
, but about its problems.ScaleGestureDetector
simply stopped responding to the touch, sending the event onScaleEnd()
. Information that there is some kind of "minimal pinch" was not present either on the docks or on stackoverflow. Perhaps, I would not even notice this, if the debugging was not done on the emulator. On it, the pinch distance can be even millimeter, which was the reason for searching for information on the subject. However, it turned out to be much closer than I thought, namely, as always, in the source code: mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan);
com.android.internal.R.dimen.config_minScalingSpan
is magical 27mm. For me in general, the existence of a minimal pinch seems to be a very strange phenomenon. Even if there is a sense in this, why not give the opportunity to change it?ScaleGestureDetector
describes it like this: mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
... and ViewConfigeuration
so: mTouchSlop = res.getDimensionPixelSize( com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
detector.getScaleFactor()
at the first pinchonScale(ScaleGestureDetector detector)
is transmitted detector
, from which using the method detector.getScaleFactor()
you can find out the pinch coefficient. So, at the very first pinch, this method returns strange jump-like values. Here are the logs of values when strictly moving zoom out
: It looked, to put it mildly, not very much - the size of the view was constantly twitching, and the animations at these jumps looked like - it was better to omit it altogether. For a long time I tried to understand what the problem was. Checked on a real device (you never know, all of a sudden the emulator problem), moved the mouse as if a cat died somewhere over an extra millimeter of movement (you never know, all of a sudden I’m twitching and from here in the logs) - but no. The answer was not found, but I was lucky. So to say "from the bald," I decided to try to send in the event0.958
0.987
0.970
1.009
0.966
0.967
1.006
. > 1
ScaleGestureDetector
MotionEvent.ACTION_CANCEL
immediately after initialization: scaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); long time = SystemClock.uptimeMillis(); MotionEvent motionEvent = MotionEvent.obtain(time - 100, time, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); scaleDetector.onTouchEvent(motionEvent); motionEvent.recycle();
ScrollView.setScroll()
triggered only after super.onLayout()
?ScrollView
established children height
's. The situation is the following: as soon as the pinch occurs, I would like the current focus (where you clicked the mouse and from which point you do zoom at all) remained the same. Why? Well, just because the zoom looks more user-friendly, consider that you do not just change the height of the child element, but just zoom to some user, while everyone else is moving away from it:ScaleGestureDetector.getFocusY()
with ScrollView.getScrollY()
. Then, it would seem, it is enough to do the ScrollView.setScrollY(newPosition)
thing in the hat ... But no, the view begins to twitch strangely at the zoom to the lowest child:setScroll()
, it checks whether the position of the scroll goes beyond the size of the child element, and if it does, we set the maximum possible position. And since it setScroll()
is called at zoom, then the following sequence of actions is obtained:height
will not change until the next super.onLayout()
, setScroll()
and therefore does not what is expected. Corrected as follows. Instead of setScroll()
doing this: nextScrollPos = newScrollY;
... and onLayout()
after the method we super.onLayout()
call this method: private void updateScrollPosition() { if (nextScrollPos != null) { setScrollY(nextScrollPos.intValue()); nextScrollPos = null; } }
super.onLayout()
. This is still the case. Later I will describe another problem associated with this solution. But the fact remains that this is the solution for the scrolling jump problem.height
for child inside child inside ScrollView
”, then there will be no such problem. But then we come back to the problem of a dozen subclasses.minScore
one also changes at zoom, because it is calculated by the formula (simplified version):userViewHeight
unchanged, but containerHeight
changes, then at different points of the pinch, the glasses at the boundary twist will be different.view.getY()
, float
, , , MarginLayoutParams.topMargin
, , ).zoom in
zoom out
zoom in
- .Group
, because in fact, a single user is just a group of one person. Also, using the word “user” I can mean both one user before joining a group, and a group of users.1. , i i+1.
2.1. – №1.
2.2. , .
3. 1 .
for (int i = 0; i < groups.size() - 1; ) { if (isInersected(groups[i], groups[i+1])) { groups[i].addUsersToGroup(groups[i+1]); groups.remove(i+1); } else { ++i; } }
0. i = 0.
1. , i i+1.
2.1. – , . 1.
2.2. , 2.
3. , 1.
4. i == 1 , .
5. 0, i ( 0 1 1 0 – ).
bool didIntersected, isEvenPass = false; do { didIntersected = false; isEvenPass = !isEvenPass; int i = isEvenPass ? 0 : 1; while (i < (groups.size() - 1)) { if (isInersected(groups[i], groups[i+1)) { didIntersected = true; groups[i].addUsersToGroup(groups[i+1]); groups.remove(i+1); i += 1; } else { i += 2; } } } while(isEvenPass || didIntersected);
Stack<Group> groupsStack;
and add each new group there. Thus, when disconnecting, only the topmost element on the stack will need to be checked.0. ( groups.size() – 1, / ).
1. . 0?
YES.1. 2- .
YES.2. . , 2- , .
YES.3. .
YES.4. 1.
NO.1. .
distances.sortAsc(); while (!distances.isEmpty() && distances.getFirst().distance <= 0) { firstDistance = distances.getFirst(); mergedGroup = new Group(firstDistance.groupLeft, firstDistance.groupRight); groupsStack.push(mergedGroup); distances.removeFirst(); firstDistance.groupLeft.notifyPositionChanged(mergedGroup.position); firstDistance.groupRight.notifyPositionChanged(mergedGroup.position); distances.sortAsc(); }
sortAsc()
did not know what to do, because the distance between users 1 and 2 is 0, but also between users 2 and 3 it is also 0. As a result, the groups merged in the wrong order. This will be especially noticeable when disconnected. The first 12 will be separated, because they are the last to unite (the stack is LIFO . Do you all remember?), Although they are closer to each other.sortAsc()
« 0». , . , , distance == 0
2- , group.scoreAfterMerge
. , GroupsDistance, : @Override public int compareTo(@NonNull GroupsDistance obj) { GroupsDistance a = this, b = obj; final int distancesComparison = MathUtil.Compare(b.distance, a.distance); if (distancesComparison != 0) { return distancesComparison; } else { final int namesComparison = a.leftGroup.name.compareTo(b.leftGroup.name); return namesComparison; } }
group.scoreAfterMerge
': @Override public int compareTo(@NonNull GroupsDistance obj) { GroupsDistance a = this, b = obj; final int distancesComparison = MathUtil.Compare(b.distance, a.distance); if (distancesComparison != 0) { return distancesComparison; } else { final int scoresComparison = MathUtil.Compare(a.scoreAfterMerge, b.scoreAfterMerge); if (scoresComparison != 0) { return scoresComparison; } else { final int namesComparison = a.leftGroup.name.compareTo(b.leftGroup.name) return namesComparison; } } }
user.score
they group.scoreAfterMerge
will always be unequal (and if they are equal, then there is no difference in the order in which to unite them — they will never get separated anyway). This means that add. comparisons score
should be enough to solve the problem ... but no, and in this case there is a flaw.distance
it would always change proportionally between users, and the position of the user view would uniquely display it score
, but this is not so. Because of this condition, at the boundary view, the dependence of the position on is violated score
. This is not obvious enough, so here is a detailed example of such a “violation”:№1 60%: [600, 700].
№2 79%: [790, 890].
№3 100%: [900, 1000] ( : [1000, 1100]).
№1 60%: [300, 400].
№2 79%: [395, 495].
№3 100%: [400, 500] ( : [500, 600]).
№1 60%: [100, 200] ( : [120, 220]).
№2 79%: [100, 200] ( : [158, 258]).
№3 100%: [100, 200] ( : [200, 300]).
score
?!» — .height
, 2 ! willIntersectWhenHeight
' , . – , ( border
bound
) / , . .willIntersectWhenHeight
' GroupCandidates
. , , , . , . private float getIntersectingHeght() { float intersectingHeight; final Float height = calcIntersectingHeightWithNoBound(); final boolean leftIsBorder = leftGroup.isLeftBorderWhen(height), rightIsBorder = rightGroup.isRightBorderWhen(height); if (!leftIsBorder && !rightIsBorder) { intersectingHeight = space; return; } if (leftIsBorder && !rightIsBorder) { intersectingHeight = (itemHeight + itemHalfHeight) / right.getNormalizedPos(); return; } if (!leftIsBorder && rightIsBorder) { intersectingHeight = (itemHeight + itemHalfHeight) / (1 - left.getNormalizedPos()); return; } if (leftIsBorder && rightIsBorder) { intersectingHeight = ((float) (itemHeight + itemHeight)); return; } return intersectingHeight; } private Float calcIntersectingHeightWithNoBound() { return itemHeight / (right.getNormalizedPos() - left.getNormalizedPos()); }
: getNormalizedPos()
(.., 0 1) .ScrollView.setScroll()
ScrollView.onLayout()
super.onLayout()
( child' child' ScrollView
)ScrollView
setScroll()
super.onLayout()
. Why is it important? requestLayout()
. « layout pass» 16-. , requestLayout()
, , super.onLayout()
( ), layout pass .requestLayout()
super.onLayout()
, . , ( 16), layout pass, ScrollView.onLayout()
, requestLayout()
super.onLayout()
… , 16- Layout
' . , . requestLayout()
super.onLayout()
, ScrollView.setScroll()
requestLayout()
, ? :View.setVisibility(GONE)
requestLayout()
View.getLocalVisibleRect()
( ) UsersView
, groupView.setUsersGroup(group)
, groupsCountView.setVisiblity(…)
.ScrollView.setScroll()
, View.getLocalVisibleRect()
. ScrollView.setScroll()
super.onLayout()
, groupView.setUsersGroup(group)
View.setVisiblity(GONE)
, requestLayout()
, , .View.getLocalVisibleRect()
, View.setVisiblity(GONE)
, requestLayout()
. , requestLayout()
– !requestLayout()
GroupView
, ( GroupView
«»… . ), « requestLayout()
» . «», . , , stackoverflow .requestLayout()
, ( ). , , RecyclerView
LayoutManager
.RecyclerView
, . , requestLayout()
, (, child.setVisibility(GONE)
), … child
' Layout
! : layout.addView(myChild)
.View
' child
';measure()
, layout()
drawChild()
child
' ;child
' , requestLayout()
.View.getLocalVisibleRect()
/ //.onScrollChanged()
onLayout()
, setScrollY()
? , View.getLocalVisibleRect()
false
(, UsersView
). , , :/ , recycling
' . 1.000 . - . NDK.4.999.
«» .Source: https://habr.com/ru/post/321890/
All Articles