Android从上到下抽屉式效果
16lz
2021-01-26
SlidingDrawerDemo.java:
package org.lee.android;import org.lee.android.ExpoInterpolator.EasingType;import org.lee.android.ExpoInterpolator.ExpoInterpolator;import org.lee.android.widget.Panel;import org.lee.android.widget.Panel.OnPanelListener;import android.app.Activity;import android.os.Bundle;import android.util.Log;public class SlidingDrawerDemo extends Activity implements OnPanelListener{@Overrideprotected void onCreate(Bundle savedInstanceState) {// TODO Auto-generated method stubsuper.onCreate(savedInstanceState);super.setContentView(R.layout.sildingdrawer);Panel panel; panel = (Panel) findViewById(R.id.topPanel); panel.setOnPanelListener(this); panel.setInterpolator(new ExpoInterpolator(EasingType.OUT)); }public void onPanelClosed(Panel panel) {String panelName = getResources().getResourceEntryName(panel.getId());Log.d("TestPanels", "Panel [" + panelName + "] closed");}public void onPanelOpened(Panel panel) {String panelName = getResources().getResourceEntryName(panel.getId());Log.d("TestPanels", "Panel [" + panelName + "] opened");}}
sildingdrawer.xml:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:panel="http://schemas.android.com/apk/res/org.lee.android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#ffffff" android:orientation="vertical" > <RelativeLayout android:id="@+id/listHeader" android:layout_width="fill_parent" android:layout_height="48dp" android:layout_alignParentTop="true" android:background="#F7BAD6" > <ImageView android:id="@+id/header_back" android:layout_width="12dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_marginLeft="12px" android:background="@drawable/header_back" /> <TextView android:id="@+id/shop_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="15px" android:layout_toRightOf="@id/header_back" android:text="标题" android:textColor="#ffffff" android:textSize="28px" /> <ImageView android:id="@+id/three_rect" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="12dp" android:background="@drawable/three_rect" /> </RelativeLayout> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/listHeader" android:orientation="vertical" > <!-- tab标签栏 --> <LinearLayout android:id="@+id/header" android:layout_width="fill_parent" android:layout_height="40dp" android:background="#FFFFFF" > <TextView android:id="@+id/text1" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡1" android:textColor="#000000" android:textSize="22.0dip" /> <View android:layout_width="2dip" android:layout_height="30dip" android:layout_gravity="center_vertical" android:background="#D7D7D7" /> <TextView android:id="@+id/text2" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡2" android:textColor="#000000" android:textSize="22.0dip" /> <View android:layout_width="2dip" android:layout_height="30dip" android:layout_gravity="center_vertical" android:background="#D7D7D7" /> <TextView android:id="@+id/text3" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡3" android:textColor="#000000" android:textSize="22.0dip" /> <View android:layout_width="2dip" android:layout_height="30dip" android:layout_gravity="center_vertical" android:background="#D7D7D7" /> <TextView android:id="@+id/text4" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1.0" android:gravity="center" android:text="页卡4" android:textColor="#000000" android:textSize="22.0dip" /> <!-- 最右侧分类标识 --> <FrameLayout android:layout_width="50dp" android:layout_height="fill_parent" android:layout_marginRight="5dp" > </FrameLayout> </LinearLayout> <TextView android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_below="@id/header" android:background="#ff6600" android:text="和哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈和" android:textColor="#ffffff" android:textSize="18sp" android:textStyle="bold" /> <org.lee.android.widget.Panel android:id="@+id/topPanel" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="top|right" android:paddingBottom="4dip" panel:animationDuration="1200" panel:content="@+id/panelContent" panel:handle="@+id/panelHandle" panel:linearFlying="true" panel:position="top" > <TextView android:id="@+id/panelHandle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:background="@drawable/up" android:gravity="center" android:padding="5dp" android:text="分类" android:textColor="#ffffff" android:textSize="15sp" /> <LinearLayout android:id="@+id/panelContent" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > <CheckBox android:layout_width="fill_parent" android:layout_height="60dip" android:background="#688" android:gravity="center" android:text="top check box" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="#323299" android:gravity="center" android:padding="4dip" android:text="Bounce\nInterpolator" android:textColor="#eee" android:textSize="16dip" android:textStyle="bold" /> </LinearLayout> </org.lee.android.widget.Panel> </RelativeLayout></RelativeLayout>
EasingType.java:
package org.lee.android.ExpoInterpolator;public class EasingType {public static final int IN = 0;public static final int OUT = 1;public static final int INOUT = 2;}
ExpoInterpolator.java:
package org.lee.android.ExpoInterpolator;/* * * Open source under the BSD License. * * Copyright © 2001 Robert Penner * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * Neither the name of the author nor the names of contributors may be used to endorse * or promote products derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * */import android.view.animation.Interpolator;public class ExpoInterpolator implements Interpolator {private int type;public ExpoInterpolator(int type) {this.type = type;}public float getInterpolation(float t) {if (type == EasingType.IN) {return in(t);} elseif (type == EasingType.OUT) {return out(t);} elseif (type == EasingType.INOUT) {return inout(t);}return 0;}private float in(float t) {return (float) ((t==0) ? 0 : Math.pow(2, 10 * (t - 1)));}private float out(float t) {return (float) ((t>=1) ? 1 : (-Math.pow(2, -10 * t) + 1));}private float inout(float t) {if (t == 0) {return 0;}if (t >= 1) {return 1;}t *= 2;if (t < 1) {return (float) (0.5f * Math.pow(2, 10 * (t - 1)));} else {return (float) (0.5f * (-Math.pow(2, -10 * --t) + 2));}}}
Panel.java:
package org.lee.android.widget;import org.lee.android.R;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.drawable.Drawable;import android.util.AttributeSet;import android.util.Log;import android.view.GestureDetector;import android.view.GestureDetector.OnGestureListener;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.view.ViewParent;import android.view.animation.Animation;import android.view.animation.Animation.AnimationListener;import android.view.animation.Interpolator;import android.view.animation.LinearInterpolator;import android.view.animation.TranslateAnimation;import android.widget.FrameLayout;import android.widget.LinearLayout;public class Panel extends LinearLayout { private static final String TAG = "Panel";/** * Callback invoked when the panel is opened/closed. */ public static interface OnPanelListener { /** * Invoked when the panel becomes fully closed. */ public void onPanelClosed(Panel panel); /** * Invoked when the panel becomes fully opened. */ public void onPanelOpened(Panel panel); } private boolean mIsShrinking;private int mPosition;private int mDuration;private boolean mLinearFlying;private int mHandleId;private int mContentId;private View mHandle;private View mContent;private Drawable mOpenedHandle;private Drawable mClosedHandle;private float mTrackX;private float mTrackY;private float mVelocity;private OnPanelListener panelListener;public static final int TOP = 0;public static final int BOTTOM = 1;public static final int LEFT = 2;public static final int RIGHT = 3;private enum State {ABOUT_TO_ANIMATE,ANIMATING,READY,TRACKING,FLYING,};private State mState;private Interpolator mInterpolator;private GestureDetector mGestureDetector;private int mContentHeight;private int mContentWidth;private int mOrientation;private float mWeight;private PanelOnGestureListener mGestureListener;private boolean mBringToFront;public Panel(Context context, AttributeSet attrs) {super(context, attrs);TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Panel);mDuration = a.getInteger(R.styleable.Panel_animationDuration, 750);// duration defaults to 750 msmPosition = a.getInteger(R.styleable.Panel_position, BOTTOM);// position defaults to BOTTOMmLinearFlying = a.getBoolean(R.styleable.Panel_linearFlying, false);// linearFlying defaults to falsemWeight = a.getFraction(R.styleable.Panel_weight, 0, 1, 0.0f);// weight defaults to 0.0if (mWeight < 0 || mWeight > 1) {mWeight = 0.0f;Log.w(TAG, a.getPositionDescription() + ": weight must be > 0 and <= 1");}mOpenedHandle = a.getDrawable(R.styleable.Panel_openedHandle);mClosedHandle = a.getDrawable(R.styleable.Panel_closedHandle);RuntimeException e = null;mHandleId = a.getResourceId(R.styleable.Panel_handle, 0);if (mHandleId == 0) {e = new IllegalArgumentException(a.getPositionDescription() + ": The handle attribute is required and must refer to a valid child.");}mContentId = a.getResourceId(R.styleable.Panel_content, 0);if (mContentId == 0) {e = new IllegalArgumentException(a.getPositionDescription() + ": The content attribute is required and must refer to a valid child.");}a.recycle();if (e != null) {throw e;}mOrientation = (mPosition == TOP || mPosition == BOTTOM)? VERTICAL : HORIZONTAL;setOrientation(mOrientation);mState = State.READY;mGestureListener = new PanelOnGestureListener();mGestureDetector = new GestureDetector(mGestureListener);mGestureDetector.setIsLongpressEnabled(false);// i DON'T really know why i need this...setBaselineAligned(false);} /** * Sets the listener that receives a notification when the panel becomes open/close. * * @param onPanelListener The listener to be notified when the panel is opened/closed. */ public void setOnPanelListener(OnPanelListener onPanelListener) { panelListener = onPanelListener; } /** * Gets Panel's mHandle * * @return Panel's mHandle */ public View getHandle() {return mHandle;} /** * Gets Panel's mContent * * @return Panel's mContent */ public View getContent() {return mContent;} /** * Sets the acceleration curve for panel's animation. * * @param i The interpolator which defines the acceleration curve */ public void setInterpolator(Interpolator i) { mInterpolator = i; } /** * Set the opened state of Panel. * * @param open True if Panel is to be opened, false if Panel is to be closed. * @param animate True if use animation, false otherwise. * * @return True if operation was performed, false otherwise. * */public boolean setOpen(boolean open, boolean animate) {if (mState == State.READY && isOpen() ^ open) {mIsShrinking = !open;if (animate) {mState = State.ABOUT_TO_ANIMATE;if (!mIsShrinking) {// this could make flicker so we test mState in dispatchDraw()// to see if is equal to ABOUT_TO_ANIMATEmContent.setVisibility(VISIBLE);}post(startAnimation);} else {mContent.setVisibility(open? VISIBLE : GONE);postProcess();}return true;}return false;} /** * Returns the opened status for Panel. * * @return True if Panel is opened, false otherwise. */public boolean isOpen() {return mContent.getVisibility() == VISIBLE;}@Overrideprotected void onFinishInflate() {super.onFinishInflate();mHandle = findViewById(mHandleId);if (mHandle == null) {String name = getResources().getResourceEntryName(mHandleId); throw new RuntimeException("Your Panel must have a child View whose id attribute is 'R.id." + name + "'");}mHandle.setOnTouchListener(touchListener);mHandle.setOnClickListener(clickListener);mContent = findViewById(mContentId);if (mContent == null) {String name = getResources().getResourceEntryName(mHandleId); throw new RuntimeException("Your Panel must have a child View whose id attribute is 'R.id." + name + "'");}// reposition childrenremoveView(mHandle);removeView(mContent);if (mPosition == TOP || mPosition == LEFT) {addView(mContent);addView(mHandle);} else {addView(mHandle);addView(mContent);}if (mClosedHandle != null) {mHandle.setBackgroundDrawable(mClosedHandle);}mContent.setClickable(true);mContent.setVisibility(GONE);if (mWeight > 0) {ViewGroup.LayoutParams params = mContent.getLayoutParams();if (mOrientation == VERTICAL) {params.height = ViewGroup.LayoutParams.FILL_PARENT;} else {params.width = ViewGroup.LayoutParams.FILL_PARENT;}mContent.setLayoutParams(params);}}@Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();ViewParent parent = getParent();if (parent != null && parent instanceof FrameLayout) {mBringToFront = true;}}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {if (mWeight > 0 && mContent.getVisibility() == VISIBLE) {View parent = (View) getParent();if (parent != null) {if (mOrientation == VERTICAL) {heightMeasureSpec = MeasureSpec.makeMeasureSpec((int) (parent.getHeight() * mWeight), MeasureSpec.EXACTLY);} else {widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (parent.getWidth() * mWeight), MeasureSpec.EXACTLY);}}}super.onMeasure(widthMeasureSpec, heightMeasureSpec);}@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {super.onLayout(changed, l, t, r, b);mContentWidth = mContent.getWidth();mContentHeight = mContent.getHeight();}@Overrideprotected void dispatchDraw(Canvas canvas) {//String name = getResources().getResourceEntryName(getId());//Log.d(TAG, name + " ispatchDraw " + mState);// this is why 'mState' was added:// avoid flicker before animation startif (mState == State.ABOUT_TO_ANIMATE && !mIsShrinking) {int delta = mOrientation == VERTICAL? mContentHeight : mContentWidth;if (mPosition == LEFT || mPosition == TOP) {delta = -delta;}if (mOrientation == VERTICAL) {canvas.translate(0, delta);} else {canvas.translate(delta, 0);}}if (mState == State.TRACKING || mState == State.FLYING) {canvas.translate(mTrackX, mTrackY);}super.dispatchDraw(canvas);}private float ensureRange(float v, int min, int max) {v = Math.max(v, min);v = Math.min(v, max);return v;}OnTouchListener touchListener = new OnTouchListener() {float touchX, touchY;public boolean onTouch(View v, MotionEvent event) {if (mState == State.ANIMATING) {// we are animatingreturn false;}int action = event.getAction();if (action == MotionEvent.ACTION_DOWN) {if (mBringToFront) {bringToFront();}touchX = event.getX();touchY = event.getY();}if (!mGestureDetector.onTouchEvent(event)) {if (action == MotionEvent.ACTION_UP) {// tup up after scrollingint size = (int) (Math.abs(touchX - event.getX()) + Math.abs(touchY - event.getY()));if (size == mContentWidth || size == mContentHeight) {mState = State.ABOUT_TO_ANIMATE;//Log.e("size", String.valueOf(size));//Log.e(String.valueOf(mContentWidth),String.valueOf(mContentHeight));}post(startAnimation);}}return false;}};OnClickListener clickListener = new OnClickListener() {public void onClick(View v) {if (mBringToFront) {bringToFront();}if (initChange()) {post(startAnimation);}}};public boolean initChange() {if (mState != State.READY) {// we are animating or just about to animatereturn false;}mState = State.ABOUT_TO_ANIMATE;mIsShrinking = mContent.getVisibility() == VISIBLE;if (!mIsShrinking) {// this could make flicker so we test mState in dispatchDraw()// to see if is equal to ABOUT_TO_ANIMATEmContent.setVisibility(VISIBLE);}return true;}Runnable startAnimation = new Runnable() {public void run() {// this is why we post this Runnable couple of lines above:// now its save to use mContent.getHeight() && mContent.getWidth()TranslateAnimation animation;int fromXDelta = 0, toXDelta = 0, fromYDelta = 0, toYDelta = 0;if (mState == State.FLYING) {mIsShrinking = (mPosition == TOP || mPosition == LEFT) ^ (mVelocity > 0);}int calculatedDuration;if (mOrientation == VERTICAL) {int height = mContentHeight;if (!mIsShrinking) {fromYDelta = mPosition == TOP? -height : height;} else {toYDelta = mPosition == TOP? -height : height;}if (mState == State.TRACKING) {if (Math.abs(mTrackY - fromYDelta) < Math.abs(mTrackY - toYDelta)) {mIsShrinking = !mIsShrinking;toYDelta = fromYDelta;}fromYDelta = (int) mTrackY;} elseif (mState == State.FLYING) {fromYDelta = (int) mTrackY;}// for FLYING events we calculate animation duration based on flying velocity// also for very high velocity make sure duration >= 20 msif (mState == State.FLYING && mLinearFlying) {calculatedDuration = (int) (1000 * Math.abs((toYDelta - fromYDelta) / mVelocity));calculatedDuration = Math.max(calculatedDuration, 20);} else {calculatedDuration = mDuration * Math.abs(toYDelta - fromYDelta) / mContentHeight;}} else {int width = mContentWidth;if (!mIsShrinking) {fromXDelta = mPosition == LEFT? -width : width;} else {toXDelta = mPosition == LEFT? -width : width;}if (mState == State.TRACKING) {if (Math.abs(mTrackX - fromXDelta) < Math.abs(mTrackX - toXDelta)) {mIsShrinking = !mIsShrinking;toXDelta = fromXDelta;}fromXDelta = (int) mTrackX;} elseif (mState == State.FLYING) {fromXDelta = (int) mTrackX;}// for FLYING events we calculate animation duration based on flying velocity// also for very high velocity make sure duration >= 20 msif (mState == State.FLYING && mLinearFlying) {calculatedDuration = (int) (1000 * Math.abs((toXDelta - fromXDelta) / mVelocity));calculatedDuration = Math.max(calculatedDuration, 20);} else {calculatedDuration = mDuration * Math.abs(toXDelta - fromXDelta) / mContentWidth;}}mTrackX = mTrackY = 0;if (calculatedDuration == 0) {mState = State.READY;if (mIsShrinking) {mContent.setVisibility(GONE);}postProcess();return;}animation = new TranslateAnimation(fromXDelta, toXDelta, fromYDelta, toYDelta);animation.setDuration(calculatedDuration);animation.setAnimationListener(animationListener);if (mState == State.FLYING && mLinearFlying) {animation.setInterpolator(new LinearInterpolator());} elseif (mInterpolator != null) {animation.setInterpolator(mInterpolator);}startAnimation(animation);}};private AnimationListener animationListener = new AnimationListener() {public void onAnimationEnd(Animation animation) {mState = State.READY;if (mIsShrinking) {mContent.setVisibility(GONE);}postProcess();}public void onAnimationRepeat(Animation animation) {}public void onAnimationStart(Animation animation) {mState = State.ANIMATING;}};private void postProcess() {if (mIsShrinking && mClosedHandle != null) {mHandle.setBackgroundDrawable(mClosedHandle);} elseif (!mIsShrinking && mOpenedHandle != null) {mHandle.setBackgroundDrawable(mOpenedHandle);}// invoke listener if anyif (panelListener != null) {if (mIsShrinking) {panelListener.onPanelClosed(Panel.this);} else {panelListener.onPanelOpened(Panel.this);}}}class PanelOnGestureListener implements OnGestureListener {float scrollY;float scrollX;public void setScroll(int initScrollX, int initScrollY) {scrollX = initScrollX;scrollY = initScrollY;}public boolean onDown(MotionEvent e) {scrollX = scrollY = 0;initChange();return true;}public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {mState = State.FLYING;mVelocity = mOrientation == VERTICAL? velocityY : velocityX;post(startAnimation);return true;}public void onLongPress(MotionEvent e) {// not used}public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {mState = State.TRACKING;float tmpY = 0, tmpX = 0;if (mOrientation == VERTICAL) {scrollY -= distanceY;if (mPosition == TOP) {tmpY = ensureRange(scrollY, -mContentHeight, 0);} else {tmpY = ensureRange(scrollY, 0, mContentHeight);}} else {scrollX -= distanceX;if (mPosition == LEFT) {tmpX = ensureRange(scrollX, -mContentWidth, 0);} else {tmpX = ensureRange(scrollX, 0, mContentWidth);}}if (tmpX != mTrackX || tmpY != mTrackY) {mTrackX = tmpX;mTrackY = tmpY;invalidate();}return true;}public void onShowPress(MotionEvent e) {// not used}public boolean onSingleTapUp(MotionEvent e) {// not usedreturn false;}}}
attrs.xml:
<?xml version="1.0" encoding="utf-8"?><resources> <declare-styleable name="Panel"> <!-- Defines panel animation duration in ms. --> <attr name="animationDuration" format="integer" /> <!-- Defines panel position on the screen. --> <attr name="position"> <!-- Panel placed at top of the screen. --> <enum name="top" value="0" /> <!-- Panel placed at bottom of the screen. --> <enum name="bottom" value="1" /> <!-- Panel placed at left of the screen. --> <enum name="left" value="2" /> <!-- Panel placed at right of the screen. --> <enum name="right" value="3" /> </attr> <!-- Identifier for the child that represents the panel's handle. --> <attr name="handle" format="reference" /> <!-- Identifier for the child that represents the panel's content. --> <attr name="content" format="reference" /> <!-- Defines if flying gesture forces linear interpolator in animation. --> <attr name="linearFlying" format="boolean" /> <!-- Defines size relative to parent (must be in form: nn%p). --> <attr name="weight" format="fraction" /> <!-- Defines opened handle (drawable/color). --> <attr name="openedHandle" format="reference|color" /> <!-- Defines closed handle (drawable/color). --> <attr name="closedHandle" format="reference|color" /> </declare-styleable> </resources>
更多相关文章
- 监听电话
- 在代码中设置RelativeLayout布局中标签的android:layout_toLeftO
- 搞清android.support.v4.app.Fragment和android.app.Fragment区
- java中采用Pull解析器对XML文件进行解析
- Android之TabLayout常见问题解决+TabLayout+ViewPager实现标签页
- Android官方文档之App Resources(下)
- Android(安卓)TabHost(简易用法)
- Android动画分类
- android tabHost布局之一 继承TabActivity并以activity布局