result = new ArrayList<>();
346 | for (TreeNode n : parent.getChildren()) {
347 | if (n.isSelected()) {
348 | result.add(n);
349 | }
350 | result.addAll(getSelected(n));
351 | }
352 | return result;
353 | }
354 |
355 | public void selectAll(boolean skipCollapsed) {
356 | makeAllSelection(true, skipCollapsed);
357 | }
358 |
359 | public void deselectAll() {
360 | makeAllSelection(false, false);
361 | }
362 |
363 | private void makeAllSelection(boolean selected, boolean skipCollapsed) {
364 | if (mSelectionModeEnabled) {
365 | for (TreeNode node : mRoot.getChildren()) {
366 | selectNode(node, selected, skipCollapsed);
367 | }
368 | }
369 | }
370 |
371 | public void selectNode(TreeNode node, boolean selected) {
372 | if (mSelectionModeEnabled) {
373 | node.setSelected(selected);
374 | toogleSelectionForNode(node, true);
375 | }
376 | }
377 |
378 | private void selectNode(TreeNode parent, boolean selected, boolean skipCollapsed) {
379 | parent.setSelected(selected);
380 | toogleSelectionForNode(parent, true);
381 | boolean toContinue = skipCollapsed ? parent.isExpanded() : true;
382 | if (toContinue) {
383 | for (TreeNode node : parent.getChildren()) {
384 | selectNode(node, selected, skipCollapsed);
385 | }
386 | }
387 | }
388 |
389 | private void toogleSelectionForNode(TreeNode node, boolean makeSelectable) {
390 | TreeNode.BaseNodeViewHolder holder = getViewHolderForNode(node);
391 | if (holder.isInitialized()) {
392 | getViewHolderForNode(node).toggleSelectionMode(makeSelectable);
393 | }
394 | }
395 |
396 | private TreeNode.BaseNodeViewHolder getViewHolderForNode(TreeNode node) {
397 | TreeNode.BaseNodeViewHolder viewHolder = node.getViewHolder();
398 | if (viewHolder == null) {
399 | try {
400 | final Object object = defaultViewHolderClass.getConstructor(Context.class).newInstance(mContext);
401 | viewHolder = (TreeNode.BaseNodeViewHolder) object;
402 | node.setViewHolder(viewHolder);
403 | } catch (Exception e) {
404 | throw new RuntimeException("Could not instantiate class " + defaultViewHolderClass);
405 | }
406 | }
407 | if (viewHolder.getContainerStyle() <= 0) {
408 | viewHolder.setContainerStyle(containerStyle);
409 | }
410 | if (viewHolder.getTreeView() == null) {
411 | viewHolder.setTreeViev(this);
412 | }
413 | return viewHolder;
414 | }
415 |
416 | private static void expand(final View v) {
417 | v.measure(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
418 | final int targetHeight = v.getMeasuredHeight();
419 |
420 | v.getLayoutParams().height = 0;
421 | v.setVisibility(View.VISIBLE);
422 | Animation a = new Animation() {
423 | @Override
424 | protected void applyTransformation(float interpolatedTime, Transformation t) {
425 | v.getLayoutParams().height = interpolatedTime == 1
426 | ? LinearLayout.LayoutParams.WRAP_CONTENT
427 | : (int) (targetHeight * interpolatedTime);
428 | v.requestLayout();
429 | }
430 |
431 | @Override
432 | public boolean willChangeBounds() {
433 | return true;
434 | }
435 | };
436 |
437 | // 1dp/ms
438 | a.setDuration((int) (targetHeight / v.getContext().getResources().getDisplayMetrics().density));
439 | v.startAnimation(a);
440 | }
441 |
442 | private static void collapse(final View v) {
443 | final int initialHeight = v.getMeasuredHeight();
444 |
445 | Animation a = new Animation() {
446 | @Override
447 | protected void applyTransformation(float interpolatedTime, Transformation t) {
448 | if (interpolatedTime == 1) {
449 | v.setVisibility(View.GONE);
450 | } else {
451 | v.getLayoutParams().height = initialHeight - (int) (initialHeight * interpolatedTime);
452 | v.requestLayout();
453 | }
454 | }
455 |
456 | @Override
457 | public boolean willChangeBounds() {
458 | return true;
459 | }
460 | };
461 |
462 | // 1dp/ms
463 | a.setDuration((int) (initialHeight / v.getContext().getResources().getDisplayMetrics().density));
464 | v.startAnimation(a);
465 | }
466 |
467 | //-----------------------------------------------------------------
468 | //Add / Remove
469 |
470 | public void addNode(TreeNode parent, final TreeNode nodeToAdd) {
471 | parent.addChild(nodeToAdd);
472 | if (parent.isExpanded()) {
473 | final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(parent);
474 | addNode(parentViewHolder.getNodeItemsView(), nodeToAdd);
475 | }
476 | }
477 |
478 | public void removeNode(TreeNode node) {
479 | if (node.getParent() != null) {
480 | TreeNode parent = node.getParent();
481 | int index = parent.deleteChild(node);
482 | if (parent.isExpanded() && index >= 0) {
483 | final TreeNode.BaseNodeViewHolder parentViewHolder = getViewHolderForNode(parent);
484 | parentViewHolder.getNodeItemsView().removeViewAt(index);
485 | }
486 | }
487 | }
488 | }
489 |
--------------------------------------------------------------------------------
/library/src/main/java/com/unnamed/b/atv/view/TreeNodeWrapperView.java:
--------------------------------------------------------------------------------
1 | package com.unnamed.b.atv.view;
2 |
3 | import android.content.Context;
4 | import android.view.ContextThemeWrapper;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 | import android.widget.LinearLayout;
8 | import android.widget.RelativeLayout;
9 |
10 | import com.unnamed.b.atv.R;
11 |
12 | /**
13 | * Created by Bogdan Melnychuk on 2/10/15.
14 | */
15 | public class TreeNodeWrapperView extends LinearLayout {
16 | private LinearLayout nodeItemsContainer;
17 | private ViewGroup nodeContainer;
18 | private final int containerStyle;
19 |
20 | public TreeNodeWrapperView(Context context, int containerStyle) {
21 | super(context);
22 | this.containerStyle = containerStyle;
23 | init();
24 | }
25 |
26 | private void init() {
27 | setOrientation(LinearLayout.VERTICAL);
28 |
29 | nodeContainer = new RelativeLayout(getContext());
30 | nodeContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
31 | nodeContainer.setId(R.id.node_header);
32 |
33 | ContextThemeWrapper newContext = new ContextThemeWrapper(getContext(), containerStyle);
34 | nodeItemsContainer = new LinearLayout(newContext, null, containerStyle);
35 | nodeItemsContainer.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
36 | nodeItemsContainer.setId(R.id.node_items);
37 | nodeItemsContainer.setOrientation(LinearLayout.VERTICAL);
38 | nodeItemsContainer.setVisibility(View.GONE);
39 |
40 | addView(nodeContainer);
41 | addView(nodeItemsContainer);
42 | }
43 |
44 |
45 | public void insertNodeView(View nodeView) {
46 | nodeContainer.addView(nodeView);
47 | }
48 |
49 | public ViewGroup getNodeContainer() {
50 | return nodeContainer;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/library/src/main/java/com/unnamed/b/atv/view/TwoDScrollView.java:
--------------------------------------------------------------------------------
1 | package com.unnamed.b.atv.view;
2 |
3 |
4 | import android.content.Context;
5 | import android.graphics.Rect;
6 | import android.util.AttributeSet;
7 | import android.view.FocusFinder;
8 | import android.view.KeyEvent;
9 | import android.view.MotionEvent;
10 | import android.view.VelocityTracker;
11 | import android.view.View;
12 | import android.view.ViewConfiguration;
13 | import android.view.ViewGroup;
14 | import android.view.ViewParent;
15 | import android.view.animation.AnimationUtils;
16 | import android.widget.FrameLayout;
17 | import android.widget.LinearLayout;
18 | import android.widget.Scroller;
19 | import android.widget.TextView;
20 |
21 | import java.util.List;
22 |
23 | /**
24 | * Layout container for a view hierarchy that can be scrolled by the user,
25 | * allowing it to be larger than the physical display. A TwoDScrollView
26 | * is a {@link FrameLayout}, meaning you should place one child in it
27 | * containing the entire contents to scroll; this child may itself be a layout
28 | * manager with a complex hierarchy of objects. A child that is often used
29 | * is a {@link LinearLayout} in a vertical orientation, presenting a vertical
30 | * array of top-level items that the user can scroll through.
31 | *
32 | * The {@link TextView} class also
33 | * takes care of its own scrolling, so does not require a TwoDScrollView, but
34 | * using the two together is possible to achieve the effect of a text view
35 | * within a larger container.
36 | */
37 | public class TwoDScrollView extends FrameLayout {
38 | static final int ANIMATED_SCROLL_GAP = 250;
39 | static final float MAX_SCROLL_FACTOR = 0.5f;
40 |
41 | private long mLastScroll;
42 |
43 | private final Rect mTempRect = new Rect();
44 | private Scroller mScroller;
45 |
46 | /**
47 | * Flag to indicate that we are moving focus ourselves. This is so the
48 | * code that watches for focus changes initiated outside this TwoDScrollView
49 | * knows that it does not have to do anything.
50 | */
51 | private boolean mTwoDScrollViewMovedFocus;
52 |
53 | /**
54 | * Position of the last motion event.
55 | */
56 | private float mLastMotionY;
57 | private float mLastMotionX;
58 |
59 | /**
60 | * True when the layout has changed but the traversal has not come through yet.
61 | * Ideally the view hierarchy would keep track of this for us.
62 | */
63 | private boolean mIsLayoutDirty = true;
64 |
65 | /**
66 | * The child to give focus to in the event that a child has requested focus while the
67 | * layout is dirty. This prevents the scroll from being wrong if the child has not been
68 | * laid out before requesting focus.
69 | */
70 | private View mChildToScrollTo = null;
71 |
72 | /**
73 | * True if the user is currently dragging this TwoDScrollView around. This is
74 | * not the same as 'is being flinged', which can be checked by
75 | * mScroller.isFinished() (flinging begins when the user lifts his finger).
76 | */
77 | private boolean mIsBeingDragged = false;
78 |
79 | /**
80 | * Determines speed during touch scrolling
81 | */
82 | private VelocityTracker mVelocityTracker;
83 |
84 | /**
85 | * Whether arrow scrolling is animated.
86 | */
87 | private int mTouchSlop;
88 | private int mMinimumVelocity;
89 | private int mMaximumVelocity;
90 |
91 | public TwoDScrollView(Context context) {
92 | super(context);
93 | initTwoDScrollView();
94 | }
95 |
96 | public TwoDScrollView(Context context, AttributeSet attrs) {
97 | super(context, attrs);
98 | initTwoDScrollView();
99 | }
100 |
101 | public TwoDScrollView(Context context, AttributeSet attrs, int defStyle) {
102 | super(context, attrs, defStyle);
103 | initTwoDScrollView();
104 | }
105 |
106 | @Override
107 | protected float getTopFadingEdgeStrength() {
108 | if (getChildCount() == 0) {
109 | return 0.0f;
110 | }
111 | final int length = getVerticalFadingEdgeLength();
112 | if (getScrollY() < length) {
113 | return getScrollY() / (float) length;
114 | }
115 | return 1.0f;
116 | }
117 |
118 | @Override
119 | protected float getBottomFadingEdgeStrength() {
120 | if (getChildCount() == 0) {
121 | return 0.0f;
122 | }
123 | final int length = getVerticalFadingEdgeLength();
124 | final int bottomEdge = getHeight() - getPaddingBottom();
125 | final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
126 | if (span < length) {
127 | return span / (float) length;
128 | }
129 | return 1.0f;
130 | }
131 |
132 | @Override
133 | protected float getLeftFadingEdgeStrength() {
134 | if (getChildCount() == 0) {
135 | return 0.0f;
136 | }
137 | final int length = getHorizontalFadingEdgeLength();
138 | if (getScrollX() < length) {
139 | return getScrollX() / (float) length;
140 | }
141 | return 1.0f;
142 | }
143 |
144 | @Override
145 | protected float getRightFadingEdgeStrength() {
146 | if (getChildCount() == 0) {
147 | return 0.0f;
148 | }
149 | final int length = getHorizontalFadingEdgeLength();
150 | final int rightEdge = getWidth() - getPaddingRight();
151 | final int span = getChildAt(0).getRight() - getScrollX() - rightEdge;
152 | if (span < length) {
153 | return span / (float) length;
154 | }
155 | return 1.0f;
156 | }
157 |
158 | /**
159 | * @return The maximum amount this scroll view will scroll in response to
160 | * an arrow event.
161 | */
162 | public int getMaxScrollAmountVertical() {
163 | return (int) (MAX_SCROLL_FACTOR * getHeight());
164 | }
165 |
166 | public int getMaxScrollAmountHorizontal() {
167 | return (int) (MAX_SCROLL_FACTOR * getWidth());
168 | }
169 |
170 | private void initTwoDScrollView() {
171 | mScroller = new Scroller(getContext());
172 | setFocusable(true);
173 | setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
174 | setWillNotDraw(false);
175 | final ViewConfiguration configuration = ViewConfiguration.get(getContext());
176 | mTouchSlop = configuration.getScaledTouchSlop();
177 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
178 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
179 | }
180 |
181 | @Override
182 | public void addView(View child) {
183 | if (getChildCount() > 0) {
184 | throw new IllegalStateException("TwoDScrollView can host only one direct child");
185 | }
186 | super.addView(child);
187 | }
188 |
189 | @Override
190 | public void addView(View child, int index) {
191 | if (getChildCount() > 0) {
192 | throw new IllegalStateException("TwoDScrollView can host only one direct child");
193 | }
194 | super.addView(child, index);
195 | }
196 |
197 | @Override
198 | public void addView(View child, ViewGroup.LayoutParams params) {
199 | if (getChildCount() > 0) {
200 | throw new IllegalStateException("TwoDScrollView can host only one direct child");
201 | }
202 | super.addView(child, params);
203 | }
204 |
205 | @Override
206 | public void addView(View child, int index, ViewGroup.LayoutParams params) {
207 | if (getChildCount() > 0) {
208 | throw new IllegalStateException("TwoDScrollView can host only one direct child");
209 | }
210 | super.addView(child, index, params);
211 | }
212 |
213 | /**
214 | * @return Returns true this TwoDScrollView can be scrolled
215 | */
216 | private boolean canScroll() {
217 | View child = getChildAt(0);
218 | if (child != null) {
219 | int childHeight = child.getHeight();
220 | int childWidth = child.getWidth();
221 | return (getHeight() < childHeight + getPaddingTop() + getPaddingBottom()) ||
222 | (getWidth() < childWidth + getPaddingLeft() + getPaddingRight());
223 | }
224 | return false;
225 | }
226 |
227 | @Override
228 | public boolean dispatchKeyEvent(KeyEvent event) {
229 | // Let the focused view and/or our descendants get the key first
230 | boolean handled = super.dispatchKeyEvent(event);
231 | if (handled) {
232 | return true;
233 | }
234 | return executeKeyEvent(event);
235 | }
236 |
237 | /**
238 | * You can call this function yourself to have the scroll view perform
239 | * scrolling from a key event, just as if the event had been dispatched to
240 | * it by the view hierarchy.
241 | *
242 | * @param event The key event to execute.
243 | * @return Return true if the event was handled, else false.
244 | */
245 | public boolean executeKeyEvent(KeyEvent event) {
246 | mTempRect.setEmpty();
247 | if (!canScroll()) {
248 | if (isFocused()) {
249 | View currentFocused = findFocus();
250 | if (currentFocused == this) currentFocused = null;
251 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN);
252 | return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN);
253 | }
254 | return false;
255 | }
256 | boolean handled = false;
257 | if (event.getAction() == KeyEvent.ACTION_DOWN) {
258 | switch (event.getKeyCode()) {
259 | case KeyEvent.KEYCODE_DPAD_UP:
260 | if (!event.isAltPressed()) {
261 | handled = arrowScroll(View.FOCUS_UP, false);
262 | } else {
263 | handled = fullScroll(View.FOCUS_UP, false);
264 | }
265 | break;
266 | case KeyEvent.KEYCODE_DPAD_DOWN:
267 | if (!event.isAltPressed()) {
268 | handled = arrowScroll(View.FOCUS_DOWN, false);
269 | } else {
270 | handled = fullScroll(View.FOCUS_DOWN, false);
271 | }
272 | break;
273 | case KeyEvent.KEYCODE_DPAD_LEFT:
274 | if (!event.isAltPressed()) {
275 | handled = arrowScroll(View.FOCUS_LEFT, true);
276 | } else {
277 | handled = fullScroll(View.FOCUS_LEFT, true);
278 | }
279 | break;
280 | case KeyEvent.KEYCODE_DPAD_RIGHT:
281 | if (!event.isAltPressed()) {
282 | handled = arrowScroll(View.FOCUS_RIGHT, true);
283 | } else {
284 | handled = fullScroll(View.FOCUS_RIGHT, true);
285 | }
286 | break;
287 | }
288 | }
289 | return handled;
290 | }
291 |
292 | @Override
293 | public boolean onInterceptTouchEvent(MotionEvent ev) {
294 | /*
295 | * This method JUST determines whether we want to intercept the motion.
296 | * If we return true, onMotionEvent will be called and we do the actual
297 | * scrolling there.
298 | *
299 | * Shortcut the most recurring case: the user is in the dragging
300 | * state and he is moving his finger. We want to intercept this
301 | * motion.
302 | */
303 | final int action = ev.getAction();
304 | if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
305 | return true;
306 | }
307 | if (!canScroll()) {
308 | mIsBeingDragged = false;
309 | return false;
310 | }
311 | final float y = ev.getY();
312 | final float x = ev.getX();
313 | switch (action) {
314 | case MotionEvent.ACTION_MOVE:
315 | /*
316 | * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
317 | * whether the user has moved far enough from his original down touch.
318 | */
319 | /*
320 | * Locally do absolute value. mLastMotionY is set to the y value
321 | * of the down event.
322 | */
323 | final int yDiff = (int) Math.abs(y - mLastMotionY);
324 | final int xDiff = (int) Math.abs(x - mLastMotionX);
325 | if (yDiff > mTouchSlop || xDiff > mTouchSlop) {
326 | mIsBeingDragged = true;
327 | }
328 | break;
329 |
330 | case MotionEvent.ACTION_DOWN:
331 | /* Remember location of down touch */
332 | mLastMotionY = y;
333 | mLastMotionX = x;
334 |
335 | /*
336 | * If being flinged and user touches the screen, initiate drag;
337 | * otherwise don't. mScroller.isFinished should be false when
338 | * being flinged.
339 | */
340 | mIsBeingDragged = !mScroller.isFinished();
341 | break;
342 |
343 | case MotionEvent.ACTION_CANCEL:
344 | case MotionEvent.ACTION_UP:
345 | /* Release the drag */
346 | mIsBeingDragged = false;
347 | break;
348 | }
349 |
350 | /*
351 | * The only time we want to intercept motion events is if we are in the
352 | * drag mode.
353 | */
354 | return mIsBeingDragged;
355 | }
356 |
357 | @Override
358 | public boolean onTouchEvent(MotionEvent ev) {
359 |
360 | if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
361 | // Don't handle edge touches immediately -- they may actually belong to one of our
362 | // descendants.
363 | return false;
364 | }
365 |
366 | if (!canScroll()) {
367 | return false;
368 | }
369 |
370 | if (mVelocityTracker == null) {
371 | mVelocityTracker = VelocityTracker.obtain();
372 | }
373 | mVelocityTracker.addMovement(ev);
374 |
375 | final int action = ev.getAction();
376 | final float y = ev.getY();
377 | final float x = ev.getX();
378 |
379 | switch (action) {
380 | case MotionEvent.ACTION_DOWN:
381 | /*
382 | * If being flinged and user touches, stop the fling. isFinished
383 | * will be false if being flinged.
384 | */
385 | if (!mScroller.isFinished()) {
386 | mScroller.abortAnimation();
387 | }
388 |
389 | // Remember where the motion event started
390 | mLastMotionY = y;
391 | mLastMotionX = x;
392 | break;
393 | case MotionEvent.ACTION_MOVE:
394 | // Scroll to follow the motion event
395 | int deltaX = (int) (mLastMotionX - x);
396 | int deltaY = (int) (mLastMotionY - y);
397 | mLastMotionX = x;
398 | mLastMotionY = y;
399 |
400 | if (deltaX < 0) {
401 | if (getScrollX() < 0) {
402 | deltaX = 0;
403 | }
404 | } else if (deltaX > 0) {
405 | final int rightEdge = getWidth() - getPaddingRight();
406 | final int availableToScroll = getChildAt(0).getRight() - getScrollX() - rightEdge;
407 | if (availableToScroll > 0) {
408 | deltaX = Math.min(availableToScroll, deltaX);
409 | } else {
410 | deltaX = 0;
411 | }
412 | }
413 | if (deltaY < 0) {
414 | if (getScrollY() < 0) {
415 | deltaY = 0;
416 | }
417 | } else if (deltaY > 0) {
418 | final int bottomEdge = getHeight() - getPaddingBottom();
419 | final int availableToScroll = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
420 | if (availableToScroll > 0) {
421 | deltaY = Math.min(availableToScroll, deltaY);
422 | } else {
423 | deltaY = 0;
424 | }
425 | }
426 | if (deltaY != 0 || deltaX != 0)
427 | scrollBy(deltaX, deltaY);
428 | break;
429 | case MotionEvent.ACTION_UP:
430 | final VelocityTracker velocityTracker = mVelocityTracker;
431 | velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
432 | int initialXVelocity = (int) velocityTracker.getXVelocity();
433 | int initialYVelocity = (int) velocityTracker.getYVelocity();
434 | if ((Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity) && getChildCount() > 0) {
435 | fling(-initialXVelocity, -initialYVelocity);
436 | }
437 | if (mVelocityTracker != null) {
438 | mVelocityTracker.recycle();
439 | mVelocityTracker = null;
440 | }
441 | }
442 | return true;
443 | }
444 |
445 | /**
446 | * Finds the next focusable component that fits in this View's bounds
447 | * (excluding fading edges) pretending that this View's top is located at
448 | * the parameter top.
449 | *
450 | * @param topFocus look for a candidate is the one at the top of the bounds
451 | * if topFocus is true, or at the bottom of the bounds if topFocus is
452 | * false
453 | * @param top the top offset of the bounds in which a focusable must be
454 | * found (the fading edge is assumed to start at this position)
455 | * @param preferredFocusable the View that has highest priority and will be
456 | * returned if it is within my bounds (null is valid)
457 | * @return the next focusable component in the bounds or null if none can be
458 | * found
459 | */
460 | private View findFocusableViewInMyBounds(final boolean topFocus, final int top, final boolean leftFocus, final int left, View preferredFocusable) {
461 | /*
462 | * The fading edge's transparent side should be considered for focus
463 | * since it's mostly visible, so we divide the actual fading edge length
464 | * by 2.
465 | */
466 | final int verticalFadingEdgeLength = getVerticalFadingEdgeLength() / 2;
467 | final int topWithoutFadingEdge = top + verticalFadingEdgeLength;
468 | final int bottomWithoutFadingEdge = top + getHeight() - verticalFadingEdgeLength;
469 | final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
470 | final int leftWithoutFadingEdge = left + horizontalFadingEdgeLength;
471 | final int rightWithoutFadingEdge = left + getWidth() - horizontalFadingEdgeLength;
472 |
473 | if ((preferredFocusable != null)
474 | && (preferredFocusable.getTop() < bottomWithoutFadingEdge)
475 | && (preferredFocusable.getBottom() > topWithoutFadingEdge)
476 | && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
477 | && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
478 | return preferredFocusable;
479 | }
480 | return findFocusableViewInBounds(topFocus, topWithoutFadingEdge, bottomWithoutFadingEdge, leftFocus, leftWithoutFadingEdge, rightWithoutFadingEdge);
481 | }
482 |
483 | /**
484 | * Finds the next focusable component that fits in the specified bounds.
485 | *
486 | *
487 | * @param topFocus look for a candidate is the one at the top of the bounds
488 | * if topFocus is true, or at the bottom of the bounds if topFocus is
489 | * false
490 | * @param top the top offset of the bounds in which a focusable must be
491 | * found
492 | * @param bottom the bottom offset of the bounds in which a focusable must
493 | * be found
494 | * @return the next focusable component in the bounds or null if none can
495 | * be found
496 | */
497 | private View findFocusableViewInBounds(boolean topFocus, int top, int bottom, boolean leftFocus, int left, int right) {
498 | List focusables = getFocusables(View.FOCUS_FORWARD);
499 | View focusCandidate = null;
500 |
501 | /*
502 | * A fully contained focusable is one where its top is below the bound's
503 | * top, and its bottom is above the bound's bottom. A partially
504 | * contained focusable is one where some part of it is within the
505 | * bounds, but it also has some part that is not within bounds. A fully contained
506 | * focusable is preferred to a partially contained focusable.
507 | */
508 | boolean foundFullyContainedFocusable = false;
509 |
510 | int count = focusables.size();
511 | for (int i = 0; i < count; i++) {
512 | View view = focusables.get(i);
513 | int viewTop = view.getTop();
514 | int viewBottom = view.getBottom();
515 | int viewLeft = view.getLeft();
516 | int viewRight = view.getRight();
517 |
518 | if (top < viewBottom && viewTop < bottom && left < viewRight && viewLeft < right) {
519 | /*
520 | * the focusable is in the target area, it is a candidate for
521 | * focusing
522 | */
523 | final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom) && (left < viewLeft) && (viewRight < right);
524 | if (focusCandidate == null) {
525 | /* No candidate, take this one */
526 | focusCandidate = view;
527 | foundFullyContainedFocusable = viewIsFullyContained;
528 | } else {
529 | final boolean viewIsCloserToVerticalBoundary =
530 | (topFocus && viewTop < focusCandidate.getTop()) ||
531 | (!topFocus && viewBottom > focusCandidate.getBottom());
532 | final boolean viewIsCloserToHorizontalBoundary =
533 | (leftFocus && viewLeft < focusCandidate.getLeft()) ||
534 | (!leftFocus && viewRight > focusCandidate.getRight());
535 | if (foundFullyContainedFocusable) {
536 | if (viewIsFullyContained && viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) {
537 | /*
538 | * We're dealing with only fully contained views, so
539 | * it has to be closer to the boundary to beat our
540 | * candidate
541 | */
542 | focusCandidate = view;
543 | }
544 | } else {
545 | if (viewIsFullyContained) {
546 | /* Any fully contained view beats a partially contained view */
547 | focusCandidate = view;
548 | foundFullyContainedFocusable = true;
549 | } else if (viewIsCloserToVerticalBoundary && viewIsCloserToHorizontalBoundary) {
550 | /*
551 | * Partially contained view beats another partially
552 | * contained view if it's closer
553 | */
554 | focusCandidate = view;
555 | }
556 | }
557 | }
558 | }
559 | }
560 | return focusCandidate;
561 | }
562 |
563 | /**
564 | * Handles scrolling in response to a "home/end" shortcut press. This
565 | * method will scroll the view to the top or bottom and give the focus
566 | * to the topmost/bottommost component in the new visible area. If no
567 | * component is a good candidate for focus, this scrollview reclaims the
568 | * focus.
569 | *
570 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
571 | * to go the top of the view or
572 | * {@link android.view.View#FOCUS_DOWN} to go the bottom
573 | * @return true if the key event is consumed by this method, false otherwise
574 | */
575 | public boolean fullScroll(int direction, boolean horizontal) {
576 | if (!horizontal) {
577 | boolean down = direction == View.FOCUS_DOWN;
578 | int height = getHeight();
579 | mTempRect.top = 0;
580 | mTempRect.bottom = height;
581 | if (down) {
582 | int count = getChildCount();
583 | if (count > 0) {
584 | View view = getChildAt(count - 1);
585 | mTempRect.bottom = view.getBottom();
586 | mTempRect.top = mTempRect.bottom - height;
587 | }
588 | }
589 | return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom, 0, 0, 0);
590 | } else {
591 | boolean right = direction == View.FOCUS_DOWN;
592 | int width = getWidth();
593 | mTempRect.left = 0;
594 | mTempRect.right = width;
595 | if (right) {
596 | int count = getChildCount();
597 | if (count > 0) {
598 | View view = getChildAt(count - 1);
599 | mTempRect.right = view.getBottom();
600 | mTempRect.left = mTempRect.right - width;
601 | }
602 | }
603 | return scrollAndFocus(0, 0, 0, direction, mTempRect.top, mTempRect.bottom);
604 | }
605 | }
606 |
607 | /**
608 | * Scrolls the view to make the area defined by top
and
609 | * bottom
visible. This method attempts to give the focus
610 | * to a component visible in this area. If no component can be focused in
611 | * the new visible area, the focus is reclaimed by this scrollview.
612 | *
613 | * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
614 | * to go upward
615 | * {@link android.view.View#FOCUS_DOWN} to downward
616 | * @param top the top offset of the new area to be made visible
617 | * @param bottom the bottom offset of the new area to be made visible
618 | * @return true if the key event is consumed by this method, false otherwise
619 | */
620 | private boolean scrollAndFocus(int directionY, int top, int bottom, int directionX, int left, int right) {
621 | boolean handled = true;
622 | int height = getHeight();
623 | int containerTop = getScrollY();
624 | int containerBottom = containerTop + height;
625 | boolean up = directionY == View.FOCUS_UP;
626 | int width = getWidth();
627 | int containerLeft = getScrollX();
628 | int containerRight = containerLeft + width;
629 | boolean leftwards = directionX == View.FOCUS_UP;
630 | View newFocused = findFocusableViewInBounds(up, top, bottom, leftwards, left, right);
631 | if (newFocused == null) {
632 | newFocused = this;
633 | }
634 | if ((top >= containerTop && bottom <= containerBottom) || (left >= containerLeft && right <= containerRight)) {
635 | handled = false;
636 | } else {
637 | int deltaY = up ? (top - containerTop) : (bottom - containerBottom);
638 | int deltaX = leftwards ? (left - containerLeft) : (right - containerRight);
639 | doScroll(deltaX, deltaY);
640 | }
641 | if (newFocused != findFocus() && newFocused.requestFocus(directionY)) {
642 | mTwoDScrollViewMovedFocus = true;
643 | mTwoDScrollViewMovedFocus = false;
644 | }
645 | return handled;
646 | }
647 |
648 | /**
649 | * Handle scrolling in response to an up or down arrow click.
650 | *
651 | * @param direction The direction corresponding to the arrow key that was
652 | * pressed
653 | * @return True if we consumed the event, false otherwise
654 | */
655 | public boolean arrowScroll(int direction, boolean horizontal) {
656 | View currentFocused = findFocus();
657 | if (currentFocused == this) currentFocused = null;
658 | View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
659 | final int maxJump = horizontal ? getMaxScrollAmountHorizontal() : getMaxScrollAmountVertical();
660 |
661 | if (!horizontal) {
662 | if (nextFocused != null) {
663 | nextFocused.getDrawingRect(mTempRect);
664 | offsetDescendantRectToMyCoords(nextFocused, mTempRect);
665 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
666 | doScroll(0, scrollDelta);
667 | nextFocused.requestFocus(direction);
668 | } else {
669 | // no new focus
670 | int scrollDelta = maxJump;
671 | if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
672 | scrollDelta = getScrollY();
673 | } else if (direction == View.FOCUS_DOWN) {
674 | if (getChildCount() > 0) {
675 | int daBottom = getChildAt(0).getBottom();
676 | int screenBottom = getScrollY() + getHeight();
677 | if (daBottom - screenBottom < maxJump) {
678 | scrollDelta = daBottom - screenBottom;
679 | }
680 | }
681 | }
682 | if (scrollDelta == 0) {
683 | return false;
684 | }
685 | doScroll(0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
686 | }
687 | } else {
688 | if (nextFocused != null) {
689 | nextFocused.getDrawingRect(mTempRect);
690 | offsetDescendantRectToMyCoords(nextFocused, mTempRect);
691 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
692 | doScroll(scrollDelta, 0);
693 | nextFocused.requestFocus(direction);
694 | } else {
695 | // no new focus
696 | int scrollDelta = maxJump;
697 | if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
698 | scrollDelta = getScrollY();
699 | } else if (direction == View.FOCUS_DOWN) {
700 | if (getChildCount() > 0) {
701 | int daBottom = getChildAt(0).getBottom();
702 | int screenBottom = getScrollY() + getHeight();
703 | if (daBottom - screenBottom < maxJump) {
704 | scrollDelta = daBottom - screenBottom;
705 | }
706 | }
707 | }
708 | if (scrollDelta == 0) {
709 | return false;
710 | }
711 | doScroll(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta, 0);
712 | }
713 | }
714 | return true;
715 | }
716 |
717 | /**
718 | * Smooth scroll by a Y delta
719 | *
720 | * @param delta the number of pixels to scroll by on the Y axis
721 | */
722 | private void doScroll(int deltaX, int deltaY) {
723 | if (deltaX != 0 || deltaY != 0) {
724 | smoothScrollBy(deltaX, deltaY);
725 | }
726 | }
727 |
728 | /**
729 | * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
730 | *
731 | * @param dx the number of pixels to scroll by on the X axis
732 | * @param dy the number of pixels to scroll by on the Y axis
733 | */
734 | public final void smoothScrollBy(int dx, int dy) {
735 | long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
736 | if (duration > ANIMATED_SCROLL_GAP) {
737 | mScroller.startScroll(getScrollX(), getScrollY(), dx, dy);
738 | awakenScrollBars(mScroller.getDuration());
739 | invalidate();
740 | } else {
741 | if (!mScroller.isFinished()) {
742 | mScroller.abortAnimation();
743 | }
744 | scrollBy(dx, dy);
745 | }
746 | mLastScroll = AnimationUtils.currentAnimationTimeMillis();
747 | }
748 |
749 | /**
750 | * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
751 | *
752 | * @param x the position where to scroll on the X axis
753 | * @param y the position where to scroll on the Y axis
754 | */
755 | public final void smoothScrollTo(int x, int y) {
756 | smoothScrollBy(x - getScrollX(), y - getScrollY());
757 | }
758 |
759 | /**
760 | * The scroll range of a scroll view is the overall height of all of its
761 | * children.
762 | */
763 | @Override
764 | protected int computeVerticalScrollRange() {
765 | int count = getChildCount();
766 | return count == 0 ? getHeight() : (getChildAt(0)).getBottom();
767 | }
768 |
769 | @Override
770 | protected int computeHorizontalScrollRange() {
771 | int count = getChildCount();
772 | return count == 0 ? getWidth() : (getChildAt(0)).getRight();
773 | }
774 |
775 | @Override
776 | protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
777 | ViewGroup.LayoutParams lp = child.getLayoutParams();
778 | int childWidthMeasureSpec;
779 | int childHeightMeasureSpec;
780 |
781 | childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
782 | childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
783 |
784 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
785 | }
786 |
787 | @Override
788 | protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
789 | final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
790 | final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
791 | final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
792 |
793 | child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
794 | }
795 |
796 | @Override
797 | public void computeScroll() {
798 | if (mScroller.computeScrollOffset()) {
799 | // This is called at drawing time by ViewGroup. We don't want to
800 | // re-show the scrollbars at this point, which scrollTo will do,
801 | // so we replicate most of scrollTo here.
802 | //
803 | // It's a little odd to call onScrollChanged from inside the drawing.
804 | //
805 | // It is, except when you remember that computeScroll() is used to
806 | // animate scrolling. So unless we want to defer the onScrollChanged()
807 | // until the end of the animated scrolling, we don't really have a
808 | // choice here.
809 | //
810 | // I agree. The alternative, which I think would be worse, is to post
811 | // something and tell the subclasses later. This is bad because there
812 | // will be a window where mScrollX/Y is different from what the app
813 | // thinks it is.
814 | //
815 | int oldX = getScrollX();
816 | int oldY = getScrollY();
817 | int x = mScroller.getCurrX();
818 | int y = mScroller.getCurrY();
819 | if (getChildCount() > 0) {
820 | View child = getChildAt(0);
821 | scrollTo(clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()),
822 | clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()));
823 | } else {
824 | scrollTo(x, y);
825 | }
826 | if (oldX != getScrollX() || oldY != getScrollY()) {
827 | onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
828 | }
829 |
830 | // Keep on drawing until the animation has finished.
831 | postInvalidate();
832 | }
833 | }
834 |
835 | /**
836 | * Scrolls the view to the given child.
837 | *
838 | * @param child the View to scroll to
839 | */
840 | private void scrollToChild(View child) {
841 | child.getDrawingRect(mTempRect);
842 | /* Offset from child's local coordinates to TwoDScrollView coordinates */
843 | offsetDescendantRectToMyCoords(child, mTempRect);
844 | int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
845 | if (scrollDelta != 0) {
846 | scrollBy(0, scrollDelta);
847 | }
848 | }
849 |
850 | /**
851 | * If rect is off screen, scroll just enough to get it (or at least the
852 | * first screen size chunk of it) on screen.
853 | *
854 | * @param rect The rectangle.
855 | * @param immediate True to scroll immediately without animation
856 | * @return true if scrolling was performed
857 | */
858 | private boolean scrollToChildRect(Rect rect, boolean immediate) {
859 | final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
860 | final boolean scroll = delta != 0;
861 | if (scroll) {
862 | if (immediate) {
863 | scrollBy(0, delta);
864 | } else {
865 | smoothScrollBy(0, delta);
866 | }
867 | }
868 | return scroll;
869 | }
870 |
871 | /**
872 | * Compute the amount to scroll in the Y direction in order to get
873 | * a rectangle completely on the screen (or, if taller than the screen,
874 | * at least the first screen size chunk of it).
875 | *
876 | * @param rect The rect.
877 | * @return The scroll delta.
878 | */
879 | protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
880 | if (getChildCount() == 0) return 0;
881 | int height = getHeight();
882 | int screenTop = getScrollY();
883 | int screenBottom = screenTop + height;
884 | int fadingEdge = getVerticalFadingEdgeLength();
885 | // leave room for top fading edge as long as rect isn't at very top
886 | if (rect.top > 0) {
887 | screenTop += fadingEdge;
888 | }
889 |
890 | // leave room for bottom fading edge as long as rect isn't at very bottom
891 | if (rect.bottom < getChildAt(0).getHeight()) {
892 | screenBottom -= fadingEdge;
893 | }
894 | int scrollYDelta = 0;
895 | if (rect.bottom > screenBottom && rect.top > screenTop) {
896 | // need to move down to get it in view: move down just enough so
897 | // that the entire rectangle is in view (or at least the first
898 | // screen size chunk).
899 | if (rect.height() > height) {
900 | // just enough to get screen size chunk on
901 | scrollYDelta += (rect.top - screenTop);
902 | } else {
903 | // get entire rect at bottom of screen
904 | scrollYDelta += (rect.bottom - screenBottom);
905 | }
906 |
907 | // make sure we aren't scrolling beyond the end of our content
908 | int bottom = getChildAt(0).getBottom();
909 | int distanceToBottom = bottom - screenBottom;
910 | scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
911 |
912 | } else if (rect.top < screenTop && rect.bottom < screenBottom) {
913 | // need to move up to get it in view: move up just enough so that
914 | // entire rectangle is in view (or at least the first screen
915 | // size chunk of it).
916 |
917 | if (rect.height() > height) {
918 | // screen size chunk
919 | scrollYDelta -= (screenBottom - rect.bottom);
920 | } else {
921 | // entire rect at top
922 | scrollYDelta -= (screenTop - rect.top);
923 | }
924 |
925 | // make sure we aren't scrolling any further than the top our content
926 | scrollYDelta = Math.max(scrollYDelta, -getScrollY());
927 | }
928 | return scrollYDelta;
929 | }
930 |
931 | @Override
932 | public void requestChildFocus(View child, View focused) {
933 | if (!mTwoDScrollViewMovedFocus) {
934 | if (!mIsLayoutDirty) {
935 | scrollToChild(focused);
936 | } else {
937 | // The child may not be laid out yet, we can't compute the scroll yet
938 | mChildToScrollTo = focused;
939 | }
940 | }
941 | super.requestChildFocus(child, focused);
942 | }
943 |
944 | /**
945 | * When looking for focus in children of a scroll view, need to be a little
946 | * more careful not to give focus to something that is scrolled off screen.
947 | *
948 | * This is more expensive than the default {@link android.view.ViewGroup}
949 | * implementation, otherwise this behavior might have been made the default.
950 | */
951 | @Override
952 | protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
953 | // convert from forward / backward notation to up / down / left / right
954 | // (ugh).
955 | if (direction == View.FOCUS_FORWARD) {
956 | direction = View.FOCUS_DOWN;
957 | } else if (direction == View.FOCUS_BACKWARD) {
958 | direction = View.FOCUS_UP;
959 | }
960 |
961 | final View nextFocus = previouslyFocusedRect == null ?
962 | FocusFinder.getInstance().findNextFocus(this, null, direction) :
963 | FocusFinder.getInstance().findNextFocusFromRect(this,
964 | previouslyFocusedRect, direction);
965 |
966 | if (nextFocus == null) {
967 | return false;
968 | }
969 |
970 | return nextFocus.requestFocus(direction, previouslyFocusedRect);
971 | }
972 |
973 | @Override
974 | public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
975 | // offset into coordinate space of this scroll view
976 | rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());
977 | return scrollToChildRect(rectangle, immediate);
978 | }
979 |
980 | @Override
981 | public void requestLayout() {
982 | mIsLayoutDirty = true;
983 | super.requestLayout();
984 | }
985 |
986 | @Override
987 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
988 | super.onLayout(changed, l, t, r, b);
989 | mIsLayoutDirty = false;
990 | // Give a child focus if it needs it
991 | if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
992 | scrollToChild(mChildToScrollTo);
993 | }
994 | mChildToScrollTo = null;
995 |
996 | // Calling this with the present values causes it to re-clam them
997 | scrollTo(getScrollX(), getScrollY());
998 | }
999 |
1000 | @Override
1001 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1002 | super.onSizeChanged(w, h, oldw, oldh);
1003 |
1004 | View currentFocused = findFocus();
1005 | if (null == currentFocused || this == currentFocused)
1006 | return;
1007 |
1008 | // If the currently-focused view was visible on the screen when the
1009 | // screen was at the old height, then scroll the screen to make that
1010 | // view visible with the new screen height.
1011 | currentFocused.getDrawingRect(mTempRect);
1012 | offsetDescendantRectToMyCoords(currentFocused, mTempRect);
1013 | int scrollDeltaX = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1014 | int scrollDeltaY = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
1015 | doScroll(scrollDeltaX, scrollDeltaY);
1016 | }
1017 |
1018 | /**
1019 | * Return true if child is an descendant of parent, (or equal to the parent).
1020 | */
1021 | private boolean isViewDescendantOf(View child, View parent) {
1022 | if (child == parent) {
1023 | return true;
1024 | }
1025 |
1026 | final ViewParent theParent = child.getParent();
1027 | return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
1028 | }
1029 |
1030 | /**
1031 | * Fling the scroll view
1032 | *
1033 | * @param velocityY The initial velocity in the Y direction. Positive
1034 | * numbers mean that the finger/curor is moving down the screen,
1035 | * which means we want to scroll towards the top.
1036 | */
1037 | public void fling(int velocityX, int velocityY) {
1038 | if (getChildCount() > 0) {
1039 | int height = getHeight() - getPaddingBottom() - getPaddingTop();
1040 | int bottom = getChildAt(0).getHeight();
1041 | int width = getWidth() - getPaddingRight() - getPaddingLeft();
1042 | int right = getChildAt(0).getWidth();
1043 |
1044 | mScroller.fling(getScrollX(), getScrollY(), velocityX, velocityY, 0, right - width, 0, bottom - height);
1045 |
1046 | final boolean movingDown = velocityY > 0;
1047 | final boolean movingRight = velocityX > 0;
1048 |
1049 | View newFocused = findFocusableViewInMyBounds(movingRight, mScroller.getFinalX(), movingDown, mScroller.getFinalY(), findFocus());
1050 | if (newFocused == null) {
1051 | newFocused = this;
1052 | }
1053 |
1054 | if (newFocused != findFocus() && newFocused.requestFocus(movingDown ? View.FOCUS_DOWN : View.FOCUS_UP)) {
1055 | mTwoDScrollViewMovedFocus = true;
1056 | mTwoDScrollViewMovedFocus = false;
1057 | }
1058 |
1059 | awakenScrollBars(mScroller.getDuration());
1060 | invalidate();
1061 | }
1062 | }
1063 |
1064 | /**
1065 | * {@inheritDoc}
1066 | *
1067 | * This version also clamps the scrolling to the bounds of our child.
1068 | */
1069 | public void scrollTo(int x, int y) {
1070 | // we rely on the fact the View.scrollBy calls scrollTo.
1071 | if (getChildCount() > 0) {
1072 | View child = getChildAt(0);
1073 | x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
1074 | y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
1075 | if (x != getScrollX() || y != getScrollY()) {
1076 | super.scrollTo(x, y);
1077 | }
1078 | }
1079 | }
1080 |
1081 | private int clamp(int n, int my, int child) {
1082 | if (my >= child || n < 0) {
1083 | /* my >= child is this case:
1084 | * |--------------- me ---------------|
1085 | * |------ child ------|
1086 | * or
1087 | * |--------------- me ---------------|
1088 | * |------ child ------|
1089 | * or
1090 | * |--------------- me ---------------|
1091 | * |------ child ------|
1092 | *
1093 | * n < 0 is this case:
1094 | * |------ me ------|
1095 | * |-------- child --------|
1096 | * |-- mScrollX --|
1097 | */
1098 | return 0;
1099 | }
1100 | if ((my + n) > child) {
1101 | /* this case:
1102 | * |------ me ------|
1103 | * |------ child ------|
1104 | * |-- mScrollX --|
1105 | */
1106 | return child - my;
1107 | }
1108 | return n;
1109 | }
1110 | }
1111 |
--------------------------------------------------------------------------------
/library/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/library/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
--------------------------------------------------------------------------------
/maven_push.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven'
2 | apply plugin: 'signing'
3 |
4 | def sonatypeRepositoryUrl
5 | if (isReleaseBuild()) {
6 | println 'RELEASE BUILD'
7 | sonatypeRepositoryUrl = hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
8 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
9 | } else {
10 | println 'DEBUG BUILD'
11 | sonatypeRepositoryUrl = hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
12 | : "https://oss.sonatype.org/content/repositories/snapshots/"
13 | }
14 |
15 | def getRepositoryUsername() {
16 | return hasProperty('nexusUsername') ? nexusUsername : ""
17 | }
18 |
19 | def getRepositoryPassword() {
20 | return hasProperty('nexusPassword') ? nexusPassword : ""
21 | }
22 |
23 | afterEvaluate { project ->
24 | uploadArchives {
25 | repositories {
26 | mavenDeployer {
27 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
28 |
29 | pom.artifactId = POM_ARTIFACT_ID
30 |
31 | repository(url: sonatypeRepositoryUrl) {
32 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
33 | }
34 |
35 | pom.project {
36 | name POM_NAME
37 | packaging POM_PACKAGING
38 | description POM_DESCRIPTION
39 | url POM_URL
40 |
41 | scm {
42 | url POM_SCM_URL
43 | connection POM_SCM_CONNECTION
44 | developerConnection POM_SCM_DEV_CONNECTION
45 | }
46 |
47 | licenses {
48 | license {
49 | name POM_LICENCE_NAME
50 | url POM_LICENCE_URL
51 | distribution POM_LICENCE_DIST
52 | }
53 | }
54 |
55 | developers {
56 | developer {
57 | id POM_DEVELOPER_ID
58 | name POM_DEVELOPER_NAME
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
66 | signing {
67 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
68 | sign configurations.archives
69 | }
70 |
71 | task androidJavadocs(type: Javadoc) {
72 | source = android.sourceSets.main.java.sourceFiles
73 | }
74 |
75 | task androidJavadocsJar(type: Jar) {
76 | classifier = 'javadoc'
77 | //basename = artifact_id
78 | from androidJavadocs.destinationDir
79 | }
80 |
81 | task androidSourcesJar(type: Jar) {
82 | classifier = 'sources'
83 | //basename = artifact_id
84 | from android.sourceSets.main.java.sourceFiles
85 | }
86 |
87 | artifacts {
88 | //archives packageReleaseJar
89 | archives androidSourcesJar
90 | archives androidJavadocsJar
91 | }
92 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------