blob: 9e5625957c418a087d4d420e9d05ecac7b93d2f3 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.taobao.weex.ui.component;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import com.taobao.weex.WXEnvironment;
import com.taobao.weex.WXSDKInstance;
import com.taobao.weex.annotation.Component;
import com.taobao.weex.annotation.JSMethod;
import com.taobao.weex.common.Constants;
import com.taobao.weex.common.ICheckBindingScroller;
import com.taobao.weex.common.OnWXScrollListener;
import com.taobao.weex.common.WXThread;
import com.taobao.weex.dom.WXDomObject;
import com.taobao.weex.ui.ComponentCreator;
import com.taobao.weex.ui.component.helper.WXStickyHelper;
import com.taobao.weex.ui.view.IWXScroller;
import com.taobao.weex.ui.view.WXBaseRefreshLayout;
import com.taobao.weex.ui.view.WXHorizontalScrollView;
import com.taobao.weex.ui.view.WXScrollView;
import com.taobao.weex.ui.view.WXScrollView.WXScrollViewListener;
import com.taobao.weex.ui.view.refresh.wrapper.BaseBounceView;
import com.taobao.weex.ui.view.refresh.wrapper.BounceScrollerView;
import com.taobao.weex.utils.WXLogUtils;
import com.taobao.weex.utils.WXUtils;
import com.taobao.weex.utils.WXViewUtils;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
//import com.taobao.weex.ui.WXRecycleImageManager;
/**
* Component for scroller. It also support features like
* "appear", "disappear" and "sticky"
*/
@Component(lazyload = false)
public class WXScroller extends WXVContainer<ViewGroup> implements WXScrollViewListener,Scrollable {
public static final String DIRECTION = "direction";
protected int mOrientation = Constants.Orientation.VERTICAL;
private List<WXComponent> mRefreshs=new ArrayList<>();
private int mChildrenLayoutOffset = 0;//Use for offset children layout
private boolean mForceLoadmoreNextTime = false;
private int mOffsetAccuracy = 10;
private Point mLastReport = new Point(-1, -1);
public static class Creator implements ComponentCreator {
public WXComponent createInstance(WXSDKInstance instance, WXDomObject node, WXVContainer parent) throws IllegalAccessException, InvocationTargetException, InstantiationException {
return new WXScroller(instance,node,parent);
}
}
/**
* Map for storing appear information
**/
private Map<String,AppearanceHelper> mAppearanceComponents = new HashMap<>();
/**
* Map for storing component that is sticky.
**/
private Map<String, Map<String, WXComponent>> mStickyMap = new HashMap<>();
private FrameLayout mRealView;
private int mContentHeight = 0;
private WXStickyHelper stickyHelper;
private Handler handler=new Handler();
private boolean isScrollable = true;
@Deprecated
public WXScroller(WXSDKInstance instance, WXDomObject dom, WXVContainer parent, String instanceId, boolean isLazy) {
this(instance,dom,parent);
}
public WXScroller(WXSDKInstance instance, WXDomObject node,
WXVContainer parent) {
super(instance, node, parent);
stickyHelper = new WXStickyHelper(this);
}
/**
* @return FrameLayout inner ScrollView
*/
@Override
public ViewGroup getRealView() {
return mRealView;
}
@Override
public void createViewImpl() {
super.createViewImpl();
for (int i = 0; i < mRefreshs.size(); i++) {
WXComponent component = mRefreshs.get(i);
component.createViewImpl();
checkRefreshOrLoading(component);
}
}
/**
* @return ScrollView
*/
public ViewGroup getInnerView() {
if(getHostView() == null)
return null;
if (getHostView() instanceof BounceScrollerView) {
return ((BounceScrollerView) getHostView()).getInnerView();
} else {
return getHostView();
}
}
@Override
public void addEvent(String type) {
super.addEvent(type);
if (Constants.Event.SCROLL.equals(type) && getInnerView() != null) {
if (getInnerView() instanceof WXScrollView) {
((WXScrollView) getInnerView()).addScrollViewListener(new WXScrollViewListener() {
@Override
public void onScrollChanged(WXScrollView scrollView, int x, int y, int oldx, int oldy) {
if (shouldReport(x, y)) {
fireScrollEvent(scrollView.getContentFrame(), x, y, oldx, oldy);
}
}
@Override
public void onScrollToBottom(WXScrollView scrollView, int x, int y) {
//ignore
}
@Override
public void onScrollStopped(WXScrollView scrollView, int x, int y) {
//ignore
}
@Override
public void onScroll(WXScrollView scrollView, int x, int y) {
//ignore
}
});
} else if (getInnerView() instanceof WXHorizontalScrollView) {
((WXHorizontalScrollView) getInnerView()).addScrollViewListener(new WXHorizontalScrollView.ScrollViewListener() {
@Override
public void onScrollChanged(WXHorizontalScrollView scrollView, int x, int y, int oldx, int oldy) {
if (shouldReport(x, y)) {
fireScrollEvent(scrollView.getContentFrame(), x, y, oldx, oldy);
}
}
});
}
}
}
private void fireScrollEvent(Rect contentFrame, int x, int y, int oldx, int oldy) {
Map<String, Object> event = new HashMap<>(2);
Map<String, Object> contentSize = new HashMap<>(2);
Map<String, Object> contentOffset = new HashMap<>(2);
int viewport = getInstance().getInstanceViewPortWidth();
contentSize.put(Constants.Name.WIDTH, WXViewUtils.getWebPxByWidth(contentFrame.width(), viewport));
contentSize.put(Constants.Name.HEIGHT, WXViewUtils.getWebPxByWidth(contentFrame.height(), viewport));
contentOffset.put(Constants.Name.X, -WXViewUtils.getWebPxByWidth(x, viewport));
contentOffset.put(Constants.Name.Y, -WXViewUtils.getWebPxByWidth(y, viewport));
event.put(Constants.Name.CONTENT_SIZE, contentSize);
event.put(Constants.Name.CONTENT_OFFSET, contentOffset);
fireEvent(Constants.Event.SCROLL, event);
}
private boolean shouldReport(int x, int y) {
if (mLastReport.x == -1 && mLastReport.y == -1) {
mLastReport.x = x;
mLastReport.y = y;
return true;
}
if (mOrientation == Constants.Orientation.HORIZONTAL
&& Math.abs(x - mLastReport.x) >= mOffsetAccuracy) {
mLastReport.x = x;
mLastReport.y = y;
return true;
}
if (mOrientation == Constants.Orientation.VERTICAL
&& Math.abs(y - mLastReport.y) >= mOffsetAccuracy) {
mLastReport.x = x;
mLastReport.y = y;
return true;
}
return false;
}
/**
* Intercept refresh view and loading view
*/
@Override
public void addSubView(View child, int index) {
if (child == null || getRealView() == null) {
return;
}
if (child instanceof WXBaseRefreshLayout) {
return;
}
int count = getRealView().getChildCount();
index = index >= count ? -1 : index;
if (index == -1) {
getRealView().addView(child);
} else {
getRealView().addView(child, index);
}
}
@Override
protected int getChildrenLayoutTopOffset() {
return mChildrenLayoutOffset;
}
/**
* Intercept refresh view and loading view
*/
@Override
public void addChild(WXComponent child, int index) {
mChildrenLayoutOffset += child.getLayoutTopOffsetForSibling();
if (child instanceof WXBaseRefresh) {
if (!checkRefreshOrLoading(child)) {
mRefreshs.add(child);
}
}
super.addChild(child,index);
}
/**
* Setting refresh view and loading view
* @param child the refresh_view or loading_view
*/
private boolean checkRefreshOrLoading(final WXComponent child) {
boolean result = false;
if (child instanceof WXRefresh && getHostView() != null) {
((BaseBounceView) getHostView()).setOnRefreshListener((WXRefresh) child);
Runnable runnable = WXThread.secure(new Runnable(){
@Override
public void run() {
((BaseBounceView) getHostView()).setHeaderView(child);
}
});
handler.postDelayed(runnable,100);
}
if (child instanceof WXLoading && getHostView() !=null) {
((BaseBounceView) getHostView()).setOnLoadingListener((WXLoading)child);
Runnable runnable= WXThread.secure(new Runnable(){
@Override
public void run() {
((BaseBounceView) getHostView()).setFooterView(child);
}
});
handler.postDelayed(runnable, 100);
result = true;
}
return result;
}
@Override
public void remove(WXComponent child,boolean destory) {
super.remove(child,destory);
if(child instanceof WXLoading){
((BaseBounceView)getHostView()).removeFooterView(child);
}else if(child instanceof WXRefresh){
((BaseBounceView)getHostView()).removeHeaderView(child);
}
}
@Override
public void destroy() {
super.destroy();
if (mAppearanceComponents != null) {
mAppearanceComponents.clear();
}
if (mStickyMap != null) {
mStickyMap.clear();
}
if (getInnerView() != null && getInnerView() instanceof IWXScroller) {
((IWXScroller) getInnerView()).destroy();
}
}
@Override
protected MeasureOutput measure(int width, int height) {
MeasureOutput measureOutput = new MeasureOutput();
if (this.mOrientation == Constants.Orientation.HORIZONTAL) {
int screenW = WXViewUtils.getScreenWidth(WXEnvironment.sApplication);
int weexW = WXViewUtils.getWeexWidth(getInstanceId());
measureOutput.width = width > (weexW >= screenW ? screenW : weexW) ? FrameLayout.LayoutParams.MATCH_PARENT
: width;
measureOutput.height = height;
} else {
int screenH = WXViewUtils.getScreenHeight(WXEnvironment.sApplication);
int weexH = WXViewUtils.getWeexHeight(getInstanceId());
measureOutput.height = height > (weexH >= screenH ? screenH : weexH) ? FrameLayout.LayoutParams.MATCH_PARENT
: height;
measureOutput.width = width;
}
return measureOutput;
}
@Override
protected ViewGroup initComponentHostView(@NonNull Context context) {
String scroll;
if (getDomObject() == null || getDomObject().getAttrs().isEmpty()) {
scroll = "vertical";
} else {
scroll = getDomObject().getAttrs().getScrollDirection();
}
ViewGroup host;
if(("horizontal").equals(scroll)){
mOrientation = Constants.Orientation.HORIZONTAL;
WXHorizontalScrollView scrollView = new WXHorizontalScrollView(context);
mRealView = new FrameLayout(context);
scrollView.setScrollViewListener(new WXHorizontalScrollView.ScrollViewListener() {
@Override
public void onScrollChanged(WXHorizontalScrollView scrollView, int x, int y, int oldx, int oldy) {
procAppear(x,y,oldx,oldy);
}
});
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
scrollView.addView(mRealView, layoutParams);
scrollView.setHorizontalScrollBarEnabled(false);
host = scrollView;
}else{
mOrientation = Constants.Orientation.VERTICAL;
BounceScrollerView scrollerView = new BounceScrollerView(context, mOrientation, this);
mRealView = new FrameLayout(context);
WXScrollView innerView = scrollerView.getInnerView();
innerView.addScrollViewListener(this);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
innerView.addView(mRealView, layoutParams);
innerView.setVerticalScrollBarEnabled(true);
innerView.addScrollViewListener(new WXScrollViewListener() {
@Override
public void onScrollChanged(WXScrollView scrollView, int x, int y, int oldx, int oldy) {
}
@Override
public void onScrollToBottom(WXScrollView scrollView, int x, int y) {
}
@Override
public void onScrollStopped(WXScrollView scrollView, int x, int y) {
List<OnWXScrollListener> listeners = getInstance().getWXScrollListeners();
if(listeners!=null && listeners.size()>0){
for (OnWXScrollListener listener : listeners) {
if (listener != null) {
listener.onScrollStateChanged(scrollView,x,y,OnWXScrollListener.IDLE);
}
}
}
}
@Override
public void onScroll(WXScrollView scrollView, int x, int y) {
List<OnWXScrollListener> listeners = getInstance().getWXScrollListeners();
if(listeners!=null && listeners.size()>0){
for (OnWXScrollListener listener : listeners) {
if (listener != null) {
if(listener instanceof ICheckBindingScroller){
if(((ICheckBindingScroller) listener).isNeedScroller(getRef(),null)){
listener.onScrolled(scrollView, x, y);
}
}else {
listener.onScrolled(scrollView, x, y);
}
}
}
}
}
});
host = scrollerView;
}
host.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public void onGlobalLayout() {
procAppear(0,0,0,0);
View view;
if( (view = getHostView()) == null){
return;
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}else{
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
}
});
return host;
}
@Override
public int getScrollY() {
return getInnerView() == null ? 0 : getInnerView().getScrollY();
}
@Override
public int getScrollX() {
return getInnerView() == null ? 0 : getInnerView().getScrollX();
}
@Override
public int getOrientation() {
return mOrientation;
}
public Map<String, Map<String, WXComponent>> getStickMap() {
return mStickyMap;
}
@Override
protected boolean setProperty(String key, Object param) {
switch (key) {
case Constants.Name.SHOW_SCROLLBAR:
Boolean result = WXUtils.getBoolean(param,null);
if (result != null)
setShowScrollbar(result);
return true;
case Constants.Name.SCROLLABLE:
boolean scrollable = WXUtils.getBoolean(param, true);
setScrollable(scrollable);
return true;
case Constants.Name.OFFSET_ACCURACY:
int accuracy = WXUtils.getInteger(param, 10);
setOffsetAccuracy(accuracy);
return true;
}
return super.setProperty(key, param);
}
@WXComponentProp(name = Constants.Name.SHOW_SCROLLBAR)
public void setShowScrollbar(boolean show) {
if(getInnerView()==null){
return;
}
if (mOrientation == Constants.Orientation.VERTICAL) {
getInnerView().setVerticalScrollBarEnabled(show);
} else {
getInnerView().setHorizontalScrollBarEnabled(show);
}
}
@WXComponentProp(name = Constants.Name.SCROLLABLE)
public void setScrollable(boolean scrollable) {
this.isScrollable = scrollable;
View hostView = getInnerView();
if(hostView instanceof WXHorizontalScrollView) {
((WXHorizontalScrollView)hostView).setScrollable(scrollable);
}else if(hostView instanceof WXScrollView) {
((WXScrollView)hostView).setScrollable(scrollable);
}
}
@WXComponentProp(name = Constants.Name.OFFSET_ACCURACY)
public void setOffsetAccuracy(int accuracy) {
float realPx = WXViewUtils.getRealPxByWidth(accuracy, getInstance().getInstanceViewPortWidth());
this.mOffsetAccuracy = (int) realPx;
}
@Override
public boolean isScrollable() {
return isScrollable;
}
// TODO Need constrain, each container can only have one sticky child
@Override
public void bindStickStyle(WXComponent component) {
stickyHelper.bindStickStyle(component,mStickyMap);
}
@Override
public void unbindStickStyle(WXComponent component) {
stickyHelper.unbindStickStyle(component,mStickyMap);
}
/**
* Bind appear event
*/
@Override
public void bindAppearEvent(WXComponent component) {
setWatch(AppearanceHelper.APPEAR,component,true);
}
private void setWatch(int event,WXComponent component,boolean isWatch){
AppearanceHelper item = mAppearanceComponents.get(component.getRef());
if (item == null) {
item = new AppearanceHelper(component);
mAppearanceComponents.put(component.getRef(),item);
}
item.setWatchEvent(event,isWatch);
procAppear(0,0,0,0);//check current components appearance status.
}
/**
* Bind disappear event
*/
@Override
public void bindDisappearEvent(WXComponent component) {
setWatch(AppearanceHelper.DISAPPEAR,component,true);
}
/**
* Remove appear event
*/
@Override
public void unbindAppearEvent(WXComponent component) {
setWatch(AppearanceHelper.APPEAR,component,false);
}
/**
* Remove disappear event
*/
@Override
public void unbindDisappearEvent(WXComponent component) {
setWatch(AppearanceHelper.DISAPPEAR,component,false);
}
@Override
public void scrollTo(WXComponent component, Map<String, Object> options) {
float offsetFloat = 0;
boolean smooth = true;
if (options != null) {
String offset = options.get(Constants.Name.OFFSET) == null ? "0" : options.get(Constants.Name.OFFSET).toString();
smooth = WXUtils.getBoolean(options.get(Constants.Name.ANIMATED), true);
if (offset != null) {
try {
offsetFloat = WXViewUtils.getRealPxByWidth(Float.parseFloat(offset), getInstance().getInstanceViewPortWidth());
}catch (Exception e ){
WXLogUtils.e("Float parseFloat error :"+e.getMessage());
}
}
}
int viewYInScroller=component.getAbsoluteY() - getAbsoluteY();
int viewXInScroller=component.getAbsoluteX() - getAbsoluteX();
scrollBy(viewXInScroller - getScrollX() + (int) offsetFloat, viewYInScroller - getScrollY() + (int) offsetFloat, smooth);
}
/**
* Scroll by specified distance. Horizontal scroll is not supported now.
* @param x horizontal distance, not support
* @param y vertical distance. Negative for scroll to top
*/
public void scrollBy(final int x, final int y) {
scrollBy(x, y, false);
}
public void scrollBy(final int x, final int y, final boolean smooth) {
if (getInnerView() == null) {
return;
}
getInnerView().postDelayed(new Runnable() {
@Override
public void run() {
if (mOrientation == Constants.Orientation.VERTICAL) {
if (smooth) {
((WXScrollView) getInnerView()).smoothScrollBy(0, y);
} else {
((WXScrollView) getInnerView()).scrollBy(0, y);
}
} else {
if (smooth) {
((WXHorizontalScrollView) getInnerView()).smoothScrollBy(x, 0);
} else {
((WXHorizontalScrollView) getInnerView()).scrollBy(x, 0);
}
}
getInnerView().invalidate();
}
}, 16);
}
@Override
public void onScrollChanged(WXScrollView scrollView, int x, int y,
int oldx, int oldy) {
procAppear( x, y, oldx, oldy);
}
/**
* Process event like appear and disappear
*/
private void procAppear(int x, int y, int oldx,
int oldy) {
int moveY = y - oldy;
int moveX = x - oldx;
String direction = moveY > 0 ? Constants.Value.DIRECTION_UP :
moveY < 0 ? Constants.Value.DIRECTION_DOWN : null;
if (mOrientation == Constants.Orientation.HORIZONTAL && moveX != 0) {
direction = moveX > 0 ? Constants.Value.DIRECTION_RIGHT : Constants.Value.DIRECTION_LEFT;
}
for (Entry<String, AppearanceHelper> item : mAppearanceComponents.entrySet()) {
AppearanceHelper helper = item.getValue();
if (!helper.isWatch()) {
continue;
}
boolean visible = helper.isViewVisible(false);
int result = helper.setAppearStatus(visible);
if (result != AppearanceHelper.RESULT_NO_CHANGE) {
helper.getAwareChild().notifyAppearStateChange(result == AppearanceHelper.RESULT_APPEAR ? Constants.Event.APPEAR : Constants.Event.DISAPPEAR, direction);
}
}
}
@Override
public void onScrollToBottom(WXScrollView scrollView, int x, int y) {
}
@Override
public void onScrollStopped(WXScrollView scrollView, int x, int y) {
}
@Override
public void onScroll(WXScrollView scrollView, int x, int y) {
this.onLoadMore(scrollView, x, y);
}
/**
* Handle loadMore Event.when Scroller has bind loadMore Event and set the attr of loadMoreOffset
* it will tell the JS to handle the event of onLoadMore;
* @param scrollView the WXScrollView
* @param x the X direction
* @param y the Y direction
*/
protected void onLoadMore(WXScrollView scrollView, int x, int y) {
try {
String offset = getDomObject().getAttrs().getLoadMoreOffset();
if (TextUtils.isEmpty(offset)) {
return;
}
int offsetInt = (int)WXViewUtils.getRealPxByWidth(Float.parseFloat(offset), getInstance().getInstanceViewPortWidth());
int contentH = scrollView.getChildAt(0).getHeight();
int scrollerH = scrollView.getHeight();
int offScreenY = contentH - y - scrollerH;
if (offScreenY < offsetInt) {
if (WXEnvironment.isApkDebugable()) {
WXLogUtils.d("[WXScroller-onScroll] offScreenY :" + offScreenY);
}
if (mContentHeight != contentH || mForceLoadmoreNextTime) {
fireEvent(Constants.Event.LOADMORE);
mContentHeight = contentH;
mForceLoadmoreNextTime = false;
}
}
} catch (Exception e) {
WXLogUtils.d("[WXScroller-onScroll] ", e);
}
}
@JSMethod
public void resetLoadmore() {
mForceLoadmoreNextTime = true;
}
}