Merge branch 'feature/dev' into 'master'

Feature/dev



See merge request !28
This commit is contained in:
pengfeizhou 2019-11-27 20:01:20 +08:00
commit 458fe2385f
53 changed files with 2369 additions and 449 deletions

View File

@ -27,6 +27,7 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.10.0'
implementation 'com.github.bumptech.glide:annotations:4.10.0'
implementation 'com.github.penfeizhou.android.animation:glide-plugin:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-4'
}

View File

@ -3,6 +3,7 @@
package="pub.doric.demo">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MyApplication"
android:allowBackup="true"
@ -12,6 +13,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".PullableActivity"></activity>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -16,6 +16,7 @@
package pub.doric.demo;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.Gravity;
@ -34,6 +35,7 @@ import java.util.List;
import pub.doric.DoricActivity;
import pub.doric.devkit.ui.DemoDebugActivity;
import pub.doric.refresh.DoricSwipeLayout;
import pub.doric.utils.DoricUtils;
public class MainActivity extends AppCompatActivity {
@ -43,11 +45,25 @@ public class MainActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final DoricSwipeLayout swipeLayout = findViewById(R.id.swipe_layout);
swipeLayout.setOnRefreshListener(new DoricSwipeLayout.OnRefreshListener() {
@Override
public void onRefresh() {
swipeLayout.setRefreshing(false);
}
});
swipeLayout.setBackgroundColor(Color.YELLOW);
swipeLayout.getRefreshView().setBackgroundColor(Color.RED);
TextView textView = new TextView(this);
textView.setText("This is header");
swipeLayout.getRefreshView().setContent(textView);
RecyclerView recyclerView = findViewById(R.id.root);
recyclerView.setBackgroundColor(Color.WHITE);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
try {
String[] demos = getAssets().list("demo");
List<String> ret = new ArrayList<>();
ret.add("Test");
for (String str : demos) {
if (str.endsWith("js")) {
ret.add(str);
@ -91,6 +107,11 @@ public class MainActivity extends AppCompatActivity {
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (data[position].contains("Test")) {
Intent intent = new Intent(tv.getContext(), PullableActivity.class);
tv.getContext().startActivity(intent);
return;
}
if (data[position].contains("NavigatorDemo")) {
Intent intent = new Intent(tv.getContext(), DoricActivity.class);
intent.putExtra("scheme", "assets://demo/" + data[position]);

View File

@ -0,0 +1,28 @@
package pub.doric.demo;
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.FrameLayout;
import pub.doric.refresh.DoricSwipeLayout;
public class PullableActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pullable);
final DoricSwipeLayout swipeRefreshLayout = findViewById(R.id.swipe_layout);
FrameLayout frameLayout = new FrameLayout(this);
frameLayout.setBackgroundColor(Color.YELLOW);
swipeRefreshLayout.addView(frameLayout);
swipeRefreshLayout.setOnRefreshListener(new DoricSwipeLayout.OnRefreshListener() {
@Override
public void onRefresh() {
swipeRefreshLayout.setRefreshing(false);
}
});
}
}

View File

@ -1,8 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<pub.doric.refresh.DoricSwipeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:id="@+id/swipe_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" />
</pub.doric.refresh.DoricSwipeLayout>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PullableActivity">
<pub.doric.refresh.DoricSwipeLayout
android:id="@+id/swipe_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View File

@ -25,6 +25,7 @@ import pub.doric.plugin.NavigatorPlugin;
import pub.doric.plugin.NetworkPlugin;
import pub.doric.plugin.ShaderPlugin;
import pub.doric.plugin.StoragePlugin;
import pub.doric.refresh.RefreshableNode;
import pub.doric.shader.HLayoutNode;
import pub.doric.shader.ImageNode;
import pub.doric.shader.ScrollerNode;
@ -96,6 +97,7 @@ public class DoricRegistry {
this.registerViewNode(ScrollerNode.class);
this.registerViewNode(SliderNode.class);
this.registerViewNode(SlideItemNode.class);
this.registerViewNode(RefreshableNode.class);
initRegistry(this);
}

View File

@ -96,6 +96,12 @@ public class DoricJSEngine implements Handler.Callback, DoricTimerExtension.Time
return null;
}
});
mDoricJSE.injectGlobalJSFunction(DoricConstant.INJECT_EMPTY, new JavaFunction() {
@Override
public JavaValue exec(JSDecoder[] args) {
return null;
}
});
mDoricJSE.injectGlobalJSFunction(DoricConstant.INJECT_REQUIRE, new JavaFunction() {
@Override
public JavaValue exec(JSDecoder[] args) {

View File

@ -0,0 +1,100 @@
package pub.doric.refresh;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.widget.FrameLayout;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* @Description: pub.doric.pullable
* @Author: pengfei.zhou
* @CreateDate: 2019-11-25
*/
public class DoricRefreshView extends FrameLayout implements PullingListener {
private View content;
private Animation.AnimationListener mListener;
private PullingListener mPullingListenr;
public DoricRefreshView(@NonNull Context context) {
super(context);
}
public DoricRefreshView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public DoricRefreshView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setContent(View v) {
removeAllViews();
content = v;
if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) {
((LayoutParams) v.getLayoutParams()).gravity = Gravity.BOTTOM;
} else {
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER;
v.setLayoutParams(params);
}
addView(v);
}
public View getContent() {
return content;
}
public void setPullingListenr(PullingListener listenr) {
this.mPullingListenr = listenr;
}
@Override
public void startAnimation() {
if (mPullingListenr != null) {
mPullingListenr.startAnimation();
}
}
@Override
public void stopAnimation() {
if (mPullingListenr != null) {
mPullingListenr.stopAnimation();
}
}
@Override
public void setProgressRotation(float rotation) {
if (mPullingListenr != null) {
mPullingListenr.setProgressRotation(rotation);
}
}
public void setAnimationListener(Animation.AnimationListener listener) {
mListener = listener;
}
@Override
protected void onAnimationStart() {
super.onAnimationStart();
if (mListener != null) {
mListener.onAnimationStart(getAnimation());
}
}
@Override
protected void onAnimationEnd() {
super.onAnimationEnd();
if (mListener != null) {
mListener.onAnimationEnd(getAnimation());
}
}
}

View File

@ -0,0 +1,958 @@
package pub.doric.refresh;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import android.widget.AbsListView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.NestedScrollingChild;
import androidx.core.view.NestedScrollingChildHelper;
import androidx.core.view.NestedScrollingParent;
import androidx.core.view.NestedScrollingParentHelper;
import androidx.core.view.ViewCompat;
import androidx.core.widget.ListViewCompat;
import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
import android.view.animation.Animation.AnimationListener;
/**
* @Description: pub.doric.pullable
* @Author: pengfei.zhou
* @CreateDate: 2019-11-25
*/
public class DoricSwipeLayout extends ViewGroup implements NestedScrollingParent,
NestedScrollingChild {
// Maps to ProgressBar.Large style
public static final int LARGE = CircularProgressDrawable.LARGE;
// Maps to ProgressBar default style
public static final int DEFAULT = CircularProgressDrawable.DEFAULT;
public static final int DEFAULT_SLINGSHOT_DISTANCE = -1;
@VisibleForTesting
static final int CIRCLE_DIAMETER = 40;
@VisibleForTesting
static final int CIRCLE_DIAMETER_LARGE = 56;
private static final String LOG_TAG = DoricSwipeLayout.class.getSimpleName();
private static final int MAX_ALPHA = 255;
private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA);
private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
private static final int INVALID_POINTER = -1;
private static final float DRAG_RATE = .5f;
// Max amount of circle that can be filled by progress during swipe gesture,
// where 1.0 is a full circle
private static final float MAX_PROGRESS_ANGLE = .8f;
private static final int SCALE_DOWN_DURATION = 150;
private static final int ALPHA_ANIMATION_DURATION = 300;
private static final int ANIMATE_TO_TRIGGER_DURATION = 200;
private static final int ANIMATE_TO_START_DURATION = 200;
// Default offset in dips from the top of the view to where the progress spinner should stop
private static final int DEFAULT_CIRCLE_TARGET = 64;
private View mTarget; // the target of the gesture
OnRefreshListener mListener;
boolean mRefreshing = false;
private int mTouchSlop;
private float mTotalDragDistance = -1;
// If nested scrolling is enabled, the total amount that needed to be
// consumed by this as the nested scrolling parent is used in place of the
// overscroll determined by MOVE events in the onTouch handler
private float mTotalUnconsumed;
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
private final NestedScrollingChildHelper mNestedScrollingChildHelper;
private final int[] mParentScrollConsumed = new int[2];
private final int[] mParentOffsetInWindow = new int[2];
private boolean mNestedScrollInProgress;
private int mMediumAnimationDuration;
int mCurrentTargetOffsetTop;
private float mInitialMotionY;
private float mInitialDownY;
private boolean mIsBeingDragged;
private int mActivePointerId = INVALID_POINTER;
// Target is returning to its start offset because it was cancelled or a
// refresh was triggered.
private boolean mReturningToStart;
private final DecelerateInterpolator mDecelerateInterpolator;
private static final int[] LAYOUT_ATTRS = new int[]{
android.R.attr.enabled
};
private int mCircleViewIndex = -1;
protected int mFrom;
float mStartingScale;
protected int mOriginalOffsetTop;
int mSpinnerOffsetEnd;
int mCustomSlingshotDistance;
private Animation mScaleAnimation;
private Animation mScaleDownAnimation;
private Animation mScaleDownToStartAnimation;
boolean mNotify;
// Whether the client has set a custom starting position;
boolean mUsingCustomStart;
private OnChildScrollUpCallback mChildScrollUpCallback;
private DoricRefreshView mRefreshView;
private AnimationListener mRefreshListener = new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (mRefreshing) {
mRefreshView.startAnimation();
if (mNotify) {
if (mListener != null) {
mListener.onRefresh();
}
}
mCurrentTargetOffsetTop = mRefreshView.getTop();
} else {
reset();
}
}
};
private int mPullDownHeight = 0;
private ValueAnimator headerViewAnimator;
void reset() {
mRefreshing = false;
if (headerViewAnimator != null && headerViewAnimator.isRunning()) {
headerViewAnimator.cancel();
}
headerViewAnimator = ValueAnimator.ofInt(mRefreshView.getBottom(), 0);
headerViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentTargetOffsetTop = (int) animation.getAnimatedValue()
- mRefreshView.getMeasuredHeight();
mRefreshView.requestLayout();
}
});
headerViewAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mRefreshView.stopAnimation();
mRefreshView.setVisibility(View.GONE);
// Return the circle to its start position
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop);
mCurrentTargetOffsetTop = mRefreshView.getTop();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
headerViewAnimator.setDuration(SCALE_DOWN_DURATION);
headerViewAnimator.start();
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (!enabled) {
reset();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
reset();
}
/**
* Simple constructor to use when creating a SwipeRefreshLayout from code.
*
* @param context
*/
public DoricSwipeLayout(@NonNull Context context) {
this(context, null);
}
/**
* Constructor that is called when inflating SwipeRefreshLayout from XML.
*
* @param context
* @param attrs
*/
public DoricSwipeLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mMediumAnimationDuration = getResources().getInteger(
android.R.integer.config_mediumAnimTime);
setWillNotDraw(false);
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
final DisplayMetrics metrics = getResources().getDisplayMetrics();
createProgressView();
setChildrenDrawingOrderEnabled(true);
// the absolute offset has to take into account that the circle starts at an offset
mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);
mTotalDragDistance = mSpinnerOffsetEnd;
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
moveToStart(1.0f);
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();
}
public void setPullDownHeight(int height) {
mPullDownHeight = height;
mOriginalOffsetTop = mCurrentTargetOffsetTop = -height;
mSpinnerOffsetEnd = height;
mTotalDragDistance = height;
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
if (mCircleViewIndex < 0) {
return i;
} else if (i == childCount - 1) {
// Draw the selected child last
return mCircleViewIndex;
} else if (i >= mCircleViewIndex) {
// Move the children after the selected child earlier one
return i + 1;
} else {
// Keep the children before the selected child the same
return i;
}
}
private void createProgressView() {
mRefreshView = new DoricRefreshView(getContext());
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
addView(mRefreshView, layoutParams);
}
public DoricRefreshView getRefreshView() {
return mRefreshView;
}
/**
* Set the listener to be notified when a refresh is triggered via the swipe
* gesture.
*/
public void setOnRefreshListener(@Nullable OnRefreshListener listener) {
mListener = listener;
}
/**
* Notify the widget that refresh state has changed. Do not call this when
* refresh is triggered by a swipe gesture.
*
* @param refreshing Whether or not the view should show refresh progress.
*/
public void setRefreshing(boolean refreshing) {
if (refreshing && mRefreshing != refreshing) {
// scale and show
mRefreshing = refreshing;
int endTarget = 0;
if (!mUsingCustomStart) {
endTarget = mSpinnerOffsetEnd + mOriginalOffsetTop;
} else {
endTarget = mSpinnerOffsetEnd;
}
setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop);
mNotify = false;
startScaleUpAnimation(mRefreshListener);
} else {
setRefreshing(refreshing, false /* notify */);
}
}
private void startScaleUpAnimation(AnimationListener listener) {
mRefreshView.setVisibility(View.VISIBLE);
mScaleAnimation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
setAnimationProgress(interpolatedTime);
}
};
mScaleAnimation.setDuration(mMediumAnimationDuration);
if (listener != null) {
mRefreshView.setAnimationListener(listener);
}
mRefreshView.clearAnimation();
mRefreshView.startAnimation(mScaleAnimation);
}
/**
* Pre API 11, this does an alpha animation.
*
* @param progress
*/
void setAnimationProgress(float progress) {
}
private void setRefreshing(boolean refreshing, final boolean notify) {
if (mRefreshing != refreshing) {
mNotify = notify;
ensureTarget();
mRefreshing = refreshing;
if (mRefreshing) {
animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
} else {
startScaleDownAnimation(mRefreshListener);
}
}
}
void startScaleDownAnimation(Animation.AnimationListener listener) {
mScaleDownAnimation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
setAnimationProgress(1 - interpolatedTime);
}
};
mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION);
mRefreshView.setAnimationListener(listener);
mRefreshView.clearAnimation();
mRefreshView.startAnimation(mScaleDownAnimation);
}
/**
* @return Whether the SwipeRefreshWidget is actively showing refresh
* progress.
*/
public boolean isRefreshing() {
return mRefreshing;
}
private void ensureTarget() {
// Don't bother getting the parent height if the parent hasn't been laid
// out yet.
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mRefreshView)) {
mTarget = child;
break;
}
}
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
int circleWidth = mRefreshView.getMeasuredWidth();
int circleHeight = mRefreshView.getMeasuredHeight();
mRefreshView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop() + mRefreshView.getBottom();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
mRefreshView.measure(
MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(
(getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) / 3,
MeasureSpec.UNSPECIFIED));
if (mPullDownHeight != mRefreshView.getMeasuredHeight()) {
setPullDownHeight(mRefreshView.getMeasuredHeight());
}
mCircleViewIndex = -1;
// Get the index of the circleview.
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mRefreshView) {
mCircleViewIndex = index;
break;
}
}
}
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
*/
public boolean canChildScrollUp() {
if (mChildScrollUpCallback != null) {
return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
}
if (mTarget instanceof ListView) {
return ListViewCompat.canScrollList((ListView) mTarget, -1);
}
return mTarget.canScrollVertically(-1);
}
/**
* Set a callback to override {@link androidx.swiperefreshlayout.widget.SwipeRefreshLayout#canChildScrollUp()} method. Non-null
* callback will return the value provided by the callback and ignore all internal logic.
*
* @param callback Callback that should be called when canChildScrollUp() is called.
*/
public void setOnChildScrollUpCallback(@Nullable OnChildScrollUpCallback callback) {
mChildScrollUpCallback = callback;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
final int action = ev.getActionMasked();
int pointerIndex;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mRefreshView.getTop());
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
mInitialDownY = ev.getY(pointerIndex);
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// if this is a List < L or another view that doesn't support nested
// scrolling, ignore this request so that the vertical scroll event
// isn't stolen
if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
|| (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
// Nope.
} else {
super.requestDisallowInterceptTouchEvent(b);
}
}
// NestedScrollingParent
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return isEnabled() && !mReturningToStart && !mRefreshing
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
// Reset the counter of how much leftover scroll needs to be consumed.
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
// Dispatch up to the nested parent
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
mTotalUnconsumed = 0;
mNestedScrollInProgress = true;
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// If we are in the middle of consuming, a scroll, then we want to move the spinner back up
// before allowing the list to scroll
if (dy > 0 && mTotalUnconsumed > 0) {
if (dy > mTotalUnconsumed) {
consumed[1] = dy - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
if (dy > 3) {
mTotalUnconsumed -= dy;
consumed[1] = dy;
}
}
moveSpinner(mTotalUnconsumed);
}
// If a client layout is using a custom start position for the circle
// view, they mean to hide it again before scrolling the child view
// If we get back to mTotalUnconsumed == 0 and there is more to go, hide
// the circle so it isn't exposed if its blocking content is moved
if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0
&& Math.abs(dy - consumed[1]) > 0) {
mRefreshView.setVisibility(View.GONE);
}
// Now let our nested parent consume the leftovers
final int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
mNestedScrollInProgress = false;
// Finish the spinner for nested scrolling if we ever consumed any
// unconsumed nested scroll
if (mTotalUnconsumed > 0) {
finishSpinner(mTotalUnconsumed);
mTotalUnconsumed = 0;
}
// Dispatch up our nested parent
stopNestedScroll();
}
@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
// Dispatch up to the nested parent first
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);
// This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
// sometimes between two nested scrolling views, we need a way to be able to know when any
// nested scrolling parent has stopped handling events. We do that by using the
// 'offset in window 'functionality to see if we have been moved from the event.
// This is a decent indication of whether we should take over the event stream or not.
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
moveSpinner(mTotalUnconsumed);
}
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mNestedScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(
dx, dy, consumed, offsetInWindow);
}
@Override
public boolean onNestedPreFling(View target, float velocityX,
float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY,
boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
private boolean isAnimationRunning(Animation animation) {
return animation != null && animation.hasStarted() && !animation.hasEnded();
}
private void moveSpinner(float overscrollTop) {
float originalDragPercent = overscrollTop / mTotalDragDistance;
float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
float slingshotDist = mCustomSlingshotDistance > 0
? mCustomSlingshotDistance
: (mUsingCustomStart
? mSpinnerOffsetEnd - mOriginalOffsetTop
: mSpinnerOffsetEnd);
float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
/ slingshotDist);
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
(tensionSlingshotPercent / 4), 2)) * 2f;
float extraMove = (slingshotDist) * tensionPercent * 2;
int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);
// where 1.0f is a full circle
if (mRefreshView.getVisibility() != View.VISIBLE) {
mRefreshView.setVisibility(View.VISIBLE);
}
mRefreshView.setScaleX(1f);
mRefreshView.setScaleY(1f);
setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop);
}
private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {
setRefreshing(true, true /* notify */);
} else {
// cancel refresh
mRefreshing = false;
Animation.AnimationListener listener = null;
listener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
startScaleDownAnimation(null);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
};
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
int pointerIndex = -1;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
if (mIsBeingDragged) {
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
pointerIndex = ev.getActionIndex();
if (pointerIndex < 0) {
Log.e(LOG_TAG,
"Got ACTION_POINTER_DOWN event but have an invalid action index.");
return false;
}
mActivePointerId = ev.getPointerId(pointerIndex);
break;
}
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP: {
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
return false;
}
if (mIsBeingDragged) {
final float y = ev.getY(pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
finishSpinner(overscrollTop);
}
mActivePointerId = INVALID_POINTER;
return false;
}
case MotionEvent.ACTION_CANCEL:
return false;
}
return true;
}
private void startDragging(float y) {
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
}
}
private void animateOffsetToCorrectPosition(int from, AnimationListener listener) {
mFrom = from;
mAnimateToCorrectPosition.reset();
mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION);
mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);
if (listener != null) {
mRefreshView.setAnimationListener(listener);
}
mRefreshView.clearAnimation();
mRefreshView.startAnimation(mAnimateToCorrectPosition);
}
private void animateOffsetToStartPosition(int from, AnimationListener listener) {
mFrom = from;
mAnimateToStartPosition.reset();
mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION);
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
if (listener != null) {
mRefreshView.setAnimationListener(listener);
}
mRefreshView.clearAnimation();
mRefreshView.startAnimation(mAnimateToStartPosition);
}
private final Animation mAnimateToCorrectPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
int targetTop = 0;
int endTarget = 0;
if (!mUsingCustomStart) {
endTarget = mSpinnerOffsetEnd - Math.abs(mOriginalOffsetTop);
} else {
endTarget = mSpinnerOffsetEnd;
}
targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime));
int offset = targetTop - mRefreshView.getTop();
setTargetOffsetTopAndBottom(offset);
}
};
void moveToStart(float interpolatedTime) {
int targetTop = 0;
targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
int offset = targetTop - mRefreshView.getTop();
setTargetOffsetTopAndBottom(offset);
}
private final Animation mAnimateToStartPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
moveToStart(interpolatedTime);
}
};
void setTargetOffsetTopAndBottom(int offset) {
mRefreshView.bringToFront();
ViewCompat.offsetTopAndBottom(mRefreshView, offset);
mCurrentTargetOffsetTop = mRefreshView.getTop();
if (mRefreshView.getMeasuredHeight() > 0) {
mRefreshView.setProgressRotation((float) mRefreshView.getBottom() / (float) mRefreshView.getMeasuredHeight());
}
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = ev.getActionIndex();
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
/**
* Classes that wish to be notified when the swipe gesture correctly
* triggers a refresh should implement this interface.
*/
public interface OnRefreshListener {
/**
* Called when a swipe gesture triggers a refresh.
*/
void onRefresh();
}
/**
* Classes that wish to override {@link androidx.swiperefreshlayout.widget.SwipeRefreshLayout#canChildScrollUp()} method
* behavior should implement this interface.
*/
public interface OnChildScrollUpCallback {
/**
* Callback that will be called when {@link androidx.swiperefreshlayout.widget.SwipeRefreshLayout#canChildScrollUp()} method
* is called to allow the implementer to override its behavior.
*
* @param parent SwipeRefreshLayout that this callback is overriding.
* @param child The child view of SwipeRefreshLayout.
* @return Whether it is possible for the child view of parent layout to scroll up.
*/
boolean canChildScrollUp(@NonNull DoricSwipeLayout parent, @Nullable View child);
}
}

View File

@ -0,0 +1,20 @@
package pub.doric.refresh;
/**
* @Description: pub.doric.pullable
* @Author: pengfei.zhou
* @CreateDate: 2019-11-25
*/
public interface PullingListener {
void startAnimation();
void stopAnimation();
/**
* Set the amount of rotation to apply to the progress spinner.
*
* @param rotation Rotation is from [0..2]
*/
void setProgressRotation(float rotation);
}

View File

@ -0,0 +1,196 @@
package pub.doric.refresh;
import com.github.pengfeizhou.jscore.JSObject;
import com.github.pengfeizhou.jscore.JSValue;
import com.github.pengfeizhou.jscore.JavaValue;
import pub.doric.DoricContext;
import pub.doric.extension.bridge.DoricMethod;
import pub.doric.extension.bridge.DoricPlugin;
import pub.doric.extension.bridge.DoricPromise;
import pub.doric.shader.SuperNode;
import pub.doric.shader.ViewNode;
/**
* @Description: pub.doric.pullable
* @Author: pengfei.zhou
* @CreateDate: 2019-11-26
*/
@DoricPlugin(name = "Refreshable")
public class RefreshableNode extends SuperNode<DoricSwipeLayout> implements PullingListener {
private String mContentViewId;
private ViewNode mContentNode;
private String mHeaderViewId;
private ViewNode mHeaderNode;
public RefreshableNode(DoricContext doricContext) {
super(doricContext);
}
@Override
protected DoricSwipeLayout build() {
DoricSwipeLayout doricSwipeLayout = new DoricSwipeLayout(getContext());
doricSwipeLayout.getRefreshView().setPullingListenr(this);
return doricSwipeLayout;
}
@Override
protected void blend(DoricSwipeLayout view, String name, JSValue prop) {
if ("content".equals(name)) {
mContentViewId = prop.asString().value();
} else if ("header".equals(name)) {
mHeaderViewId = prop.asString().value();
} else if ("onRefresh".equals(name)) {
final String funcId = prop.asString().value();
mView.setOnRefreshListener(new DoricSwipeLayout.OnRefreshListener() {
@Override
public void onRefresh() {
callJSResponse(funcId);
}
});
} else {
super.blend(view, name, prop);
}
}
@Override
public void blend(JSObject jsObject) {
super.blend(jsObject);
blendContentNode();
blendHeadNode();
}
private void blendContentNode() {
JSObject contentModel = getSubModel(mContentViewId);
if (contentModel == null) {
return;
}
String viewId = contentModel.getProperty("id").asString().value();
String type = contentModel.getProperty("type").asString().value();
JSObject props = contentModel.getProperty("props").asObject();
if (mContentNode != null) {
if (mContentNode.getId().equals(viewId)) {
//skip
} else {
if (mReusable && type.equals(mContentNode.getType())) {
mContentNode.setId(viewId);
mContentNode.blend(props);
} else {
mView.removeAllViews();
mContentNode = ViewNode.create(getDoricContext(), type);
mContentNode.setId(viewId);
mContentNode.init(this);
mContentNode.blend(props);
mView.addView(mContentNode.getDoricLayer());
}
}
} else {
mContentNode = ViewNode.create(getDoricContext(), type);
mContentNode.setId(viewId);
mContentNode.init(this);
mContentNode.blend(props);
mView.addView(mContentNode.getDoricLayer());
}
}
private void blendHeadNode() {
JSObject headerModel = getSubModel(mHeaderViewId);
if (headerModel == null) {
return;
}
String viewId = headerModel.getProperty("id").asString().value();
String type = headerModel.getProperty("type").asString().value();
JSObject props = headerModel.getProperty("props").asObject();
if (mHeaderNode != null) {
if (mHeaderNode.getId().equals(viewId)) {
//skip
} else {
if (mReusable && type.equals(mHeaderNode.getType())) {
mHeaderNode.setId(viewId);
mHeaderNode.blend(props);
} else {
mHeaderNode = ViewNode.create(getDoricContext(), type);
mHeaderNode.setId(viewId);
mHeaderNode.init(this);
mHeaderNode.blend(props);
mView.getRefreshView().setContent(mHeaderNode.getDoricLayer());
}
}
} else {
mHeaderNode = ViewNode.create(getDoricContext(), type);
mHeaderNode.setId(viewId);
mHeaderNode.init(this);
mHeaderNode.blend(props);
mView.getRefreshView().setContent(mHeaderNode.getDoricLayer());
}
}
@Override
public ViewNode getSubNodeById(String id) {
if (id.equals(mContentViewId)) {
return mContentNode;
}
if (id.equals(mHeaderViewId)) {
return mHeaderNode;
}
return null;
}
@Override
protected void blendSubNode(JSObject subProperties) {
String viewId = subProperties.getProperty("id").asString().value();
ViewNode node = getSubNodeById(viewId);
if (node != null) {
node.blend(subProperties.getProperty("props").asObject());
}
}
@DoricMethod
public void setRefreshable(JSValue jsValue, DoricPromise doricPromise) {
boolean refreshable = jsValue.asBoolean().value();
this.mView.setEnabled(refreshable);
doricPromise.resolve();
}
@DoricMethod
public void setRefreshing(JSValue jsValue, DoricPromise doricPromise) {
boolean refreshing = jsValue.asBoolean().value();
this.mView.setRefreshing(refreshing);
doricPromise.resolve();
}
@DoricMethod
public void isRefreshable(DoricPromise doricPromise) {
doricPromise.resolve(new JavaValue(this.mView.isEnabled()));
}
@DoricMethod
public void isRefreshing(DoricPromise doricPromise) {
doricPromise.resolve(new JavaValue(this.mView.isRefreshing()));
}
@Override
public void startAnimation() {
if (mHeaderNode != null) {
mHeaderNode.callJSResponse("startAnimation");
}
}
@Override
public void stopAnimation() {
if (mHeaderNode != null) {
mHeaderNode.callJSResponse("stopAnimation");
}
}
@Override
public void setProgressRotation(float rotation) {
if (mHeaderNode != null) {
mHeaderNode.callJSResponse("setProgressRotation", rotation);
}
}
}

View File

@ -34,6 +34,7 @@ import com.bumptech.glide.request.target.Target;
import pub.doric.DoricContext;
import pub.doric.extension.bridge.DoricPlugin;
import pub.doric.utils.DoricUtils;
import com.github.pengfeizhou.jscore.JSONBuilder;
import com.github.pengfeizhou.jscore.JSValue;
@ -77,8 +78,8 @@ public class ImageNode extends ViewNode<ImageView> {
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
if (!TextUtils.isEmpty(loadCallbackId)) {
callJSResponse(loadCallbackId, new JSONBuilder()
.put("width", resource.getIntrinsicWidth())
.put("height", resource.getIntrinsicHeight())
.put("width", DoricUtils.px2dp(resource.getIntrinsicWidth()))
.put("height", DoricUtils.px2dp(resource.getIntrinsicHeight()))
.toJSONObject());
}
return false;

View File

@ -257,4 +257,21 @@ public abstract class ViewNode<T extends View> extends DoricContextHolder {
public int getHeight() {
return mView.getHeight();
}
@DoricMethod
public void setRotation(JSValue jsValue) {
float rotation = jsValue.asNumber().toFloat();
while (rotation > 1) {
rotation = rotation - 1;
}
while (rotation < -1) {
rotation = rotation + 1;
}
doricLayer.setRotation(rotation * 360);
}
@DoricMethod
public float getRotation() {
return doricLayer.getRotation() / 360;
}
}

View File

@ -31,6 +31,7 @@ public class DoricConstant {
public static final String INJECT_TIMER_SET = "nativeSetTimer";
public static final String INJECT_TIMER_CLEAR = "nativeClearTimer";
public static final String INJECT_BRIDGE = "nativeBridge";
public static final String INJECT_EMPTY = "nativeEmpty";
public static final String TEMPLATE_CONTEXT_CREATE = "Reflect.apply(" +
"function(doric,context,Entry,require,exports){" + "\n" +

View File

@ -81,6 +81,8 @@ public class DoricUtils {
return new JavaValue((Integer) arg);
} else if (arg instanceof Long) {
return new JavaValue((Long) arg);
} else if (arg instanceof Float) {
return new JavaValue((Float) arg);
} else if (arg instanceof Double) {
return new JavaValue((Double) arg);
} else if (arg instanceof Boolean) {

View File

@ -12,4 +12,5 @@ export default [
'src/StorageDemo',
'src/NavigatorDemo',
'src/NavbarDemo',
'src/RefreshableDemo',
]

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
import { Group, Panel, Text, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, slider, slideItem, scroller, IVLayout, IHLayout } from "doric";
import { Group, Panel, Text, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, slider, slideItem, scroller, IVLayout, IHLayout, layoutConfig } from "doric";
import { O_TRUNC } from "constants";
const colors = [
@ -453,11 +453,7 @@ class LayoutDemo extends Panel {
it.space = 20
}),
).also(it => {
it.layoutConfig = {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
alignment: gravity().centerX(),
}
it.layoutConfig = layoutConfig().atmost()
})
.in(rootView)
}

View File

@ -1,15 +1,10 @@
import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, Text } from "doric";
const colors = [
"#f0932b",
"#eb4d4b",
"#6ab04c",
"#e056fd",
"#686de0",
"#30336b",
]
import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, Text, refreshable, Refreshable, ListItem } from "doric";
import { rotatedArrow, colors } from "./utils";
@Entry
class ListPanel extends Panel {
build(rootView: Group): void {
let refreshView: Refreshable
let offset = Math.ceil(Math.random() * colors.length)
vlayout([
text({
text: "ListDemo",
@ -23,76 +18,90 @@ class ListPanel extends Panel {
textAlignment: gravity().center(),
height: 50,
}),
list({
itemCount: 1000,
renderItem: (idx: number) => {
let counter!: Text
return listItem(
hlayout([
text({
layoutConfig: {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.EXACTLY,
alignment: gravity().center(),
},
text: `Cell At Line ${idx}`,
textAlignment: gravity().center(),
textColor: Color.parse("#ffffff"),
textSize: 20,
height: 50,
}),
text({
textColor: Color.parse("#ffffff"),
textSize: 20,
text: "",
}).also(it => {
counter = it
it.layoutConfig = {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
margin: {
left: 10,
refreshView = refreshable({
onRefresh: () => {
refreshView.setRefreshing(context, false).then(() => {
(refreshView.content as List).also(it => {
it.reset()
offset = Math.ceil(Math.random() * colors.length)
it.itemCount = 40
it.renderItem = (idx: number) => {
let counter!: Text
return listItem(
hlayout([
text({
layoutConfig: {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.EXACTLY,
alignment: gravity().center(),
},
text: `Cell At Line ${idx}`,
textAlignment: gravity().center(),
textColor: Color.parse("#ffffff"),
textSize: 20,
height: 50,
}),
text({
textColor: Color.parse("#ffffff"),
textSize: 20,
text: "",
}).also(it => {
counter = it
it.layoutConfig = {
widthSpec: LayoutSpec.WRAP_CONTENT,
heightSpec: LayoutSpec.WRAP_CONTENT,
margin: {
left: 10,
}
}
})
]).also(it => {
it.layoutConfig = {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.WRAP_CONTENT,
margin: {
bottom: 2,
}
}
it.gravity = gravity().center()
it.bgColor = colors[(idx + offset) % colors.length]
let clicked = 0
it.onClick = () => {
counter.text = `Item Clicked ${++clicked}`
}
})
).also(it => {
it.layoutConfig = {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.WRAP_CONTENT,
}
}
})
]).also(it => {
it.layoutConfig = {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.WRAP_CONTENT,
margin: {
bottom: 2,
}
}
it.gravity = gravity().center()
it.bgColor = Color.parse(colors[idx % colors.length])
let clicked = 0
it.onClick = () => {
counter.text = `Item Clicked ${++clicked}`
it.onClick = () => {
log(`Click item at ${idx}`)
it.height += 10
it.nativeChannel(context, "getWidth")().then(
resolve => {
log(`resolve,${resolve}`)
},
reject => {
log(`reject,${reject}`)
})
}
})
}
})
).also(it => {
it.layoutConfig = {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.WRAP_CONTENT,
}
it.onClick = () => {
log(`Click item at ${idx}`)
it.height += 10
it.nativeChannel(context, "getWidth")().then(
resolve => {
log(`resolve,${resolve}`)
},
reject => {
log(`reject,${reject}`)
})
}
})
},
layoutConfig: {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.AT_MOST,
},
header: rotatedArrow(context),
content: list({
itemCount: 0,
renderItem: () => new ListItem,
layoutConfig: {
widthSpec: LayoutSpec.AT_MOST,
heightSpec: LayoutSpec.AT_MOST,
},
}),
}),
]).also(it => {
it.layoutConfig = {
widthSpec: LayoutSpec.AT_MOST,
@ -100,5 +109,6 @@ class ListPanel extends Panel {
}
it.bgColor = Color.WHITE
}).in(rootView)
refreshView.bgColor = Color.YELLOW
}
}

109
demo/src/RefreshableDemo.ts Normal file
View File

@ -0,0 +1,109 @@
import { refreshable, Group, Panel, pullable, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, Text, scroller, layoutConfig, image, IView, IVLayout, ScaleType, modal, IText, network, navigator, stack, Image } from "doric";
import { title, label, colors, icon_refresh } from "./utils";
@Entry
class RefreshableDemo extends Panel {
build(rootView: Group): void {
let refreshImage: Image
let refreshView = refreshable({
layoutConfig: layoutConfig().atmost(),
onRefresh: () => {
log('onRefresh')
setTimeout(() => {
refreshView.setRefreshing(context, false)
}, 5000)
},
header: pullable(context,
stack([
image({
layoutConfig: layoutConfig().exactly().m({ top: 50, bottom: 10, }),
width: 30,
height: 30,
imageBase64: icon_refresh,
}).also(v => {
refreshImage = v
}),
]),
{
startAnimation: () => {
log('startAnimation')
},
stopAnimation: () => {
log('stopAnimation')
},
setProgressRotation: (rotation: number) => {
refreshImage.setRotation(context, rotation)
},
}),
content: scroller(vlayout([
title("Refreshable Demo"),
label('start Refresh').apply({
width: 300,
height: 50,
bgColor: colors[0],
textSize: 30,
textColor: Color.WHITE,
layoutConfig: layoutConfig().exactly(),
onClick: () => {
refreshView.setRefreshing(context, true)
}
} as IText),
label('stop Refresh').apply({
width: 300,
height: 50,
bgColor: colors[0],
textSize: 30,
textColor: Color.WHITE,
layoutConfig: layoutConfig().exactly(),
onClick: () => {
refreshView.setRefreshing(context, false)
}
} as IText),
label('Enable Refresh').apply({
width: 300,
height: 50,
bgColor: colors[0],
textSize: 30,
textColor: Color.WHITE,
layoutConfig: layoutConfig().exactly(),
onClick: () => {
refreshView.setRefreshable(context, true)
}
} as IText),
label('Disable Refresh').apply({
width: 300,
height: 50,
bgColor: colors[0],
textSize: 30,
textColor: Color.WHITE,
layoutConfig: layoutConfig().exactly(),
onClick: () => {
refreshView.setRefreshable(context, false)
}
} as IText),
label('Rotate self').apply({
width: 300,
height: 50,
bgColor: colors[0],
textSize: 30,
textColor: Color.WHITE,
layoutConfig: layoutConfig().exactly(),
} as IText).also(v => {
v.onClick = () => {
v.nativeChannel(context, "setRotation")(0.25)
}
}),
]).apply({
layoutConfig: layoutConfig().atmost().h(LayoutSpec.WRAP_CONTENT),
gravity: gravity().center(),
space: 10,
} as IVLayout)).apply({
layoutConfig: layoutConfig().atmost(),
})
}).apply({
bgColor: Color.YELLOW
}).in(rootView)
}
}

View File

@ -1,59 +1,34 @@
import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, scroller } from "doric";
const colors = [
"#f0932b",
"#eb4d4b",
"#6ab04c",
"#e056fd",
"#686de0",
"#30336b",
]
import { Group, Panel, List, text, gravity, Color, Stack, LayoutSpec, list, NativeCall, listItem, log, vlayout, Gravity, hlayout, scroller, layoutConfig } from "doric";
import { label } from "./utils";
@Entry
class ScrollerPanel extends Panel {
build(rootView: Group): void {
rootView.addChild(scroller(vlayout(
[
// ...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5].map(e => text({
// text: colors[e % colors.length],
// textColor: Color.parse('#ffffff'),
// textSize: 20,
// bgColor: Color.parse(colors[e % colors.length]),
// layoutConfig: {
// widthSpec: LayoutSpec.EXACTLY,
// heightSpec: LayoutSpec.EXACTLY,
// },
// width: 200,
// height: 50,
// })),
...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5].map(i => hlayout([
...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5].map(j => text({
text: colors[(i + j) % colors.length],
textColor: Color.parse('#ffffff'),
textSize: 20,
bgColor: Color.parse(colors[(i + j) % colors.length]),
layoutConfig: {
widthSpec: LayoutSpec.EXACTLY,
heightSpec: LayoutSpec.EXACTLY,
},
width: 80,
height: 50,
})),
]).also(it => it.space = 20)),
hlayout([
...[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5].map(e => text({
text: colors[e % colors.length],
textColor: Color.parse('#ffffff'),
textSize: 20,
bgColor: Color.parse(colors[e % colors.length]),
layoutConfig: {
widthSpec: LayoutSpec.EXACTLY,
heightSpec: LayoutSpec.EXACTLY,
},
width: 200,
height: 50,
})),
]
),
]
).also(it => it.space = 20)))
scroller(
vlayout([
scroller(
vlayout(new Array(100).fill(1).map(e => label('Scroll Content')))
).apply({
layoutConfig: layoutConfig().exactly(),
width: 300,
height: 500,
bgColor: Color.RED,
}),
scroller(
vlayout(new Array(100).fill(1).map(e => label('Scroll Content')))
).apply({
layoutConfig: layoutConfig().exactly(),
width: 300,
height: 500,
bgColor: Color.BLUE,
})
])
)
.apply({
layoutConfig: layoutConfig().atmost().h(LayoutSpec.EXACTLY),
height: 500,
bgColor: Color.YELLOW,
})
.in(rootView)
}
}

2
demo/src/image_base64.ts Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
// "incremental": true, /* Enable incremental compilation */
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
"lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */

View File

@ -39,8 +39,9 @@ TODO: Add long description of the pod here.
s.public_header_files = 'Pod/Classes/**/*.h'
# s.frameworks = 'UIKit', 'MapKit'
# s.dependency 'AFNetworking', '~> 2.3'
s.dependency 'SDWebImage', '~> 4.4.7'
s.dependency 'SDWebImage/WebP'
# s.dependency 'SDWebImage', '~> 5.0'
s.dependency 'YYWebImage', '~>1.0.5'
s.dependency 'YYImage/WebP'
s.dependency 'SocketRocket', '~> 0.5.1'
s.dependency 'GCDWebServer', '~> 3.0'
s.dependency 'YYCache', '~> 1.0.4'

View File

@ -1,30 +1,23 @@
PODS:
- Doric (0.1.0):
- GCDWebServer (~> 3.0)
- SDWebImage (~> 4.4.7)
- SDWebImage/WebP
- SocketRocket (~> 0.5.1)
- YYCache (~> 1.0.4)
- YYImage/WebP
- YYWebImage
- GCDWebServer (3.5.3):
- GCDWebServer/Core (= 3.5.3)
- GCDWebServer/Core (3.5.3)
- libwebp (1.0.3):
- libwebp/demux (= 1.0.3)
- libwebp/mux (= 1.0.3)
- libwebp/webp (= 1.0.3)
- libwebp/demux (1.0.3):
- libwebp/webp
- libwebp/mux (1.0.3):
- libwebp/demux
- libwebp/webp (1.0.3)
- SDWebImage (4.4.7):
- SDWebImage/Core (= 4.4.7)
- SDWebImage/Core (4.4.7)
- SDWebImage/WebP (4.4.7):
- libwebp (< 2.0, >= 0.5)
- SDWebImage/Core
- SocketRocket (0.5.1)
- YYCache (1.0.4)
- YYImage (1.0.4):
- YYImage/Core (= 1.0.4)
- YYImage/Core (1.0.4)
- YYImage/WebP (1.0.4):
- YYImage/Core
- YYWebImage (1.0.5):
- YYCache
- YYImage
DEPENDENCIES:
- Doric (from `../`)
@ -32,22 +25,22 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/cocoapods/specs.git:
- GCDWebServer
- libwebp
- SDWebImage
- SocketRocket
- YYCache
- YYImage
- YYWebImage
EXTERNAL SOURCES:
Doric:
:path: "../"
SPEC CHECKSUMS:
Doric: c71287d68afeeb79bfd3c680ed2dd3b90d515c12
Doric: e73b17b0e46198994f5c3d8af49f26fd9f49df09
GCDWebServer: c0ab22c73e1b84f358d1e2f74bf6afd1c60829f2
libwebp: 057912d6d0abfb6357d8bb05c0ea470301f5d61e
SDWebImage: c10d14a8883ebd89664f02a422006f66a85c0c5d
SocketRocket: d57c7159b83c3c6655745cd15302aa24b6bae531
YYCache: 8105b6638f5e849296c71f331ff83891a4942952
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
YYWebImage: 5f7f36aee2ae293f016d418c7d6ba05c4863e928
PODFILE CHECKSUM: 012563d71439e7e33e976dca3b59664ed56cee39

View File

@ -24,4 +24,5 @@
#import "DoricJSLoaderManager.h"
#import "DoricNavigatorDelegate.h"
#import "DoricNavBarDelegate.h"
#import "DoricViewController.h"
#import "DoricViewController.h"
#import "DoricPromise.h"

View File

@ -37,6 +37,7 @@
#import "DoricStoragePlugin.h"
#import "DoricNavigatorPlugin.h"
#import "DoricNavBarPlugin.h"
#import "DoricRefreshableNode.h"
@interface DoricRegistry ()
@ -76,6 +77,7 @@ - (void)innerRegister {
[self registerViewNode:DoricScrollerNode.class withName:@"Scroller"];
[self registerViewNode:DoricSliderNode.class withName:@"Slider"];
[self registerViewNode:DoricSlideItemNode.class withName:@"SlideItem"];
[self registerViewNode:DoricRefreshableNode.class withName:@"Refreshable"];
}
- (void)registerJSBundle:(NSString *)bundle withName:(NSString *)name {

View File

@ -59,7 +59,9 @@ - (void)initJSExecutor {
[self.jsExecutor injectGlobalJSObject:INJECT_LOG obj:^(NSString *type, NSString *message) {
DoricLog(@"JS:%@", message);
}];
[self.jsExecutor injectGlobalJSObject:INJECT_EMPTY obj:^() {
}];
[self.jsExecutor injectGlobalJSObject:INJECT_REQUIRE obj:^(NSString *name) {
__strong typeof(_self) self = _self;
if (!self) return NO;

View File

@ -25,8 +25,6 @@
#import "DoricUtil.h"
#import "Doric.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import <objc/runtime.h>
@implementation DoricShaderPlugin
@ -110,9 +108,13 @@ - (id)findClass:(Class)clz target:(id)target method:(NSString *)name promise:(Do
dispatch_async(dispatch_get_main_queue(), ^{
void *retValue;
block();
[invocation getReturnValue:&retValue];
id returnValue = (__bridge id) retValue;
[promise resolve:returnValue];
const char *retType = methodSignature.methodReturnType;
if (!strcmp(retType, @encode(void))) {
} else {
[invocation getReturnValue:&retValue];
id returnValue = (__bridge id) retValue;
[promise resolve:returnValue];
}
});
return ret;
}

View File

@ -0,0 +1,10 @@
//
// Created by pengfei.zhou on 2019/11/26.
//
#import <Foundation/Foundation.h>
#import "DoricSuperNode.h"
#import "DoricSwipeRefreshLayout.h"
@interface DoricRefreshableNode : DoricSuperNode<DoricSwipeRefreshLayout *>
@end

View File

@ -0,0 +1,161 @@
//
// Created by pengfei.zhou on 2019/11/26.
//
#import "DoricRefreshableNode.h"
#import "Doric.h"
@interface DoricRefreshableNode () <DoricSwipePullingDelegate>
@property(nonatomic, strong) DoricViewNode *contentNode;
@property(nonatomic, copy) NSString *contentViewId;
@property(nonatomic, strong) DoricViewNode *headerNode;
@property(nonatomic, copy) NSString *headerViewId;
@end
@implementation DoricRefreshableNode
- (DoricSwipeRefreshLayout *)build {
return [[DoricSwipeRefreshLayout new] also:^(DoricSwipeRefreshLayout *it) {
it.swipePullingDelegate = self;
}];
}
- (void)blendView:(DoricSwipeRefreshLayout *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"content" isEqualToString:name]) {
self.contentViewId = prop;
} else if ([@"header" isEqualToString:name]) {
self.headerViewId = prop;
} else if ([@"onRefresh" isEqualToString:name]) {
__weak typeof(self) _self = self;
NSString *funcId = prop;
self.view.onRefreshBlock = ^{
__strong typeof(_self) self = _self;
[self callJSResponse:funcId, nil];
};
} else {
[super blendView:view forPropName:name propValue:prop];
}
}
- (DoricViewNode *)subNodeWithViewId:(NSString *)viewId {
if ([viewId isEqualToString:self.contentViewId]) {
return self.contentNode;
} else if ([viewId isEqualToString:self.headerViewId]) {
return self.headerNode;
} else {
return nil;
}
}
- (void)blend:(NSDictionary *)props {
[super blend:props];
[self blendHeader];
[self blendContent];
}
- (void)blendContent {
NSDictionary *contentModel = [self subModelOf:self.contentViewId];
if (!contentModel) {
return;
}
NSString *viewId = contentModel[@"id"];
NSString *type = contentModel[@"type"];
NSDictionary *childProps = contentModel[@"props"];
if (self.contentNode) {
if ([self.contentNode.viewId isEqualToString:viewId]) {
//skip
} else {
if (self.reusable && [type isEqualToString:self.contentNode.type]) {
[self.contentNode also:^(DoricViewNode *it) {
it.viewId = viewId;
[it blend:childProps];
}];
} else {
self.contentNode = [[DoricViewNode create:self.doricContext withType:type] also:^(DoricViewNode *it) {
it.viewId = viewId;
[it initWithSuperNode:self];
[it blend:childProps];
self.view.contentView = it.view;
}];
}
}
} else {
self.contentNode = [[DoricViewNode create:self.doricContext withType:type] also:^(DoricViewNode *it) {
it.viewId = viewId;
[it initWithSuperNode:self];
[it blend:childProps];
self.view.contentView = it.view;
}];
}
}
- (void)blendHeader {
NSDictionary *headerModel = [self subModelOf:self.headerViewId];
if (!headerModel) {
return;
}
NSString *viewId = headerModel[@"id"];
NSString *type = headerModel[@"type"];
NSDictionary *childProps = headerModel[@"props"];
if (self.headerNode) {
if ([self.headerNode.viewId isEqualToString:viewId]) {
//skip
} else {
if (self.reusable && [type isEqualToString:self.headerNode.type]) {
[self.headerNode also:^(DoricViewNode *it) {
it.viewId = viewId;
[it blend:childProps];
}];
} else {
self.headerNode = [[DoricViewNode create:self.doricContext withType:type] also:^(DoricViewNode *it) {
it.viewId = viewId;
[it initWithSuperNode:self];
[it blend:childProps];
self.view.headerView = it.view;
}];
}
}
} else {
self.headerNode = [[DoricViewNode create:self.doricContext withType:type] also:^(DoricViewNode *it) {
it.viewId = viewId;
[it initWithSuperNode:self];
[it blend:childProps];
self.view.headerView = it.view;
}];
}
}
- (void)blendSubNode:(NSDictionary *)subModel {
[[self subNodeWithViewId:subModel[@"id"]] blend:subModel[@"props"]];
}
- (void)startAnimation {
[self.headerNode callJSResponse:@"startAnimation", nil];
}
- (void)stopAnimation {
[self.headerNode callJSResponse:@"stopAnimation", nil];
}
- (void)setProgressRotation:(CGFloat)rotation {
[self.headerNode callJSResponse:@"setProgressRotation", @(rotation), nil];
}
- (void)setRefreshing:(NSNumber *)refreshable withPromise:(DoricPromise *)promise {
self.view.refreshing = [refreshable boolValue];
[promise resolve:nil];
}
- (void)setRefreshable:(NSNumber *)refreshing withPromise:(DoricPromise *)promise {
self.view.refreshable = [refreshing boolValue];
[promise resolve:nil];
}
- (NSNumber *)isRefreshing {
return @(self.view.refreshing);
}
- (NSNumber *)isRefreshable {
return @(self.view.refreshable);
}
@end

View File

@ -0,0 +1,22 @@
//
// Created by pengfei.zhou on 2019/11/26.
//
#import <Foundation/Foundation.h>
@protocol DoricSwipePullingDelegate <NSObject>
- (void)startAnimation;
- (void)stopAnimation;
- (void)setProgressRotation:(CGFloat)rotation;
@end
@interface DoricSwipeRefreshLayout : UIScrollView
@property(nonatomic, strong) UIView *contentView;
@property(nonatomic, strong) UIView *headerView;
@property(nonatomic, assign) BOOL refreshable;
@property(nonatomic, assign) BOOL refreshing;
@property(nonatomic, strong) void (^onRefreshBlock)(void);
@property(nonatomic, weak) id <DoricSwipePullingDelegate> swipePullingDelegate;
@end

View File

@ -0,0 +1,146 @@
//
// Created by pengfei.zhou on 2019/11/26.
//
#import "DoricSwipeRefreshLayout.h"
#import "UIView+Doric.h"
#import "DoricLayouts.h"
@interface DoricSwipeRefreshLayout () <UIScrollViewDelegate>
@end
@implementation DoricSwipeRefreshLayout
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.showsHorizontalScrollIndicator = NO;
self.showsVerticalScrollIndicator = NO;
self.alwaysBounceVertical = YES;
self.delegate = self;
if (@available(iOS 11, *)) {
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
return self;
}
- (instancetype)init {
if (self = [super init]) {
self.showsHorizontalScrollIndicator = NO;
self.showsVerticalScrollIndicator = NO;
self.alwaysBounceVertical = YES;
self.delegate = self;
if (@available(iOS 11, *)) {
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
return self;
}
- (CGSize)sizeThatFits:(CGSize)size {
if (self.contentView) {
return [self.contentView measureSize:size];
}
return CGSizeZero;
}
- (BOOL)requestFromSubview:(UIView *)subview {
if (subview == self.headerView) {
return NO;
}
return [super requestFromSubview:subview];
}
- (void)layoutSelf:(CGSize)targetSize {
[super layoutSelf:targetSize];
self.headerView.bottom = 0;
self.headerView.centerX = self.centerX;
self.contentSize = self.frame.size;
}
- (void)setContentView:(UIView *)contentView {
if (_contentView) {
[_contentView removeFromSuperview];
}
_contentView = contentView;
[self addSubview:contentView];
}
- (void)setHeaderView:(UIView *)headerView {
if (_headerView) {
[_headerView removeFromSuperview];
}
_headerView = headerView;
[self addSubview:headerView];
self.refreshable = YES;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (-scrollView.contentOffset.y >= self.headerView.height) {
dispatch_async(dispatch_get_main_queue(), ^{
self.refreshing = YES;
});
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.y <= 0) {
[self.swipePullingDelegate setProgressRotation:-scrollView.contentOffset.y / self.headerView.height];
}
}
- (void)setContentOffset:(CGPoint)contentOffset {
[super setContentOffset:contentOffset];
}
- (void)setRefreshing:(BOOL)refreshing {
if (_refreshing == refreshing) {
return;
}
if (refreshing) {
if (self.onRefreshBlock) {
self.onRefreshBlock();
}
[UIView animateWithDuration:.3f
animations:^{
self.contentOffset = (CGPoint) {0, -self.headerView.height};
self.contentInset = UIEdgeInsetsMake(self.headerView.height, 0, 0, 0);
}
completion:^(BOOL finished) {
[self.swipePullingDelegate startAnimation];
self.scrollEnabled = NO;
}
];
} else {
self.bounces = YES;
[UIView animateWithDuration:.3f
animations:^{
self.contentOffset = (CGPoint) {0, 0};
self.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
}
completion:^(BOOL finished) {
[self.swipePullingDelegate stopAnimation];
self.scrollEnabled = YES;
}
];
}
_refreshing = refreshing;
}
- (void)setRefreshable:(BOOL)refreshable {
self.scrollEnabled = refreshable;
if (refreshable) {
self.contentOffset = (CGPoint) {0, 0};
self.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
}
}
- (BOOL)refreshable {
return self.scrollEnabled;
}
- (void)setContentSize:(CGSize)contentSize {
[super setContentSize:contentSize];
}
@end

View File

@ -22,7 +22,7 @@
#import "DoricImageNode.h"
#import "Doric.h"
#import <SDWebImage/UIImageView+WebCache.h>
#import "YYWebImage.h"
@interface DoricImageNode ()
@property(nonatomic, copy) NSString *loadCallbackId;
@ -31,7 +31,7 @@ @interface DoricImageNode ()
@implementation DoricImageNode
- (UIImageView *)build {
return [[UIImageView new] also:^(UIImageView *it) {
return [[YYAnimatedImageView new] also:^(UIImageView *it) {
it.clipsToBounds = YES;
}];
}
@ -39,7 +39,7 @@ - (UIImageView *)build {
- (void)blendView:(UIImageView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"imageUrl" isEqualToString:name]) {
__weak typeof(self) _self = self;
[view sd_setImageWithURL:[NSURL URLWithString:prop] completed:^(UIImage *_Nullable image, NSError *_Nullable error, SDImageCacheType cacheType, NSURL *_Nullable imageURL) {
[view yy_setImageWithURL:[NSURL URLWithString:prop] placeholder:nil options:0 completion:^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError *error) {
__strong typeof(_self) self = _self;
if (error) {
if (self.loadCallbackId.length > 0) {
@ -53,7 +53,6 @@ - (void)blendView:(UIImageView *)view forPropName:(NSString *)name propValue:(id
}
[self requestLayout];
}
}];
} else if ([@"scaleType" isEqualToString:name]) {
switch ([prop integerValue]) {

View File

@ -19,14 +19,7 @@
#import <Foundation/Foundation.h>
struct DoricMargin {
CGFloat left;
CGFloat right;
CGFloat top;
CGFloat bottom;
};
typedef struct DoricMargin DoricMargin;
typedef UIEdgeInsets DoricMargin;
DoricMargin DoricMarginMake(CGFloat left, CGFloat top, CGFloat right, CGFloat bottom);
@ -68,17 +61,13 @@ typedef NS_ENUM(NSInteger, DoricGravity) {
@interface DoricLayoutContainer : UIView
@property(nonatomic, assign) DoricGravity gravity;
- (void)layout:(CGSize)targetSize;
- (CGSize)sizeContent:(CGSize)size;
@end
@interface DoricStackView : DoricLayoutContainer
@end
@interface DoricLinearView : DoricLayoutContainer
@property(nonatomic, assign) DoricGravity gravity;
@property(nonatomic, assign) CGFloat space;
@end
@ -91,7 +80,20 @@ typedef NS_ENUM(NSInteger, DoricGravity) {
@interface UIView (DoricLayoutConfig)
@property(nonatomic, strong) DoricLayoutConfig *layoutConfig;
@end
@interface UIView (DoricTag)
@property(nonatomic, copy) NSString *tagString;
- (UIView *)viewWithTagString:(NSString *)tagString;
@end
@interface UIView (DoricLayouts)
- (void)layoutSelf:(CGSize)targetSize;
- (CGSize)measureSize:(CGSize)targetSize;
- (void)doricLayoutSubviews;
- (BOOL)requestFromSubview:(UIView *)subview;
@end

View File

@ -19,8 +19,107 @@
#import "DoricLayouts.h"
#import <objc/runtime.h>
#import <Doric/DoricLayouts.h>
#import "UIView+Doric.h"
static const void *kLayoutConfig = &kLayoutConfig;
@implementation UIView (DoricLayoutConfig)
@dynamic layoutConfig;
- (void)setLayoutConfig:(DoricLayoutConfig *)layoutConfig {
objc_setAssociatedObject(self, kLayoutConfig, layoutConfig, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (DoricLayoutConfig *)layoutConfig {
return objc_getAssociatedObject(self, kLayoutConfig);
}
@end
static const void *kTagString = &kTagString;
@implementation UIView (DoricTag)
- (void)setTagString:(NSString *)tagString {
objc_setAssociatedObject(self, kTagString, tagString, OBJC_ASSOCIATION_COPY_NONATOMIC);
self.tag = [tagString hash];
}
- (NSString *)tagString {
return objc_getAssociatedObject(self, kTagString);
}
- (UIView *)viewWithTagString:(NSString *)tagString {
// notice the potential hash collision
return [self viewWithTag:[tagString hash]];
}
@end
@implementation UIView (DoricLayouts)
/**
* Measure self's size
* */
- (CGSize)measureSize:(CGSize)targetSize {
CGFloat width = self.width;
CGFloat height = self.height;
DoricLayoutConfig *config = self.layoutConfig;
if (!config) {
config = [DoricLayoutConfig new];
}
if (config.widthSpec == DoricLayoutAtMost
|| config.widthSpec == DoricLayoutWrapContent) {
width = targetSize.width - config.margin.left - config.margin.right;
}
if (config.heightSpec == DoricLayoutAtMost
|| config.heightSpec == DoricLayoutWrapContent) {
height = targetSize.height - config.margin.top - config.margin.bottom;
}
CGSize contentSize = [self sizeThatFits:CGSizeMake(width, height)];
if (config.widthSpec == DoricLayoutWrapContent) {
width = contentSize.width;
}
if (config.heightSpec == DoricLayoutWrapContent) {
height = contentSize.height;
}
return CGSizeMake(width, height);
}
/**
* layout self and subviews
* */
- (void)layoutSelf:(CGSize)targetSize {
self.width = targetSize.width;
self.height = targetSize.height;
for (UIView *view in self.subviews) {
[view layoutSelf:[view measureSize:targetSize]];
}
}
- (void)doricLayoutSubviews {
if ([self.superview requestFromSubview:self]) {
[self.superview doricLayoutSubviews];
} else {
[self layoutSelf:CGSizeMake(self.width, self.height)];
}
}
- (BOOL)requestFromSubview:(UIView *)subview {
if (self.layoutConfig
&& self.layoutConfig.widthSpec != DoricLayoutExact
&& self.layoutConfig.heightSpec != DoricLayoutExact) {
return YES;
}
return NO;
}
@end
DoricMargin DoricMarginMake(CGFloat left, CGFloat top, CGFloat right, CGFloat bottom) {
DoricMargin margin;
margin.left = left;
@ -65,63 +164,23 @@ @interface DoricLayoutContainer ()
@end
@implementation DoricLayoutContainer
- (void)setNeedsLayout {
[super setNeedsLayout];
}
- (void)layoutSubviews {
if ([self.superview isKindOfClass:[DoricLayoutContainer class]]) {
[self.superview layoutSubviews];
} else {
CGSize size = [self sizeThatFits:CGSizeMake(self.superview.width, self.superview.height)];
[self layout:size];
}
}
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat width = self.width;
CGFloat height = self.height;
DoricLayoutConfig *config = self.layoutConfig;
if (!config) {
config = [DoricLayoutConfig new];
}
if (config.widthSpec == DoricLayoutAtMost
|| config.widthSpec == DoricLayoutWrapContent) {
width = size.width - config.margin.left - config.margin.right;
}
if (config.heightSpec == DoricLayoutAtMost
|| config.heightSpec == DoricLayoutWrapContent) {
height = size.height - config.margin.top - config.margin.bottom;
}
CGSize contentSize = [self sizeContent:CGSizeMake(width, height)];
if (config.widthSpec == DoricLayoutWrapContent) {
width = contentSize.width;
}
if (config.heightSpec == DoricLayoutWrapContent) {
height = contentSize.height;
}
return CGSizeMake(width, height);
}
- (CGSize)sizeContent:(CGSize)size {
return size;
}
- (void)layout:(CGSize)targetSize {
self.width = targetSize.width;
self.height = targetSize.height;
[super layoutSubviews];
[self doricLayoutSubviews];
}
@end
@interface DoricStackView ()
@property(nonatomic, assign) CGFloat contentWidth;
@property(nonatomic, assign) CGFloat contentHeight;
@end
@implementation DoricStackView
- (CGSize)sizeContent:(CGSize)size {
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat contentWidth = 0;
CGFloat contentHeight = 0;
for (UIView *child in self.subviews) {
@ -132,24 +191,11 @@ - (CGSize)sizeContent:(CGSize)size {
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize childSize = CGSizeMake(child.width, child.height);
if ([child isKindOfClass:[DoricLayoutContainer class]]
|| childConfig.widthSpec == DoricLayoutWrapContent
|| childConfig.heightSpec == DoricLayoutWrapContent) {
childSize = [child sizeThatFits:CGSizeMake(size.width, size.height - contentHeight)];
}
if (childConfig.widthSpec == DoricLayoutExact) {
childSize.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
childSize.width = size.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
childSize.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
childSize.height = size.height - contentHeight;
}
if (childConfig.weight) {
childSize.height = child.height;
CGSize childSize;
if (CGAffineTransformEqualToTransform(child.transform, CGAffineTransformIdentity)) {
childSize = [child measureSize:CGSizeMake(size.width, size.height)];
} else {
childSize = child.bounds.size;
}
contentWidth = MAX(contentWidth, childSize.width + childConfig.margin.left + childConfig.margin.right);
contentHeight = MAX(contentHeight, childSize.height + childConfig.margin.top + childConfig.margin.bottom);
@ -159,38 +205,23 @@ - (CGSize)sizeContent:(CGSize)size {
return CGSizeMake(contentWidth, contentHeight);
}
- (void)layout:(CGSize)targetSize {
- (void)layoutSelf:(CGSize)targetSize {
self.width = targetSize.width;
self.height = targetSize.height;
for (UIView *child in self.subviews) {
if (child.isHidden) {
continue;
}
if (!CGAffineTransformEqualToTransform(child.transform, CGAffineTransformIdentity)) {
continue;
}
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize size = [child sizeThatFits:CGSizeMake(targetSize.width, targetSize.height)];
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
}
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
size.width = targetSize.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
size.height = targetSize.height;
}
child.width = size.width;
child.height = size.height;
DoricGravity gravity = childConfig.alignment | self.gravity;
CGSize size = [child measureSize:CGSizeMake(targetSize.width, targetSize.height)];
[child layoutSelf:size];
DoricGravity gravity = childConfig.alignment;
if ((gravity & LEFT) == LEFT) {
child.left = 0;
} else if ((gravity & RIGHT) == RIGHT) {
@ -204,7 +235,6 @@ - (void)layout:(CGSize)targetSize {
child.right = targetSize.width - childConfig.margin.right;
}
}
if ((gravity & TOP) == TOP) {
child.top = 0;
} else if ((gravity & BOTTOM) == BOTTOM) {
@ -218,13 +248,7 @@ - (void)layout:(CGSize)targetSize {
child.bottom = targetSize.height - childConfig.margin.bottom;
}
}
if ([child isKindOfClass:[DoricLayoutContainer class]]) {
[(DoricLayoutContainer *) child layout:size];
}
}
self.width = targetSize.width;
self.height = targetSize.height;
}
@end
@ -232,7 +256,8 @@ @implementation DoricLinearView
@end
@implementation DoricVLayoutView
- (CGSize)sizeContent:(CGSize)size {
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat contentWidth = 0;
CGFloat contentHeight = 0;
NSUInteger contentWeight = 0;
@ -244,24 +269,11 @@ - (CGSize)sizeContent:(CGSize)size {
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize childSize = CGSizeMake(child.width, child.height);
if ([child isKindOfClass:[DoricLayoutContainer class]]
|| childConfig.widthSpec == DoricLayoutWrapContent
|| childConfig.heightSpec == DoricLayoutWrapContent) {
childSize = [child sizeThatFits:CGSizeMake(size.width, size.height - contentHeight)];
}
if (childConfig.widthSpec == DoricLayoutExact) {
childSize.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
childSize.width = size.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
childSize.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
childSize.height = size.height - contentHeight;
}
if (childConfig.weight) {
childSize.height = child.height;
CGSize childSize;
if (CGAffineTransformEqualToTransform(child.transform, CGAffineTransformIdentity)) {
childSize = [child measureSize:CGSizeMake(size.width, size.height - contentHeight)];
} else {
childSize = child.bounds.size;
}
contentWidth = MAX(contentWidth, childSize.width + childConfig.margin.left + childConfig.margin.right);
contentHeight += childSize.height + self.space + childConfig.margin.top + childConfig.margin.bottom;
@ -277,7 +289,9 @@ - (CGSize)sizeContent:(CGSize)size {
return CGSizeMake(contentWidth, contentHeight);
}
- (void)layout:(CGSize)targetSize {
- (void)layoutSelf:(CGSize)targetSize {
self.width = targetSize.width;
self.height = targetSize.height;
CGFloat yStart = 0;
if ((self.gravity & TOP) == TOP) {
yStart = 0;
@ -291,40 +305,20 @@ - (void)layout:(CGSize)targetSize {
if (child.isHidden) {
continue;
}
if (!CGAffineTransformEqualToTransform(child.transform, CGAffineTransformIdentity)) {
continue;
}
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize size = [child sizeThatFits:CGSizeMake(targetSize.width, targetSize.height - yStart)];
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
}
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
size.width = targetSize.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
size.height = targetSize.height - yStart;
}
if (childConfig.weight) {
size.height = child.height;
}
CGSize size = [child measureSize:CGSizeMake(targetSize.width, targetSize.height - yStart)];
if (childConfig.weight) {
size.height += remain / self.contentWeight * childConfig.weight;
}
child.width = size.width;
child.height = size.height;
[child layoutSelf:size];
DoricGravity gravity = childConfig.alignment | self.gravity;
if ((gravity & LEFT) == LEFT) {
child.left = 0;
} else if ((gravity & RIGHT) == RIGHT) {
@ -346,18 +340,12 @@ - (void)layout:(CGSize)targetSize {
if (childConfig.margin.bottom) {
yStart += childConfig.margin.bottom;
}
if ([child isKindOfClass:[DoricLayoutContainer class]]) {
[(DoricLayoutContainer *) child layout:size];
}
}
self.width = targetSize.width;
self.height = targetSize.height;
}
@end
@implementation DoricHLayoutView
- (CGSize)sizeContent:(CGSize)size {
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat contentWidth = 0;
CGFloat contentHeight = 0;
NSUInteger contentWeight = 0;
@ -369,24 +357,11 @@ - (CGSize)sizeContent:(CGSize)size {
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize childSize = CGSizeMake(child.width, child.height);
if ([child isKindOfClass:[DoricLayoutContainer class]]
|| childConfig.widthSpec == DoricLayoutWrapContent
|| childConfig.heightSpec == DoricLayoutWrapContent) {
childSize = [child sizeThatFits:CGSizeMake(size.width - contentWidth, size.height)];
}
if (childConfig.widthSpec == DoricLayoutExact) {
childSize.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
childSize.width = size.width - contentWidth;
}
if (childConfig.heightSpec == DoricLayoutExact) {
childSize.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
childSize.height = size.height;
}
if (childConfig.weight) {
childSize.width = child.width;
CGSize childSize;
if (CGAffineTransformEqualToTransform(child.transform, CGAffineTransformIdentity)) {
childSize = [child measureSize:CGSizeMake(size.width - contentWidth, size.height)];
} else {
childSize = child.bounds.size;
}
contentWidth += childSize.width + self.space + childConfig.margin.left + childConfig.margin.right;
contentHeight = MAX(contentHeight, childSize.height + childConfig.margin.top + childConfig.margin.bottom);
@ -402,7 +377,9 @@ - (CGSize)sizeContent:(CGSize)size {
return CGSizeMake(contentWidth, contentHeight);
}
- (void)layout:(CGSize)targetSize {
- (void)layoutSelf:(CGSize)targetSize {
self.width = targetSize.width;
self.height = targetSize.height;
CGFloat xStart = 0;
if (self.contentWeight) {
xStart = 0;
@ -418,37 +395,20 @@ - (void)layout:(CGSize)targetSize {
if (child.isHidden) {
continue;
}
if (!CGAffineTransformEqualToTransform(child.transform, CGAffineTransformIdentity)) {
continue;
}
DoricLayoutConfig *childConfig = child.layoutConfig;
if (!childConfig) {
childConfig = [DoricLayoutConfig new];
}
CGSize size = [child sizeThatFits:CGSizeMake(targetSize.width - xStart, targetSize.height)];
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
}
if (childConfig.widthSpec == DoricLayoutExact) {
size.width = child.width;
} else if (childConfig.widthSpec == DoricLayoutAtMost) {
size.width = targetSize.width - xStart;
}
if (childConfig.heightSpec == DoricLayoutExact) {
size.height = child.height;
} else if (childConfig.heightSpec == DoricLayoutAtMost) {
size.height = targetSize.height;
}
if (childConfig.weight) {
size.width = child.width;
}
CGSize size = [child measureSize:CGSizeMake(targetSize.width - xStart, targetSize.height)];
if (childConfig.weight) {
size.width += remain / self.contentWeight * childConfig.weight;
}
child.width = size.width;
child.height = size.height;
[child layoutSelf:size];
DoricGravity gravity = childConfig.alignment | self.gravity;
if ((gravity & TOP) == TOP) {
@ -473,45 +433,10 @@ - (void)layout:(CGSize)targetSize {
if (childConfig.margin.right) {
xStart += childConfig.margin.right;
}
if ([child isKindOfClass:[DoricLayoutContainer class]]) {
[(DoricLayoutContainer *) child layout:size];
}
}
self.width = targetSize.width;
self.height = targetSize.height;
}
@end
static const void *kLayoutConfig = &kLayoutConfig;
static const void *kTagString = &kTagString;
@implementation UIView (DoricLayoutConfig)
@dynamic layoutConfig;
- (void)setLayoutConfig:(DoricLayoutConfig *)layoutConfig {
objc_setAssociatedObject(self, kLayoutConfig, layoutConfig, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (DoricLayoutConfig *)layoutConfig {
return objc_getAssociatedObject(self, kLayoutConfig);
}
- (void)setTagString:(NSString *)tagString {
objc_setAssociatedObject(self, kTagString, tagString, OBJC_ASSOCIATION_COPY_NONATOMIC);
self.tag = [tagString hash];
}
- (NSString *)tagString {
return objc_getAssociatedObject(self, kTagString);
}
- (UIView *)viewWithTagString:(NSString *)tagString {
// notice the potential hash collision
return [self viewWithTag:[tagString hash]];
}
@end
DoricVLayoutView *vLayout(NSArray <__kindof UIView *> *views) {
DoricVLayoutView *layout = [[DoricVLayoutView alloc] initWithFrame:CGRectZero];

View File

@ -21,6 +21,7 @@
#import "DoricListNode.h"
#import "DoricExtensions.h"
#import "DoricListItemNode.h"
#import "DoricLayouts.h"
@interface DoricTableViewCell : UITableViewCell
@property(nonatomic, strong) DoricListItemNode *doricListItemNode;
@ -36,13 +37,22 @@ @implementation DoricTableView
- (CGSize)sizeThatFits:(CGSize)size {
if (self.subviews.count > 0) {
CGFloat width = size.width;
CGFloat height = 0;
for (UIView *child in self.subviews) {
width = MAX(child.width, width);
CGSize childSize = [child measureSize:size];
width = MAX(childSize.width, width);
height += childSize.height;
}
return CGSizeMake(width, size.width);
return CGSizeMake(width, MAX(height, size.height));
}
return size;
}
- (void)layoutSelf:(CGSize)targetSize {
[super layoutSelf:targetSize];
[self reloadData];
}
@end
@ -74,9 +84,11 @@ - (UITableView *)build {
- (void)blendView:(UITableView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"itemCount" isEqualToString:name]) {
self.itemCount = [prop unsignedIntegerValue];
[self.view reloadData];
} else if ([@"renderItem" isEqualToString:name]) {
[self.itemViewIds removeAllObjects];
[self clearSubModel];
[self.view reloadData];
} else if ([@"batchCount" isEqualToString:name]) {
self.batchCount = [prop unsignedIntegerValue];
} else {
@ -109,8 +121,8 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
DoricListItemNode *node = cell.doricListItemNode;
node.viewId = model[@"id"];
[node blend:props];
[node.view setNeedsLayout];
CGSize size = [node.view sizeThatFits:CGSizeMake(cell.width, cell.height)];
CGSize size = [node.view measureSize:CGSizeMake(tableView.width, tableView.height)];
[node.view layoutSelf:size];
[self callItem:position height:size.height];
return cell;
}

View File

@ -22,5 +22,9 @@
#import <Foundation/Foundation.h>
#import "DoricSuperNode.h"
@interface DoricScrollerNode : DoricSuperNode<UIScrollView *>
@interface DoricScrollView : UIScrollView
@property(nonatomic, strong) UIView *contentView;
@end
@interface DoricScrollerNode : DoricSuperNode<DoricScrollView *>
@end

View File

@ -22,27 +22,28 @@
#import "DoricScrollerNode.h"
#import "DoricExtensions.h"
@interface DoricScrollView : UIScrollView
@end
@implementation DoricScrollView
- (void)layoutSubviews {
[super layoutSubviews];
if (self.subviews.count > 0) {
UIView *child = self.subviews[0];
[self setContentSize:child.frame.size];
- (void)setContentView:(UIView *)contentView {
if (_contentView) {
[_contentView removeFromSuperview];
}
_contentView = contentView;
[self addSubview:contentView];
}
- (CGSize)sizeThatFits:(CGSize)size {
if (self.subviews.count > 0) {
UIView *child = self.subviews[0];
CGSize childSize = [child sizeThatFits:size];
return CGSizeMake(MIN(size.width, childSize.width), MIN(size.height, childSize.height));
if (self.contentView) {
return [self.contentView sizeThatFits:size];
}
return CGSizeZero;
}
- (void)layoutSelf:(CGSize)targetSize {
[super layoutSelf:targetSize];
[self setContentSize:self.contentView.frame.size];
}
@end
@interface DoricScrollerNode ()
@ -51,7 +52,7 @@ @interface DoricScrollerNode ()
@end
@implementation DoricScrollerNode
- (UIScrollView *)build {
- (DoricScrollView *)build {
return [DoricScrollView new];
}
@ -74,12 +75,11 @@ - (void)blend:(NSDictionary *)props {
[it blend:childProps];
}];
} else {
[self.childNode.view removeFromSuperview];
self.childNode = [[DoricViewNode create:self.doricContext withType:type] also:^(DoricViewNode *it) {
it.viewId = viewId;
[it initWithSuperNode:self];
[it blend:childProps];
[self.view addSubview:it.view];
self.view.contentView = it.view;
}];
}
}
@ -88,12 +88,12 @@ - (void)blend:(NSDictionary *)props {
it.viewId = viewId;
[it initWithSuperNode:self];
[it blend:childProps];
[self.view addSubview:it.view];
self.view.contentView = it.view;
}];
}
}
- (void)blendView:(UIScrollView *)view forPropName:(NSString *)name propValue:(id)prop {
- (void)blendView:(DoricScrollView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"content" isEqualToString:name]) {
self.childViewId = prop;
} else {
@ -104,4 +104,11 @@ - (void)blendView:(UIScrollView *)view forPropName:(NSString *)name propValue:(i
- (void)blendSubNode:(NSDictionary *)subModel {
[self.childNode blend:subModel[@"props"]];
}
@end
- (DoricViewNode *)subNodeWithViewId:(NSString *)viewId {
if ([viewId isEqualToString:self.childViewId]) {
return self.childNode;
}
return nil;
}
@end

View File

@ -25,6 +25,9 @@ @interface DoricSlideItemView : DoricStackView
@end
@implementation DoricSlideItemView
- (void)layoutSubviews {
[super layoutSubviews];
}
@end
@implementation DoricSlideItemNode

View File

@ -45,12 +45,18 @@ - (CGSize)sizeThatFits:(CGSize)size {
if (self.subviews.count > 0) {
CGFloat height = size.height;
for (UIView *child in self.subviews) {
height = MAX(child.height, height);
CGSize childSize = [child measureSize:size];
height = MAX(childSize.height, height);
}
return CGSizeMake(height, size.height);
return CGSizeMake(size.width, size.height);
}
return size;
}
- (void)layoutSelf:(CGSize)targetSize {
[super layoutSelf:targetSize];
[self reloadData];
}
@end
@implementation DoricSliderNode
@ -80,9 +86,11 @@ - (UICollectionView *)build {
- (void)blendView:(UICollectionView *)view forPropName:(NSString *)name propValue:(id)prop {
if ([@"itemCount" isEqualToString:name]) {
self.itemCount = [prop unsignedIntegerValue];
[self.view reloadData];
} else if ([@"renderPage" isEqualToString:name]) {
[self.itemViewIds removeAllObjects];
[self clearSubModel];
[self.view reloadData];
} else if ([@"batchCount" isEqualToString:name]) {
self.batchCount = [prop unsignedIntegerValue];
} else {
@ -120,7 +128,8 @@ - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collection
DoricSlideItemNode *node = cell.doricSlideItemNode;
node.viewId = model[@"id"];
[node blend:props];
[node.view setNeedsLayout];
CGSize size = [node.view measureSize:CGSizeMake(collectionView.width, collectionView.height)];
[node.view layoutSelf:size];
return cell;
}

View File

@ -150,9 +150,11 @@ - (void)clearSubModel {
}
- (DoricViewNode *)subNodeWithViewId:(NSString *)viewId {
NSAssert(NO, @"Should override class:%@ ,method:%@.", NSStringFromClass([self class]),
NSStringFromSelector(_cmd));
return nil;
}
- (void)requestLayout {
[self.view setNeedsLayout];
}
@end
@end

View File

@ -232,4 +232,18 @@ - (NSNumber *)getHeight {
return @(self.view.height);
}
- (void)setRotation:(NSNumber *)rotation {
if (rotation.floatValue == 0) {
self.view.transform = CGAffineTransformIdentity;
} else {
self.view.transform = CGAffineTransformMakeRotation(M_PI * rotation.floatValue * 2);
}
}
- (NSNumber *)getRotation {
float radius = atan2f((float) self.view.transform.b, (float) self.view.transform.a);
float degree = (float) (radius / M_PI / 2);
return @(degree);
}
@end

View File

@ -32,6 +32,7 @@ extern NSString *const INJECT_REQUIRE;
extern NSString *const INJECT_TIMER_SET;
extern NSString *const INJECT_TIMER_CLEAR;
extern NSString *const INJECT_BRIDGE;
extern NSString *const INJECT_EMPTY;
extern NSString *const TEMPLATE_CONTEXT_CREATE;

View File

@ -32,6 +32,7 @@
NSString *const INJECT_TIMER_SET = @"nativeSetTimer";
NSString *const INJECT_TIMER_CLEAR = @"nativeClearTimer";
NSString *const INJECT_BRIDGE = @"nativeBridge";
NSString *const INJECT_EMPTY = @"nativeEmpty";
NSString *const TEMPLATE_CONTEXT_CREATE = @"Reflect.apply("
"function(doric,context,Entry,require,exports){" "\n"

View File

@ -21,6 +21,7 @@ export * from "./src/ui/scroller"
export * from "./src/ui/widgets"
export * from "./src/ui/panel"
export * from "./src/ui/declarative"
export * from "./src/ui/refreshable"
export * from "./src/util/color"
export * from './src/util/log'
export * from './src/util/types'

View File

@ -57,6 +57,10 @@ export class List extends Superview implements IList {
@Property
batchCount = 15
reset() {
this.cachedViews.clear()
this.itemCount = 0
}
private getItem(itemIdx: number) {
let view = this.cachedViews.get(`${itemIdx}`)
if (view === undefined) {

View File

@ -31,6 +31,8 @@ export function NativeCall(target: Panel, propertyKey: string, descriptor: Prope
type Frame = { width: number, height: number }
declare function nativeEmpty(): void
export abstract class Panel {
context?: any
onCreate() { }
@ -132,7 +134,7 @@ export abstract class Panel {
private hookAfterNativeCall() {
//Here insert a native call to ensure the promise is resolved done.
log('Check Dirty')
nativeEmpty()
if (this.__root__.isDirty()) {
const model = this.__root__.toModel()
this.nativeRender(model)

View File

@ -0,0 +1,74 @@
import { View, Property, Superview, IView } from "./view";
import { List } from "./list";
import { Scroller } from "./scroller";
import { BridgeContext } from "../runtime/global";
import { layoutConfig } from "./declarative";
export interface IRefreshable extends IView {
content: List | Scroller
header?: View
onRefresh?: () => void
}
export class Refreshable extends Superview implements IRefreshable {
content!: List | Scroller
header?: View
@Property
onRefresh?: () => void
allSubviews() {
const ret: View[] = [this.content]
if (this.header) {
ret.push(this.header)
}
return ret
}
setRefreshable(context: BridgeContext, refreshable: boolean) {
return this.nativeChannel(context, 'setRefreshable')(refreshable)
}
setRefreshing(context: BridgeContext, refreshing: boolean) {
return this.nativeChannel(context, 'setRefreshing')(refreshing)
}
isRefreshable(context: BridgeContext) {
return this.nativeChannel(context, 'isRefreshable')() as Promise<boolean>
}
isRefreshing(context: BridgeContext) {
return this.nativeChannel(context, 'isRefreshing')() as Promise<boolean>
}
toModel() {
this.dirtyProps.content = this.content.viewId
this.dirtyProps.header = (this.header || {}).viewId
return super.toModel()
}
}
export function refreshable(config: IRefreshable) {
const ret = new Refreshable
ret.layoutConfig = layoutConfig().wrap()
for (let key in config) {
Reflect.set(ret, key, Reflect.get(config, key, config), ret)
}
return ret
}
export interface IPullable {
startAnimation(): void
stopAnimation(): void
setProgressRotation(rotation: number): void
}
export function pullable(context: BridgeContext, v: View, config: IPullable) {
Reflect.set(v, 'startAnimation', config.startAnimation)
Reflect.set(v, 'stopAnimation', config.stopAnimation)
Reflect.set(v, 'setProgressRotation', config.setProgressRotation)
return v
}

View File

@ -18,6 +18,7 @@ import { Modeling, Model, obj2Model } from "../util/types";
import { uniqueId } from "../util/uniqueId";
import { Gravity } from "../util/gravity";
import { loge } from "../util/log";
import { BridgeContext } from "../runtime/global";
export enum LayoutSpec {
EXACTLY = 0,
@ -266,7 +267,7 @@ export abstract class View implements Modeling, IView {
nativeChannel(context: any, name: string) {
let thisView: View | undefined = this
return function (...args: any) {
return function (args: any = undefined) {
const func = context.shader.command
const viewIds = []
while (thisView != undefined) {
@ -281,6 +282,29 @@ export abstract class View implements Modeling, IView {
return Reflect.apply(func, undefined, [params]) as Promise<any>
}
}
getWidth(context: BridgeContext) {
return this.nativeChannel(context, 'getWidth')() as Promise<number>
}
getHeight(context: BridgeContext) {
return this.nativeChannel(context, 'getHeight')() as Promise<number>
}
/**
*
* @param rotation [0..1]
*/
setRotation(context: BridgeContext, rotation: number) {
return this.nativeChannel(context, 'setRotation')(rotation)
}
/**
*
* @return rotation [0..1]
*/
getRotation(context: BridgeContext) {
return this.nativeChannel(context, 'getRotation')() as Promise<number>
}
}
export abstract class Superview extends View {
@ -316,6 +340,7 @@ export abstract class Superview extends View {
toModel() {
const subviews = []
for (let v of this.allSubviews()) {
v.superview = this
if (v.isDirty()) {
subviews.push(v.toModel())
}
@ -341,7 +366,6 @@ export abstract class Group extends Superview {
}
addChild(view: View) {
view.superview = this
this.children.push(view)
}
}

View File

@ -5,7 +5,7 @@
// "incremental": true, /* Enable incremental compilation */
"target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
"lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */