Java tutorial
/** * Appcelerator Titanium Mobile * Copyright (c) 2009-2012 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Apache Public License * Please see the LICENSE included with this distribution for details. */ package ti.modules.titanium.ui.widget; import java.util.HashMap; import org.appcelerator.kroll.KrollDict; import org.appcelerator.kroll.KrollProxy; import org.appcelerator.kroll.common.Log; import org.appcelerator.titanium.TiC; import org.appcelerator.titanium.TiDimension; import org.appcelerator.titanium.proxy.TiViewProxy; import org.appcelerator.titanium.util.TiConvert; import org.appcelerator.titanium.TiBaseActivity; import org.appcelerator.titanium.view.TiCompositeLayout; import org.appcelerator.titanium.view.TiCompositeLayout.LayoutArrangement; import org.appcelerator.titanium.view.TiUIView; import ti.modules.titanium.ui.RefreshControlProxy; import android.content.Context; import android.graphics.Canvas; import android.os.Build; import android.support.v4.widget.NestedScrollView; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.HorizontalScrollView; public class TiUIScrollView extends TiUIView { public static final int TYPE_VERTICAL = 0; public static final int TYPE_HORIZONTAL = 1; private static final String TAG = "TiUIScrollView"; private View scrollView; private int offsetX = 0, offsetY = 0; private boolean setInitialOffset = false; private boolean mScrollingEnabled = true; private boolean isScrolling = false; private boolean isTouching = false; public class TiScrollViewLayout extends TiCompositeLayout { private static final int AUTO = Integer.MAX_VALUE; private int parentContentWidth = 0; private int parentContentHeight = 0; private boolean canCancelEvents = true; private GestureDetector gestureDetector; private boolean wasMeasured; public TiScrollViewLayout(Context context, LayoutArrangement arrangement) { super(context, arrangement, proxy); gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public void onLongPress(MotionEvent e) { if (proxy.hierarchyHasListener(TiC.EVENT_LONGPRESS)) { fireEvent(TiC.EVENT_LONGPRESS, dictFromEvent(e)); } } }); setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return gestureDetector.onTouchEvent(event); } }); } /** * Sets the width of this view's parent, excluding its left/right padding. * @param width The parent view's width, excluding padding. */ public void setParentContentWidth(int width) { if (width < 0) { width = 0; } this.parentContentWidth = width; } /** * Gets the value set via the setParentContentWidth() method. * Note that this value is not assignd automatically. The owner must assign it. * @return Returns the parent view's width, excluding its left/right padding. */ public int getParentContentWidth() { return this.parentContentWidth; } /** * Sets the height of this view's parent, excluding its top/bottom padding. * @param width The parent view's height, excluding padding. */ public void setParentContentHeight(int height) { if (height < 0) { height = 0; } this.parentContentHeight = height; } /** * Gets the value set via the setParentContentHeight() method. * Note that this value is not assignd automatically. The owner must assign it. * @return Returns the parent view's height, excluding its top/bottom padding. */ public int getParentContentHeight() { return this.parentContentHeight; } @Override public void setMinimumWidth(int value) { // Make sure given minimum is valid. if (value < 0) { value = 0; } // Update the minimum value, but only if it is changing. // Note: This is an optimization. Avoids unnecessary requestLayout() calls in UI tree. if (value != getMinimumWidth()) { super.setMinimumWidth(value); } } @Override public void setMinimumHeight(int value) { // Make sure given minimum is valid. if (value < 0) { value = 0; } // Update the minimum value, but only if it is changing. // Note: This is an optimization. Avoids unnecessary requestLayout() calls in UI tree. if (value != getMinimumHeight()) { super.setMinimumHeight(value); } } public void setCanCancelEvents(boolean value) { canCancelEvents = value; } /** * Determines if this view's onMeasure() has been called. * @return Returns true if this view's onMeasure() has been called. Returns false if not. */ public boolean wasMeasured() { return this.wasMeasured; } /** * Sets the flag to be returned by this object's wasMeasured() method. * <p> * Intended to be set "false" by the parent view to determine if this view's onMeasure() * method got called afterwards. * @param value The value to be returned by the wasMeasured() method. */ public void setWasMeasured(boolean value) { this.wasMeasured = value; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { // If canCancelEvents is false, then we want to prevent the scroll view from canceling the touch // events of the child view if (!canCancelEvents) { requestDisallowInterceptTouchEvent(true); } return super.dispatchTouchEvent(ev); } private int getContentProperty(String property) { Object value = getProxy().getProperty(property); if (value != null) { if (value.equals(TiC.SIZE_AUTO) || value.equals(TiC.LAYOUT_SIZE)) { return AUTO; } else if (value.equals(TiC.LAYOUT_FILL)) { if (TiC.PROPERTY_CONTENT_HEIGHT.equals(property)) { return this.parentContentHeight; } else if (TiC.PROPERTY_CONTENT_WIDTH.equals(property)) { return this.parentContentWidth; } } else if (value instanceof Number) { return ((Number) value).intValue(); } else { int type = 0; TiDimension dimension; if (TiC.PROPERTY_CONTENT_HEIGHT.equals(property)) { type = TiDimension.TYPE_HEIGHT; } else if (TiC.PROPERTY_CONTENT_WIDTH.equals(property)) { type = TiDimension.TYPE_WIDTH; } dimension = new TiDimension(value.toString(), type); return dimension.getUnits() == TiDimension.COMPLEX_UNIT_AUTO ? AUTO : dimension.getIntValue(); } } return AUTO; } @Override protected int getWidthMeasureSpec(View child) { int contentWidth = getContentProperty(TiC.PROPERTY_CONTENT_WIDTH); if (contentWidth == AUTO) { return MeasureSpec.UNSPECIFIED; } else { return super.getWidthMeasureSpec(child); } } @Override protected int getHeightMeasureSpec(View child) { int contentHeight = getContentProperty(TiC.PROPERTY_CONTENT_HEIGHT); if (contentHeight == AUTO) { return MeasureSpec.UNSPECIFIED; } else { return super.getHeightMeasureSpec(child); } } @Override protected int getMeasuredWidth(int maxWidth, int widthSpec) { int contentWidth = getContentProperty(TiC.PROPERTY_CONTENT_WIDTH); if (contentWidth == AUTO) { contentWidth = maxWidth; // measuredWidth; } // Returns the content's width when it's greater than the scrollview's width if (contentWidth >= this.parentContentWidth) { return contentWidth; } else { return resolveSize(maxWidth, widthSpec); } } @Override protected int getMeasuredHeight(int maxHeight, int heightSpec) { int contentHeight = getContentProperty(TiC.PROPERTY_CONTENT_HEIGHT); if (contentHeight == AUTO) { contentHeight = maxHeight; // measuredHeight; } // Returns the content's height when it's greater than the scrollview's height if (contentHeight >= this.parentContentHeight) { return contentHeight; } else { return resolveSize(maxHeight, heightSpec); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Flag that the onMeasure() method has been called. this.wasMeasured = true; // Apply the "contentWidth" and "contentHeight" sizes to the child instead, if provided. int contentWidth = getContentProperty(TiC.PROPERTY_CONTENT_WIDTH); if ((contentWidth != AUTO) && (contentWidth >= this.parentContentWidth)) { widthMeasureSpec = MeasureSpec.makeMeasureSpec(contentWidth, MeasureSpec.EXACTLY); } int contentHeight = getContentProperty(TiC.PROPERTY_CONTENT_HEIGHT); if ((contentHeight != AUTO) && (contentHeight >= this.parentContentHeight)) { heightMeasureSpec = MeasureSpec.makeMeasureSpec(contentHeight, MeasureSpec.EXACTLY); } // If contentWidth/contentHeight is set to AUTO, then child views set to TI.UI.FILL // must use the ScrollView's container size instead of filling remaining content area. // Note: This matches iOS' behavior. if (contentWidth == AUTO) { setChildFillWidth(this.parentContentWidth); } else { setChildFillWidthToParent(); } if (contentHeight == AUTO) { setChildFillHeight(this.parentContentHeight); } else { setChildFillHeightToParent(); } // Child views using "percent" width/height and top/left/bottom/right/center settings // must be relative to the ScrollView container, not the scrollable content area. setChildRelativeSizingTo(this.parentContentWidth, this.parentContentHeight); // Request the composite layout to measure/resize itself. (Must be done after the above.) super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } // same code, different super-classes private class TiVerticalScrollView extends NestedScrollView { private TiScrollViewLayout layout; public TiVerticalScrollView(Context context, LayoutArrangement arrangement) { super(context); // TIMOB-25359: allow window to re-size when keyboard is shown if (context instanceof TiBaseActivity) { Window window = ((TiBaseActivity) context).getWindow(); int softInputMode = window.getAttributes().softInputMode; if ((softInputMode & WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN) == 0) { window.setSoftInputMode(softInputMode | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); } } setScrollBarStyle(SCROLLBARS_INSIDE_OVERLAY); layout = new TiScrollViewLayout(context, arrangement); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); layout.setLayoutParams(params); super.addView(layout, params); } public TiScrollViewLayout getLayout() { return layout; } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_MOVE && !mScrollingEnabled) { return false; } if (event.getAction() == MotionEvent.ACTION_MOVE && !isTouching) { isTouching = true; } if (event.getAction() == MotionEvent.ACTION_UP && isScrolling) { isScrolling = false; isTouching = false; KrollDict data = new KrollDict(); data.put("decelerate", true); getProxy().fireEvent(TiC.EVENT_DRAGEND, data); } //There's a known Android bug (version 3.1 and above) that will throw an exception when we use 3+ fingers to touch the scrollview. //Link: http://code.google.com/p/android/issues/detail?id=18990 try { return super.onTouchEvent(event); } catch (IllegalArgumentException e) { return false; } } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (mScrollingEnabled) { return super.onInterceptTouchEvent(event); } return false; } /** * Called when a NestedScrollingChild view within the ListView wants to scroll the ListView. * <p> * This can happen with a NestedScrollView or a scrollable TiUIEditText where scrolling * past the top/bottom of the child view should cause the ListView to scroll. * @param target The NestedScrollingChild view that wants to scroll this view. * @param dxConsumed Horizontal scroll distance in pixels already consumed by the child. * @param dyConsumed Vertical scroll distance in pixels already consumed by the child. * @param dxUnconsumed Horizontal distance in pixels that this view is being requested to scroll by. * @param dyUnconsumed Vertical distance in pixels that this view is being requested to scroll by. */ @Override public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if (mScrollingEnabled) { super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); } } public void onDraw(Canvas canvas) { super.onDraw(canvas); // setting offset once when this view is visible if (!setInitialOffset) { scrollTo(offsetX, offsetY); setInitialOffset = true; } } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (!isScrolling && isTouching) { isScrolling = true; KrollDict data = new KrollDict(); getProxy().fireEvent(TiC.EVENT_DRAGSTART, data); } KrollDict data = new KrollDict(); data.put(TiC.EVENT_PROPERTY_X, l); data.put(TiC.EVENT_PROPERTY_Y, t); setContentOffset(l, t); getProxy().fireEvent(TiC.EVENT_SCROLL, data); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Reset flag used to detect if child view's onMeasure() got called. layout.setWasMeasured(false); // Store this view's new size, minus the padding. // Must be assigned before calling onMeasure() below. layout.setParentContentWidth( MeasureSpec.getSize(widthMeasureSpec) - (getPaddingLeft() + getPaddingRight())); layout.setParentContentHeight( MeasureSpec.getSize(heightMeasureSpec) - (getPaddingTop() + getPaddingBottom())); // If the scroll view container has a fixed size (ie: not using AT_MOST/WRAP_CONTENT), // then set up the scrollable content area to be at least the size of the container. // Note: Allows views to be docked to bottom or right side when using a "composite" layout. boolean hasFixedSize = (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY); layout.setMinimumWidth(hasFixedSize ? layout.getParentContentWidth() : 0); hasFixedSize = (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY); layout.setMinimumHeight(hasFixedSize ? layout.getParentContentHeight() : 0); // Update the size of this view and its children. super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Google's scroll view won't call child's measure() method if content height is less than // the scroll view's height. If it wasn't called, then do so now. (See: TIMOB-8243) if (!layout.wasMeasured() && (getChildCount() > 0)) { final View child = getChildAt(0); int height = getMeasuredHeight(); final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width); height -= getPaddingTop(); height -= getPaddingBottom(); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } private class TiHorizontalScrollView extends HorizontalScrollView { private TiScrollViewLayout layout; public TiHorizontalScrollView(Context context, LayoutArrangement arrangement) { super(context); setScrollBarStyle(SCROLLBARS_INSIDE_OVERLAY); setScrollContainer(true); layout = new TiScrollViewLayout(context, arrangement); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); layout.setLayoutParams(params); super.addView(layout, params); } public TiScrollViewLayout getLayout() { return layout; } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_MOVE && !mScrollingEnabled) { return false; } if (event.getAction() == MotionEvent.ACTION_MOVE && !isTouching) { isTouching = true; } if (event.getAction() == MotionEvent.ACTION_UP && isScrolling) { isScrolling = false; isTouching = false; KrollDict data = new KrollDict(); data.put("decelerate", true); getProxy().fireEvent(TiC.EVENT_DRAGEND, data); } //There's a known Android bug (version 3.1 and above) that will throw an exception when we use 3+ fingers to touch the scrollview. //Link: http://code.google.com/p/android/issues/detail?id=18990 try { return super.onTouchEvent(event); } catch (IllegalArgumentException e) { return false; } } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (mScrollingEnabled) { return super.onInterceptTouchEvent(event); } return false; } public void onDraw(Canvas canvas) { super.onDraw(canvas); // setting offset once this view is visible if (!setInitialOffset) { scrollTo(offsetX, offsetY); setInitialOffset = true; } } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); KrollDict data = new KrollDict(); if (!isScrolling && isTouching) { isScrolling = true; getProxy().fireEvent(TiC.EVENT_DRAGSTART, data); } data = new KrollDict(); data.put(TiC.EVENT_PROPERTY_X, l); data.put(TiC.EVENT_PROPERTY_Y, t); setContentOffset(l, t); getProxy().fireEvent(TiC.EVENT_SCROLL, data); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Reset flag used to detect if child view's onMeasure() got called. layout.setWasMeasured(false); // Store this view's new size, minus the padding. // Must be assigned before calling onMeasure() below. layout.setParentContentWidth( MeasureSpec.getSize(widthMeasureSpec) - (getPaddingLeft() + getPaddingRight())); layout.setParentContentHeight( MeasureSpec.getSize(heightMeasureSpec) - (getPaddingTop() + getPaddingBottom())); // If the scroll view container has a fixed size (ie: not using AT_MOST/WRAP_CONTENT), // then set up the scrollable content area to be at least the size of the container. // Note: Allows views to be docked to bottom or right side when using a "composite" layout. boolean hasFixedSize = (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY); layout.setMinimumWidth(hasFixedSize ? layout.getParentContentWidth() : 0); hasFixedSize = (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY); layout.setMinimumHeight(hasFixedSize ? layout.getParentContentHeight() : 0); // Update the size of this view and its children. super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Google's scroll view won't call child's measure() method if content height is less than // the scroll view's height. If it wasn't called, then do so now. (See: TIMOB-8243) if (!layout.wasMeasured() && (getChildCount() > 0)) { final View child = getChildAt(0); int width = getMeasuredWidth(); final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height); width -= getPaddingLeft(); width -= getPaddingRight(); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } public TiUIScrollView(TiViewProxy proxy) { // we create the view after the properties are processed super(proxy); getLayoutParams().autoFillsHeight = true; getLayoutParams().autoFillsWidth = true; } @Override public void release() { // If a refresh control is currently assigned, then detach it. View nativeView = getNativeView(); if (nativeView instanceof TiSwipeRefreshLayout) { RefreshControlProxy.unassignFrom((TiSwipeRefreshLayout) nativeView); } // Release scroll view reference. this.scrollView = null; // Release this object's resources. super.release(); } public void setContentOffset(int x, int y) { KrollDict offset = new KrollDict(); offsetX = x; offsetY = y; offset.put(TiC.EVENT_PROPERTY_X, offsetX); offset.put(TiC.EVENT_PROPERTY_Y, offsetY); getProxy().setProperty(TiC.PROPERTY_CONTENT_OFFSET, offset); } public void setContentOffset(Object hashMap) { if (hashMap instanceof HashMap) { HashMap contentOffset = (HashMap) hashMap; offsetX = TiConvert.toInt(contentOffset, TiC.PROPERTY_X); offsetY = TiConvert.toInt(contentOffset, TiC.PROPERTY_Y); } else { Log.e(TAG, "ContentOffset must be an instance of HashMap"); } } @Override public void propertyChanged(String key, Object oldValue, Object newValue, KrollProxy proxy) { if (Log.isDebugModeEnabled()) { Log.d(TAG, "Property: " + key + " old: " + oldValue + " new: " + newValue, Log.DEBUG_MODE); } if (key.equals(TiC.PROPERTY_CONTENT_OFFSET)) { setContentOffset(newValue); scrollTo(offsetX, offsetY, false); } else if (key.equals(TiC.PROPERTY_CAN_CANCEL_EVENTS)) { View view = this.scrollView; boolean canCancelEvents = TiConvert.toBoolean(newValue); if (view instanceof TiHorizontalScrollView) { ((TiHorizontalScrollView) view).getLayout().setCanCancelEvents(canCancelEvents); } else if (view instanceof TiVerticalScrollView) { ((TiVerticalScrollView) view).getLayout().setCanCancelEvents(canCancelEvents); } } else if (TiC.PROPERTY_SCROLLING_ENABLED.equals(key)) { setScrollingEnabled(newValue); } else if (TiC.PROPERTY_REFRESH_CONTROL.equals(key)) { View nativeView = getNativeView(); if (nativeView instanceof TiSwipeRefreshLayout) { if (newValue == null) { RefreshControlProxy.unassignFrom((TiSwipeRefreshLayout) nativeView); } else if (newValue instanceof RefreshControlProxy) { ((RefreshControlProxy) newValue).assignTo((TiSwipeRefreshLayout) nativeView); } else { Log.e(TAG, "Invalid value assigned to property '" + key + "'. Must be of type 'RefreshControl'."); } } else { Log.e(TAG, "ScrollView failed to obtain reference to 'TiSwipeRefreshLayout' object."); } } else if (TiC.PROPERTY_OVER_SCROLL_MODE.equals(key)) { if (this.scrollView != null) { this.scrollView.setOverScrollMode(TiConvert.toInt(newValue, View.OVER_SCROLL_ALWAYS)); } } super.propertyChanged(key, oldValue, newValue, proxy); } @Override public void processProperties(KrollDict d) { boolean showHorizontalScrollBar = false; boolean showVerticalScrollBar = false; if (d.containsKey(TiC.PROPERTY_SCROLLING_ENABLED)) { setScrollingEnabled(d.get(TiC.PROPERTY_SCROLLING_ENABLED)); } if (d.containsKey(TiC.PROPERTY_SHOW_HORIZONTAL_SCROLL_INDICATOR)) { showHorizontalScrollBar = TiConvert.toBoolean(d, TiC.PROPERTY_SHOW_HORIZONTAL_SCROLL_INDICATOR); } if (d.containsKey(TiC.PROPERTY_SHOW_VERTICAL_SCROLL_INDICATOR)) { showVerticalScrollBar = TiConvert.toBoolean(d, TiC.PROPERTY_SHOW_VERTICAL_SCROLL_INDICATOR); } if (showHorizontalScrollBar && showVerticalScrollBar) { Log.w(TAG, "Both scroll bars cannot be shown. Defaulting to vertical shown"); showHorizontalScrollBar = false; } if (d.containsKey(TiC.PROPERTY_CONTENT_OFFSET)) { Object offset = d.get(TiC.PROPERTY_CONTENT_OFFSET); setContentOffset(offset); } int type = TYPE_VERTICAL; boolean deduced = false; if (d.containsKey(TiC.PROPERTY_WIDTH) && d.containsKey(TiC.PROPERTY_CONTENT_WIDTH)) { Object width = d.get(TiC.PROPERTY_WIDTH); Object contentWidth = d.get(TiC.PROPERTY_CONTENT_WIDTH); if (width.equals(contentWidth) || showVerticalScrollBar) { type = TYPE_VERTICAL; deduced = true; } } if (d.containsKey(TiC.PROPERTY_HEIGHT) && d.containsKey(TiC.PROPERTY_CONTENT_HEIGHT)) { Object height = d.get(TiC.PROPERTY_HEIGHT); Object contentHeight = d.get(TiC.PROPERTY_CONTENT_HEIGHT); if (height.equals(contentHeight) || showHorizontalScrollBar) { type = TYPE_HORIZONTAL; deduced = true; } } // android only property if (d.containsKey(TiC.PROPERTY_SCROLL_TYPE)) { Object scrollType = d.get(TiC.PROPERTY_SCROLL_TYPE); if (scrollType.equals(TiC.LAYOUT_VERTICAL)) { type = TYPE_VERTICAL; } else if (scrollType.equals(TiC.LAYOUT_HORIZONTAL)) { type = TYPE_HORIZONTAL; } else { Log.w(TAG, "scrollType value '" + TiConvert.toString(scrollType) + "' is invalid. Only 'vertical' and 'horizontal' are supported."); } } else if (!deduced && type == TYPE_VERTICAL) { Log.w(TAG, "Scroll direction could not be determined based on the provided view properties. Default VERTICAL scroll direction being used. Use the 'scrollType' property to explicitly set the scrolling direction."); } // we create the view here since we now know the potential widget type LayoutArrangement arrangement = LayoutArrangement.DEFAULT; TiScrollViewLayout scrollViewLayout; if (d.containsKey(TiC.PROPERTY_LAYOUT) && d.getString(TiC.PROPERTY_LAYOUT).equals(TiC.LAYOUT_VERTICAL)) { arrangement = LayoutArrangement.VERTICAL; } else if (d.containsKey(TiC.PROPERTY_LAYOUT) && d.getString(TiC.PROPERTY_LAYOUT).equals(TiC.LAYOUT_HORIZONTAL)) { arrangement = LayoutArrangement.HORIZONTAL; } switch (type) { case TYPE_HORIZONTAL: Log.d(TAG, "creating horizontal scroll view", Log.DEBUG_MODE); this.scrollView = new TiHorizontalScrollView(getProxy().getActivity(), arrangement); scrollViewLayout = ((TiHorizontalScrollView) this.scrollView).getLayout(); break; case TYPE_VERTICAL: default: Log.d(TAG, "creating vertical scroll view", Log.DEBUG_MODE); this.scrollView = new TiVerticalScrollView(getProxy().getActivity(), arrangement); scrollViewLayout = ((TiVerticalScrollView) this.scrollView).getLayout(); } if (d.containsKey(TiC.PROPERTY_CAN_CANCEL_EVENTS)) { ((TiScrollViewLayout) scrollViewLayout) .setCanCancelEvents(TiConvert.toBoolean(d, TiC.PROPERTY_CAN_CANCEL_EVENTS)); } boolean autoContentWidth = (scrollViewLayout .getContentProperty(TiC.PROPERTY_CONTENT_WIDTH) == TiScrollViewLayout.AUTO); boolean wrap = !autoContentWidth; if (d.containsKey(TiC.PROPERTY_HORIZONTAL_WRAP) && wrap) { wrap = TiConvert.toBoolean(d, TiC.PROPERTY_HORIZONTAL_WRAP, true); } scrollViewLayout.setEnableHorizontalWrap(wrap); if (d.containsKey(TiC.PROPERTY_OVER_SCROLL_MODE)) { if (Build.VERSION.SDK_INT >= 9) { this.scrollView.setOverScrollMode( TiConvert.toInt(d.get(TiC.PROPERTY_OVER_SCROLL_MODE), View.OVER_SCROLL_ALWAYS)); } } // Set up the swipe refresh layout container which wraps the scroll view. TiSwipeRefreshLayout swipeRefreshLayout = new TiSwipeRefreshLayout(getProxy().getActivity()) { @Override public void setClickable(boolean value) { View view = getLayout(); if (view != null) { view.setClickable(value); } } @Override public void setLongClickable(boolean value) { View view = getLayout(); if (view != null) { view.setLongClickable(value); } } @Override public void setOnClickListener(View.OnClickListener listener) { View view = getLayout(); if (view != null) { view.setOnClickListener(listener); } } @Override public void setOnLongClickListener(View.OnLongClickListener listener) { View view = getLayout(); if (view != null) { view.setOnLongClickListener(listener); } } }; swipeRefreshLayout.setSwipeRefreshEnabled(false); swipeRefreshLayout.addView(this.scrollView); if (d.containsKey(TiC.PROPERTY_REFRESH_CONTROL)) { Object object = d.get(TiC.PROPERTY_REFRESH_CONTROL); if (object instanceof RefreshControlProxy) { ((RefreshControlProxy) object).assignTo(swipeRefreshLayout); } } setNativeView(swipeRefreshLayout); this.scrollView.setHorizontalScrollBarEnabled(showHorizontalScrollBar); this.scrollView.setVerticalScrollBarEnabled(showVerticalScrollBar); super.processProperties(d); } public TiScrollViewLayout getLayout() { View nativeView = this.scrollView; if (nativeView instanceof TiVerticalScrollView) { return ((TiVerticalScrollView) nativeView).layout; } else if (nativeView instanceof TiHorizontalScrollView) { return ((TiHorizontalScrollView) nativeView).layout; } return null; } public void setScrollingEnabled(Object value) { try { mScrollingEnabled = TiConvert.toBoolean(value); } catch (IllegalArgumentException e) { mScrollingEnabled = true; } } public boolean getScrollingEnabled() { return mScrollingEnabled; } public void scrollTo(int x, int y, boolean smoothScroll) { // Fetch the scroll view. final View view = this.scrollView; if (view == null) { return; } // Convert the given coordinates to pixels. x = TiConvert.toTiDimension(x, -1).getAsPixels(view); y = TiConvert.toTiDimension(y, -1).getAsPixels(view); // Disable smooth scrolling for vertical scroll views if not at top of view. // Note: This works-around a bug in Google's NestedScrollView where attempting to // smooth scrolls will move to a totally different position or opposite directions. if (smoothScroll && (view instanceof TiVerticalScrollView)) { if (((TiVerticalScrollView) view).getScrollY() > 0) { smoothScroll = false; } } // Scroll to the given position. if (smoothScroll) { if (view instanceof TiHorizontalScrollView) { ((TiHorizontalScrollView) view).smoothScrollTo(x, y); } else if (view instanceof TiVerticalScrollView) { ((TiVerticalScrollView) view).smoothScrollTo(x, y); } } else { view.scrollTo(x, y); } view.computeScroll(); } public void scrollToBottom() { View view = this.scrollView; if (view instanceof TiHorizontalScrollView) { ((TiHorizontalScrollView) view).fullScroll(View.FOCUS_RIGHT); } else if (view instanceof TiVerticalScrollView) { ((TiVerticalScrollView) view).fullScroll(View.FOCUS_DOWN); } } public void scrollToTop() { View view = this.scrollView; if (view instanceof TiHorizontalScrollView) { // Scroll to the left-most side of the horizontal scroll view. ((TiHorizontalScrollView) view).fullScroll(View.FOCUS_LEFT); } else if (view instanceof TiVerticalScrollView) { // Scroll to the top of the vertical scroll view. // Note: There is a bug in Google's NestedScrollView where smooth scrolling to top fails // and can scroll down instead. We must work-around it by temporarily disabling it. TiVerticalScrollView verticalScrollView = (TiVerticalScrollView) view; boolean wasEnabled = verticalScrollView.isSmoothScrollingEnabled(); verticalScrollView.setSmoothScrollingEnabled(false); try { ((TiVerticalScrollView) view).fullScroll(View.FOCUS_UP); } finally { verticalScrollView.setSmoothScrollingEnabled(wasEnabled); } } } @Override public void add(TiUIView child) { View nativeView = this.nativeView; try { this.nativeView = getLayout(); super.add(child); } finally { this.nativeView = nativeView; } } @Override public void insertAt(TiUIView child, int position) { View nativeView = this.nativeView; try { this.nativeView = getLayout(); super.insertAt(child, position); } finally { this.nativeView = nativeView; } } @Override public void remove(TiUIView child) { View nativeView = this.nativeView; try { this.nativeView = getLayout(); super.remove(child); } finally { this.nativeView = nativeView; } } @Override public void resort() { View v = getLayout(); if (v instanceof TiCompositeLayout) { ((TiCompositeLayout) v).resort(); } } @Override public void registerForTouch() { if (this.scrollView != null) { registerForTouch(this.scrollView); } } @Override public void registerForKeyPress() { if (this.scrollView != null) { registerForKeyPress(this.scrollView); } } }