Home | History | Annotate | Download | only in launcher3
      1 package com.android.launcher3;
      2 
      3 import android.animation.AnimatorSet;
      4 import android.animation.ObjectAnimator;
      5 import android.animation.PropertyValuesHolder;
      6 import android.animation.ValueAnimator;
      7 import android.animation.ValueAnimator.AnimatorUpdateListener;
      8 import android.appwidget.AppWidgetHostView;
      9 import android.appwidget.AppWidgetProviderInfo;
     10 import android.content.Context;
     11 import android.content.res.Resources;
     12 import android.graphics.Point;
     13 import android.graphics.Rect;
     14 import android.util.AttributeSet;
     15 import android.view.KeyEvent;
     16 import android.view.MotionEvent;
     17 import android.view.View;
     18 import android.widget.FrameLayout;
     19 
     20 import com.android.launcher3.accessibility.DragViewStateAnnouncer;
     21 import com.android.launcher3.dragndrop.DragLayer;
     22 import com.android.launcher3.util.FocusLogic;
     23 import com.android.launcher3.util.TouchController;
     24 
     25 public class AppWidgetResizeFrame extends FrameLayout
     26         implements View.OnKeyListener, TouchController {
     27     private static final int SNAP_DURATION = 150;
     28     private static final float DIMMED_HANDLE_ALPHA = 0f;
     29     private static final float RESIZE_THRESHOLD = 0.66f;
     30 
     31     private static final Rect sTmpRect = new Rect();
     32 
     33     // Represents the cell size on the grid in the two orientations.
     34     private static Point[] sCellSize;
     35 
     36     private static final int HANDLE_COUNT = 4;
     37     private static final int INDEX_LEFT = 0;
     38     private static final int INDEX_TOP = 1;
     39     private static final int INDEX_RIGHT = 2;
     40     private static final int INDEX_BOTTOM = 3;
     41 
     42     private final Launcher mLauncher;
     43     private final DragViewStateAnnouncer mStateAnnouncer;
     44 
     45     private final View[] mDragHandles = new View[HANDLE_COUNT];
     46 
     47     private LauncherAppWidgetHostView mWidgetView;
     48     private CellLayout mCellLayout;
     49     private DragLayer mDragLayer;
     50 
     51     private Rect mWidgetPadding;
     52 
     53     private final int mBackgroundPadding;
     54     private final int mTouchTargetWidth;
     55 
     56     private final int[] mDirectionVector = new int[2];
     57     private final int[] mLastDirectionVector = new int[2];
     58     private final int[] mTmpPt = new int[2];
     59 
     60     private final IntRange mTempRange1 = new IntRange();
     61     private final IntRange mTempRange2 = new IntRange();
     62 
     63     private final IntRange mDeltaXRange = new IntRange();
     64     private final IntRange mBaselineX = new IntRange();
     65 
     66     private final IntRange mDeltaYRange = new IntRange();
     67     private final IntRange mBaselineY = new IntRange();
     68 
     69     private boolean mLeftBorderActive;
     70     private boolean mRightBorderActive;
     71     private boolean mTopBorderActive;
     72     private boolean mBottomBorderActive;
     73 
     74     private int mResizeMode;
     75 
     76     private int mRunningHInc;
     77     private int mRunningVInc;
     78     private int mMinHSpan;
     79     private int mMinVSpan;
     80     private int mDeltaX;
     81     private int mDeltaY;
     82     private int mDeltaXAddOn;
     83     private int mDeltaYAddOn;
     84 
     85     private int mTopTouchRegionAdjustment = 0;
     86     private int mBottomTouchRegionAdjustment = 0;
     87 
     88     private int mXDown, mYDown;
     89 
     90     public AppWidgetResizeFrame(Context context) {
     91         this(context, null);
     92     }
     93 
     94     public AppWidgetResizeFrame(Context context, AttributeSet attrs) {
     95         this(context, attrs, 0);
     96     }
     97 
     98     public AppWidgetResizeFrame(Context context, AttributeSet attrs, int defStyleAttr) {
     99         super(context, attrs, defStyleAttr);
    100 
    101         mLauncher = Launcher.getLauncher(context);
    102         mStateAnnouncer = DragViewStateAnnouncer.createFor(this);
    103 
    104         mBackgroundPadding = getResources()
    105                 .getDimensionPixelSize(R.dimen.resize_frame_background_padding);
    106         mTouchTargetWidth = 2 * mBackgroundPadding;
    107     }
    108 
    109     @Override
    110     protected void onFinishInflate() {
    111         super.onFinishInflate();
    112 
    113         for (int i = 0; i < HANDLE_COUNT; i ++) {
    114             mDragHandles[i] = getChildAt(i);
    115         }
    116     }
    117 
    118     public void setupForWidget(LauncherAppWidgetHostView widgetView, CellLayout cellLayout,
    119             DragLayer dragLayer) {
    120         mCellLayout = cellLayout;
    121         mWidgetView = widgetView;
    122         LauncherAppWidgetProviderInfo info = (LauncherAppWidgetProviderInfo)
    123                 widgetView.getAppWidgetInfo();
    124         mResizeMode = info.resizeMode;
    125         mDragLayer = dragLayer;
    126 
    127         mMinHSpan = info.minSpanX;
    128         mMinVSpan = info.minSpanY;
    129 
    130         if (!info.isCustomWidget) {
    131             mWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(getContext(),
    132                     widgetView.getAppWidgetInfo().provider, null);
    133         } else {
    134             Resources r = getContext().getResources();
    135             int padding = r.getDimensionPixelSize(R.dimen.default_widget_padding);
    136             mWidgetPadding = new Rect(padding, padding, padding, padding);
    137         }
    138 
    139         if (mResizeMode == AppWidgetProviderInfo.RESIZE_HORIZONTAL) {
    140             mDragHandles[INDEX_TOP].setVisibility(GONE);
    141             mDragHandles[INDEX_BOTTOM].setVisibility(GONE);
    142         } else if (mResizeMode == AppWidgetProviderInfo.RESIZE_VERTICAL) {
    143             mDragHandles[INDEX_LEFT].setVisibility(GONE);
    144             mDragHandles[INDEX_RIGHT].setVisibility(GONE);
    145         }
    146 
    147         // When we create the resize frame, we first mark all cells as unoccupied. The appropriate
    148         // cells (same if not resized, or different) will be marked as occupied when the resize
    149         // frame is dismissed.
    150         mCellLayout.markCellsAsUnoccupiedForView(mWidgetView);
    151 
    152         setOnKeyListener(this);
    153     }
    154 
    155     public boolean beginResizeIfPointInRegion(int x, int y) {
    156         boolean horizontalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_HORIZONTAL) != 0;
    157         boolean verticalActive = (mResizeMode & AppWidgetProviderInfo.RESIZE_VERTICAL) != 0;
    158 
    159         mLeftBorderActive = (x < mTouchTargetWidth) && horizontalActive;
    160         mRightBorderActive = (x > getWidth() - mTouchTargetWidth) && horizontalActive;
    161         mTopBorderActive = (y < mTouchTargetWidth + mTopTouchRegionAdjustment) && verticalActive;
    162         mBottomBorderActive = (y > getHeight() - mTouchTargetWidth + mBottomTouchRegionAdjustment)
    163                 && verticalActive;
    164 
    165         boolean anyBordersActive = mLeftBorderActive || mRightBorderActive
    166                 || mTopBorderActive || mBottomBorderActive;
    167 
    168         if (anyBordersActive) {
    169             mDragHandles[INDEX_LEFT].setAlpha(mLeftBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
    170             mDragHandles[INDEX_RIGHT].setAlpha(mRightBorderActive ? 1.0f :DIMMED_HANDLE_ALPHA);
    171             mDragHandles[INDEX_TOP].setAlpha(mTopBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
    172             mDragHandles[INDEX_BOTTOM].setAlpha(mBottomBorderActive ? 1.0f : DIMMED_HANDLE_ALPHA);
    173         }
    174 
    175         if (mLeftBorderActive) {
    176             mDeltaXRange.set(-getLeft(), getWidth() - 2 * mTouchTargetWidth);
    177         } else if (mRightBorderActive) {
    178             mDeltaXRange.set(2 * mTouchTargetWidth - getWidth(), mDragLayer.getWidth() - getRight());
    179         } else {
    180             mDeltaXRange.set(0, 0);
    181         }
    182         mBaselineX.set(getLeft(), getRight());
    183 
    184         if (mTopBorderActive) {
    185             mDeltaYRange.set(-getTop(), getHeight() - 2 * mTouchTargetWidth);
    186         } else if (mBottomBorderActive) {
    187             mDeltaYRange.set(2 * mTouchTargetWidth - getHeight(), mDragLayer.getHeight() - getBottom());
    188         } else {
    189             mDeltaYRange.set(0, 0);
    190         }
    191         mBaselineY.set(getTop(), getBottom());
    192 
    193         return anyBordersActive;
    194     }
    195 
    196     /**
    197      *  Based on the deltas, we resize the frame.
    198      */
    199     public void visualizeResizeForDelta(int deltaX, int deltaY) {
    200         mDeltaX = mDeltaXRange.clamp(deltaX);
    201         mDeltaY = mDeltaYRange.clamp(deltaY);
    202 
    203         DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
    204         mDeltaX = mDeltaXRange.clamp(deltaX);
    205         mBaselineX.applyDelta(mLeftBorderActive, mRightBorderActive, mDeltaX, mTempRange1);
    206         lp.x = mTempRange1.start;
    207         lp.width = mTempRange1.size();
    208 
    209         mDeltaY = mDeltaYRange.clamp(deltaY);
    210         mBaselineY.applyDelta(mTopBorderActive, mBottomBorderActive, mDeltaY, mTempRange1);
    211         lp.y = mTempRange1.start;
    212         lp.height = mTempRange1.size();
    213 
    214         resizeWidgetIfNeeded(false);
    215 
    216         // When the widget resizes in multi-window mode, the translation value changes to maintain
    217         // a center fit. These overrides ensure the resize frame always aligns with the widget view.
    218         getSnappedRectRelativeToDragLayer(sTmpRect);
    219         if (mLeftBorderActive) {
    220             lp.width = sTmpRect.width() + sTmpRect.left - lp.x;
    221         }
    222         if (mTopBorderActive) {
    223             lp.height = sTmpRect.height() + sTmpRect.top - lp.y;
    224         }
    225         if (mRightBorderActive) {
    226             lp.x = sTmpRect.left;
    227         }
    228         if (mBottomBorderActive) {
    229             lp.y = sTmpRect.top;
    230         }
    231 
    232         requestLayout();
    233     }
    234 
    235     private static int getSpanIncrement(float deltaFrac) {
    236         return Math.abs(deltaFrac) > RESIZE_THRESHOLD ? Math.round(deltaFrac) : 0;
    237     }
    238 
    239     /**
    240      *  Based on the current deltas, we determine if and how to resize the widget.
    241      */
    242     private void resizeWidgetIfNeeded(boolean onDismiss) {
    243         float xThreshold = mCellLayout.getCellWidth();
    244         float yThreshold = mCellLayout.getCellHeight();
    245 
    246         int hSpanInc = getSpanIncrement((mDeltaX + mDeltaXAddOn) / xThreshold - mRunningHInc);
    247         int vSpanInc = getSpanIncrement((mDeltaY + mDeltaYAddOn) / yThreshold - mRunningVInc);
    248 
    249         if (!onDismiss && (hSpanInc == 0 && vSpanInc == 0)) return;
    250 
    251         mDirectionVector[0] = 0;
    252         mDirectionVector[1] = 0;
    253 
    254         CellLayout.LayoutParams lp = (CellLayout.LayoutParams) mWidgetView.getLayoutParams();
    255 
    256         int spanX = lp.cellHSpan;
    257         int spanY = lp.cellVSpan;
    258         int cellX = lp.useTmpCoords ? lp.tmpCellX : lp.cellX;
    259         int cellY = lp.useTmpCoords ? lp.tmpCellY : lp.cellY;
    260 
    261         // For each border, we bound the resizing based on the minimum width, and the maximum
    262         // expandability.
    263         mTempRange1.set(cellX, spanX + cellX);
    264         int hSpanDelta = mTempRange1.applyDeltaAndBound(mLeftBorderActive, mRightBorderActive,
    265                 hSpanInc, mMinHSpan, mCellLayout.getCountX(), mTempRange2);
    266         cellX = mTempRange2.start;
    267         spanX = mTempRange2.size();
    268         if (hSpanDelta != 0) {
    269             mDirectionVector[0] = mLeftBorderActive ? -1 : 1;
    270         }
    271 
    272         mTempRange1.set(cellY, spanY + cellY);
    273         int vSpanDelta = mTempRange1.applyDeltaAndBound(mTopBorderActive, mBottomBorderActive,
    274                 vSpanInc, mMinVSpan, mCellLayout.getCountY(), mTempRange2);
    275         cellY = mTempRange2.start;
    276         spanY = mTempRange2.size();
    277         if (vSpanDelta != 0) {
    278             mDirectionVector[1] = mTopBorderActive ? -1 : 1;
    279         }
    280 
    281         if (!onDismiss && vSpanDelta == 0 && hSpanDelta == 0) return;
    282 
    283         // We always want the final commit to match the feedback, so we make sure to use the
    284         // last used direction vector when committing the resize / reorder.
    285         if (onDismiss) {
    286             mDirectionVector[0] = mLastDirectionVector[0];
    287             mDirectionVector[1] = mLastDirectionVector[1];
    288         } else {
    289             mLastDirectionVector[0] = mDirectionVector[0];
    290             mLastDirectionVector[1] = mDirectionVector[1];
    291         }
    292 
    293         if (mCellLayout.createAreaForResize(cellX, cellY, spanX, spanY, mWidgetView,
    294                 mDirectionVector, onDismiss)) {
    295             if (mStateAnnouncer != null && (lp.cellHSpan != spanX || lp.cellVSpan != spanY) ) {
    296                 mStateAnnouncer.announce(
    297                         mLauncher.getString(R.string.widget_resized, spanX, spanY));
    298             }
    299 
    300             lp.tmpCellX = cellX;
    301             lp.tmpCellY = cellY;
    302             lp.cellHSpan = spanX;
    303             lp.cellVSpan = spanY;
    304             mRunningVInc += vSpanDelta;
    305             mRunningHInc += hSpanDelta;
    306 
    307             if (!onDismiss) {
    308                 updateWidgetSizeRanges(mWidgetView, mLauncher, spanX, spanY);
    309             }
    310         }
    311         mWidgetView.requestLayout();
    312     }
    313 
    314     static void updateWidgetSizeRanges(AppWidgetHostView widgetView, Launcher launcher,
    315             int spanX, int spanY) {
    316         getWidgetSizeRanges(launcher, spanX, spanY, sTmpRect);
    317         widgetView.updateAppWidgetSize(null, sTmpRect.left, sTmpRect.top,
    318                 sTmpRect.right, sTmpRect.bottom);
    319     }
    320 
    321     public static Rect getWidgetSizeRanges(Context context, int spanX, int spanY, Rect rect) {
    322         if (sCellSize == null) {
    323             InvariantDeviceProfile inv = LauncherAppState.getIDP(context);
    324 
    325             // Initiate cell sizes.
    326             sCellSize = new Point[2];
    327             sCellSize[0] = inv.landscapeProfile.getCellSize();
    328             sCellSize[1] = inv.portraitProfile.getCellSize();
    329         }
    330 
    331         if (rect == null) {
    332             rect = new Rect();
    333         }
    334         final float density = context.getResources().getDisplayMetrics().density;
    335 
    336         // Compute landscape size
    337         int landWidth = (int) ((spanX * sCellSize[0].x) / density);
    338         int landHeight = (int) ((spanY * sCellSize[0].y) / density);
    339 
    340         // Compute portrait size
    341         int portWidth = (int) ((spanX * sCellSize[1].x) / density);
    342         int portHeight = (int) ((spanY * sCellSize[1].y) / density);
    343         rect.set(portWidth, landHeight, landWidth, portHeight);
    344         return rect;
    345     }
    346 
    347     /**
    348      * This is the final step of the resize. Here we save the new widget size and position
    349      * to LauncherModel and animate the resize frame.
    350      */
    351     public void commitResize() {
    352         resizeWidgetIfNeeded(true);
    353         requestLayout();
    354     }
    355 
    356     private void onTouchUp() {
    357         int xThreshold = mCellLayout.getCellWidth();
    358         int yThreshold = mCellLayout.getCellHeight();
    359 
    360         mDeltaXAddOn = mRunningHInc * xThreshold;
    361         mDeltaYAddOn = mRunningVInc * yThreshold;
    362         mDeltaX = 0;
    363         mDeltaY = 0;
    364 
    365         post(new Runnable() {
    366             @Override
    367             public void run() {
    368                 snapToWidget(true);
    369             }
    370         });
    371     }
    372 
    373     /**
    374      * Returns the rect of this view when the frame is snapped around the widget, with the bounds
    375      * relative to the {@link DragLayer}.
    376      */
    377     private void getSnappedRectRelativeToDragLayer(Rect out) {
    378         float scale = mWidgetView.getScaleToFit();
    379 
    380         mDragLayer.getViewRectRelativeToSelf(mWidgetView, out);
    381 
    382         int width = 2 * mBackgroundPadding
    383                 + (int) (scale * (out.width() - mWidgetPadding.left - mWidgetPadding.right));
    384         int height = 2 * mBackgroundPadding
    385                 + (int) (scale * (out.height() - mWidgetPadding.top - mWidgetPadding.bottom));
    386 
    387         int x = (int) (out.left - mBackgroundPadding + scale * mWidgetPadding.left);
    388         int y = (int) (out.top - mBackgroundPadding + scale * mWidgetPadding.top);
    389 
    390         out.left = x;
    391         out.top = y;
    392         out.right = out.left + width;
    393         out.bottom = out.top + height;
    394     }
    395 
    396     public void snapToWidget(boolean animate) {
    397         getSnappedRectRelativeToDragLayer(sTmpRect);
    398         int newWidth = sTmpRect.width();
    399         int newHeight = sTmpRect.height();
    400         int newX = sTmpRect.left;
    401         int newY = sTmpRect.top;
    402 
    403         // We need to make sure the frame's touchable regions lie fully within the bounds of the
    404         // DragLayer. We allow the actual handles to be clipped, but we shift the touch regions
    405         // down accordingly to provide a proper touch target.
    406         if (newY < 0) {
    407             // In this case we shift the touch region down to start at the top of the DragLayer
    408             mTopTouchRegionAdjustment = -newY;
    409         } else {
    410             mTopTouchRegionAdjustment = 0;
    411         }
    412         if (newY + newHeight > mDragLayer.getHeight()) {
    413             // In this case we shift the touch region up to end at the bottom of the DragLayer
    414             mBottomTouchRegionAdjustment = -(newY + newHeight - mDragLayer.getHeight());
    415         } else {
    416             mBottomTouchRegionAdjustment = 0;
    417         }
    418 
    419         final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) getLayoutParams();
    420         if (!animate) {
    421             lp.width = newWidth;
    422             lp.height = newHeight;
    423             lp.x = newX;
    424             lp.y = newY;
    425             for (int i = 0; i < HANDLE_COUNT; i++) {
    426                 mDragHandles[i].setAlpha(1.0f);
    427             }
    428             requestLayout();
    429         } else {
    430             PropertyValuesHolder width = PropertyValuesHolder.ofInt("width", lp.width, newWidth);
    431             PropertyValuesHolder height = PropertyValuesHolder.ofInt("height", lp.height,
    432                     newHeight);
    433             PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX);
    434             PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY);
    435             ObjectAnimator oa =
    436                     LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y);
    437             oa.addUpdateListener(new AnimatorUpdateListener() {
    438                 public void onAnimationUpdate(ValueAnimator animation) {
    439                     requestLayout();
    440                 }
    441             });
    442             AnimatorSet set = LauncherAnimUtils.createAnimatorSet();
    443             set.play(oa);
    444             for (int i = 0; i < HANDLE_COUNT; i++) {
    445                 set.play(LauncherAnimUtils.ofFloat(mDragHandles[i], ALPHA, 1.0f));
    446             }
    447 
    448             set.setDuration(SNAP_DURATION);
    449             set.start();
    450         }
    451 
    452         setFocusableInTouchMode(true);
    453         requestFocus();
    454     }
    455 
    456     @Override
    457     public boolean onKey(View v, int keyCode, KeyEvent event) {
    458         // Clear the frame and give focus to the widget host view when a directional key is pressed.
    459         if (FocusLogic.shouldConsume(keyCode)) {
    460             mDragLayer.clearResizeFrame();
    461             mWidgetView.requestFocus();
    462             return true;
    463         }
    464         return false;
    465     }
    466 
    467     private boolean handleTouchDown(MotionEvent ev) {
    468         Rect hitRect = new Rect();
    469         int x = (int) ev.getX();
    470         int y = (int) ev.getY();
    471 
    472         getHitRect(hitRect);
    473         if (hitRect.contains(x, y)) {
    474             if (beginResizeIfPointInRegion(x - getLeft(), y - getTop())) {
    475                 mXDown = x;
    476                 mYDown = y;
    477                 return true;
    478             }
    479         }
    480         return false;
    481     }
    482 
    483     @Override
    484     public boolean onControllerTouchEvent(MotionEvent ev) {
    485         int action = ev.getAction();
    486         int x = (int) ev.getX();
    487         int y = (int) ev.getY();
    488 
    489         switch (action) {
    490             case MotionEvent.ACTION_DOWN:
    491                 return handleTouchDown(ev);
    492             case MotionEvent.ACTION_MOVE:
    493                 visualizeResizeForDelta(x - mXDown, y - mYDown);
    494                 break;
    495             case MotionEvent.ACTION_CANCEL:
    496             case MotionEvent.ACTION_UP:
    497                 visualizeResizeForDelta(x - mXDown, y - mYDown);
    498                 onTouchUp();
    499                 mXDown = mYDown = 0;
    500                 break;
    501         }
    502         return true;
    503     }
    504 
    505     @Override
    506     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
    507         if (ev.getAction() == MotionEvent.ACTION_DOWN && handleTouchDown(ev)) {
    508             return true;
    509         }
    510         return false;
    511     }
    512 
    513     /**
    514      * A mutable class for describing the range of two int values.
    515      */
    516     private static class IntRange {
    517 
    518         public int start, end;
    519 
    520         public int clamp(int value) {
    521             return Utilities.boundToRange(value, start, end);
    522         }
    523 
    524         public void set(int s, int e) {
    525             start = s;
    526             end = e;
    527         }
    528 
    529         public int size() {
    530             return end - start;
    531         }
    532 
    533         /**
    534          * Moves either the start or end edge (but never both) by {@param delta} and  sets the
    535          * result in {@param out}
    536          */
    537         public void applyDelta(boolean moveStart, boolean moveEnd, int delta, IntRange out) {
    538             out.start = moveStart ? start + delta : start;
    539             out.end = moveEnd ? end + delta : end;
    540         }
    541 
    542         /**
    543          * Applies delta similar to {@link #applyDelta(boolean, boolean, int, IntRange)},
    544          * with extra conditions.
    545          * @param minSize minimum size after with the moving edge should not be shifted any further.
    546          *                For eg, if delta = -3 when moving the endEdge brings the size to less than
    547          *                minSize, only delta = -2 will applied
    548          * @param maxEnd The maximum value to the end edge (start edge is always restricted to 0)
    549          * @return the amount of increase when endEdge was moves and the amount of decrease when
    550          * the start edge was moved.
    551          */
    552         public int applyDeltaAndBound(boolean moveStart, boolean moveEnd, int delta,
    553                 int minSize, int maxEnd, IntRange out) {
    554             applyDelta(moveStart, moveEnd, delta, out);
    555             if (out.start < 0) {
    556                 out.start = 0;
    557             }
    558             if (out.end > maxEnd) {
    559                 out.end = maxEnd;
    560             }
    561             if (out.size() < minSize) {
    562                 if (moveStart) {
    563                     out.start = out.end - minSize;
    564                 } else if (moveEnd) {
    565                     out.end = out.start + minSize;
    566                 }
    567             }
    568             return moveEnd ? out.size() - size() : size() - out.size();
    569         }
    570     }
    571 }
    572