侧滑面板(对ViewGroup的自定义)
应用场景: 扩展主面板的功能
功能实现:1.ViewDragHelper: Google2013年IO大会提出的,>解决界面控件拖拽移动问题. (v4包下)
2.mTouchSlop 最小敏感范围, 值越小, 越敏感
伴随动画: 1. 左面板: 缩放动画, 平移动画, 透明度动画
2.主面板: 缩放动画
3.背景动画: 亮度变化 (颜色变化)
状态监听\触摸优化:1.设置并更新状态
2.触摸优化: 重写ViewGroup里onInterceptTouchEvent和onTouchEvent
1.下面是自定义ViewGroup的类 Draglayout(整个demo最主要的地方)
public class Draglayout extends FrameLayout {
private ViewDragHelper drag;
private ViewGroup mLeftContent;
private ViewGroup mMainContent;
private int mrange;
private int measuredHeight;
private int measuredWidth;
private Status status = Status.Close;// 默认状态
public Draglayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public Draglayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Draglayout(Context context) {
super(context);
init();
}
// 状态枚举
public static enum Status {
Close, Open, Draging;
}
// 定义接口,当开关状态改变的时候执行特定的方法
private onDragStatusChangeListener listener;
public void setDragStatusChangeListener(onDragStatusChangeListener mListener) {
this.listener = mListener;
}
public interface onDragStatusChangeListener {
void onClose();// 当关闭时
void onOpen();// 当打开时
void onDraging(float percent);// 当在滑动时
}
public Status getStatus() {
return status;
}
public void setStatus(Status status) {
this.status = status;
}
private void init() {
// 1.初始化(谷歌大会13推出解决界面拖拽问题)
drag = ViewDragHelper.create(this, callback);
}
ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
// 3.重写方法
@Override
public boolean tryCaptureView(View child, int pointerId) {
// 1. 根据返回结果决定当前child是否可以拖拽
// child 当前被拖拽的View
// pointerId 区分多点触摸的id
Log.d("tencent", "child" + child);
return true;
}
public void onViewCaptured(View capturedChild, int activePointerId) {
// 当被捕获时调用
Log.d("tencent", "capturedChild" + capturedChild);
};
public int getViewHorizontalDragRange(View child) {
// 返回拖拽的范围, 不对拖拽进行真正的限制. 仅仅决定了动画执行速度,通过这个范围计算出速度
return mrange;
};
// 就这么定义
public int clampViewPositionHorizontal(View child, int left, int dx) {
// 根据建议值 修正将要移动到的(横向)位置 (重要)
// 此时没有发生真正的移动
// child: 当前拖拽的View
// left 新的位置的建议值, dx 位置变化量
// left = oldLeft + dx;
//主页面显示的位置
if (child == mMainContent) {
left = fixLeft(left);
}
return left;
};
// public int clampViewPositionVertical(View child, int top, int dy) {
// return top;
// };
// 位置改变的时候
public void onViewPositionChanged(View changedView, int left, int top,
int dx, int dy) {
// 当View位置改变的时候, 处理要做的事情 (更新状态, 伴随动画, 重绘界面)
// 此时,View已经发生了位置的改变
int newLeft = left;
// 滑动侧边页,带动主页面滑动
if (changedView == mLeftContent) {
newLeft = mMainContent.getLeft() + dx;
}
newLeft = fixLeft(newLeft);// 修正
if (changedView == mLeftContent) {
//强制让左面板回到原始位置
mLeftContent.layout(0, 0, measuredWidth, measuredHeight);
mMainContent.layout(newLeft, 0, newLeft + measuredWidth,
measuredHeight);
}
// 设置伴随动画
dispatchDragEvent(newLeft);
invalidate();// 为了兼容低版本,保存设置
};
// 松手的时候设置页面的位置
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// View releasedChild 被释放的子View
// float xvel 水平方向的速度, 向右为+
// float yvel 竖直方向的速度, 向下为+
if (xvel == 0 && mMainContent.getLeft() > mrange / 2.0f) {
open();
} else if (xvel > 0) {
open();
} else {
close();
}
}
};
// 设置限制位置
protected int fixLeft(int left) {
if (left < 0)
return 0;
if (left > mrange)
return mrange;
return left;
}
/** 状态改变设置动画 */
protected void dispatchDragEvent(int newLeft) {
// 状态改变的时候设置动画百分比
float percent = newLeft / (1.0f * mrange);
// 伴随动画
animationViews(percent);
// 设置监听执行
if (listener != null) {
listener.onDraging(percent);
}
//记录状态
Status preStatus = status;
//根据percent 状态发生改变
if (percent == 0f) {
status = Status.Close;
} else if (percent == 1.0f) {
status = Status.Open;
} else {
status = Status.Draging;
}
//如果不等于上次的状态,发生了变化
if (status != preStatus) {
if (status == Status.Close) {
if (listener != null)
listener.onClose();
} else if (status == Status.Open) {
if (listener != null)
listener.onOpen();
}
}
}
private void animationViews(float percent) {
// * 伴随动画:
// > 1. 左面板: 缩放动画, 平移动画, 透明度动画
// > 2. 主面板: 缩放动画
// > 3. 背景动画: 亮度变化 (颜色变化)
// > 1. 左面板: 缩放动画, 平移动画, 透明度动画
// 缩放动画 从0.5--》1
// mLeftContent.setScaleX(0.5f*percent+0.5f);
// mLeftContent.setScaleY(0.5f*percent+0.5f);
ViewHelper.setScaleX(mLeftContent, 0.5f * percent + 0.5f);// 为了兼容低版本,9个低版本兼容动画
ViewHelper.setScaleY(mLeftContent, evaluate(percent, 0.5f, 1.0f));
// 平移动画 从 -measuredWidth/2.0f---> 0.0f
ViewHelper.setTranslationX(mLeftContent,
evaluate(percent, -measuredWidth / 2.0f, 0.0f));
// 透明度 从0.5f-->1.0f
ViewHelper.setAlpha(mLeftContent, evaluate(percent, 0.5f, 1.0f));
// > 2. 主面板: 缩放动画
// 1.0f -> 0.8f
ViewHelper.setScaleX(mMainContent, evaluate(percent, 1.0f, 0.8f));
ViewHelper.setScaleY(mMainContent, evaluate(percent, 1.0f, 0.8f));
// > 3. 背景动画: 亮度变化 (颜色变化)
getBackground().setColorFilter((Integer) evaluateColor(
percent, Color.BLACK,Color.TRANSPARENT), Mode.SRC_OVER);
}
// 数值估值器
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
// 颜色估值器
public Object evaluateColor(float fraction, Object startValue,
Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int) ((startA + (int) (fraction * (endA - startA))) << 24)
| (int) ((startR + (int) (fraction * (endR - startR))) << 16)
| (int) ((startG + (int) (fraction * (endG - startG))) << 8)
| (int) ((startB + (int) (fraction * (endB - startB))));
}
protected void open() {
open(true);
}
protected void close() {
close(true);
}
@Override
public void computeScroll() {
super.computeScroll();
// 持续平滑动画 (高频率调用)
if (drag.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
private void open(boolean issmooth) {
int finalleft = mrange;
if (issmooth) {
if (drag.smoothSlideViewTo(mMainContent, finalleft, 0)) {// 左和上的位置
ViewCompat.postInvalidateOnAnimation(this);
}
} else {
mMainContent.layout(finalleft, 0, measuredWidth + finalleft,
measuredHeight);
}
}
private void close(boolean issmooth) {
int finalleft = 0;
// 判断是否需要设置平滑
if (issmooth) {
if (drag.smoothSlideViewTo(mMainContent, finalleft, 0)) {
ViewCompat.postInvalidateOnAnimation(this);
}
} else {
mMainContent.layout(finalleft, 0, measuredWidth + finalleft,
measuredHeight);
}
}
// 2.传递触摸事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return drag.shouldInterceptTouchEvent(ev);
}
// public boolean dispatchTouchEvent(android.view.MotionEvent ev) {
// return true;
// };
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
drag.processTouchEvent(event);// 传递给drag
} catch (Exception e) {
e.printStackTrace();
}
// 返回true,持续接收事件
return true;
}
// 获得自定义控件里面的子view
@Override
protected void onFinishInflate() {
super.onFinishInflate();
// Github
// 写注释
// 容错性检查 (至少有俩子View, 子View必须是ViewGroup的子类)
if (getChildCount() < 2) {
throw new IllegalStateException(
"布局至少有俩孩子. Your ViewGroup must have 2 children at least.");
}
if (!(getChildAt(0) instanceof ViewGroup && getChildAt(1) instanceof ViewGroup)) {
throw new IllegalArgumentException(
"子View必须是ViewGroup的子类. Your children must be an instance of ViewGroup");
}
mLeftContent = (ViewGroup) getChildAt(0);
mMainContent = (ViewGroup) getChildAt(1);
}
// 获得测量宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measuredHeight = getMeasuredHeight();
measuredWidth = getMeasuredWidth();
mrange = (int) (measuredWidth * 0.6f);
}
}
2.MainActivity类(填充数据,设置监听)
public class MainActivity extends Activity {
private Draglayout draglayout;
private ListView leftLV;
private ListView mainLV;
private ImageView iv_header;
private MyLinearlayout myLinearlayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
initView();
initData();
}
private void initView() {
draglayout = (Draglayout) findViewById(R.id.dl);
leftLV = (ListView) findViewById(R.id.lv_left);
mainLV = (ListView) findViewById(R.id.lv_main);
iv_header = (ImageView) findViewById(R.id.iv_header);
myLinearlayout = (MyLinearlayout) findViewById(R.id.mainLL);
}
private void initData() {// 监听
draglayout.setDragStatusChangeListener(new onDragStatusChangeListener() {
@Override
public void onOpen() {
Utils.showToast(MainActivity.this, "打开");
Random random = new Random();
int position = random.nextInt(20);
leftLV.smoothScrollToPosition(position);// 随机位置
}
@Override
public void onDraging(float percent) {
// iv_header.setAlpha(1-percent);//设置图标的透明度
ViewHelper.setAlpha(iv_header, 1 - percent);
}
@Override
public void onClose() {
Utils.showToast(MainActivity.this, "关闭");
// 设置图标的震动
// 图标反复移动的偏移
ObjectAnimator ofFloat = ObjectAnimator.ofFloat(
iv_header, "translationX", 10.0f);
ofFloat.setInterpolator(new CycleInterpolator(1));// 来回一次
ofFloat.setDuration(200);
ofFloat.start();
}
});
myLinearlayout.setDraglayout(draglayout);
// 设置适配器
leftLV.setAdapter(new ArrayAdapter<String>(MainActivity.this,
android.R.layout.simple_list_item_1, Cheeses.sCheeseStrings) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);
TextView textView = (TextView) view;
textView.setTextColor(Color.WHITE);
return textView;
}
});
mainLV.setAdapter(new ArrayAdapter<String>(MainActivity.this,
android.R.layout.simple_list_item_1, Cheeses.NAMES));
}
}
4.数据
public class Cheeses {
public static final String[] sCheeseStrings = {
"Abbaye de Belloc", "Abbaye du Mont des Cats", "Abertam", "Abondance", "Ackawi",
"Acorn", "Adelost", "Affidelice au Chablis", "Afuega'l Pitu", "Airag", "Airedale",
"Aisy Cendre", "Allgauer Emmentaler", "Alverca", "Ambert", "American Cheese",
"Ami du Chambertin", "Anejo Enchilado", "Anneau du Vic-Bilh", "Anthoriro", "Appenzell",
"Aragon", "Ardi Gasna", "Ardrahan", "Armenian String", "Aromes au Gene de Marc",
"Asadero", "Asiago", "Aubisque Pyrenees", "Autun", "Avaxtskyr", "Baby Swiss",
"Babybel", "Baguette Laonnaise", "Bakers", "Baladi", "Balaton", "Bandal", "Banon",
"Barry's Bay Cheddar", "Basing", "Basket Cheese", "Bath Cheese", "Bavarian Bergkase",
"Baylough", "Beaufort", "Beauvoorde", "Beenleigh Blue", "Beer Cheese", "Bel Paese",
"Bergader", "Bergere Bleue", "Berkswell", "Beyaz Peynir", "Bierkase", "Bishop Kennedy",
"Blarney", "Bleu d'Auvergne", "Bleu de Gex", "Bleu de Laqueuille",
"Bleu de Septmoncel", "Bleu Des Causses", "Blue", "Blue Castello", "Blue Rathgore",
"Blue Vein (Australian)", "Blue Vein Cheeses", "Bocconcini", "Bocconcini (Australian)",
"Boeren Leidenkaas", "Bonchester", "Bosworth", "Bougon", "Boule Du Roves",
"Boulette d'Avesnes", "Boursault", "Boursin", "Bouyssou", "Bra", "Braudostur",
"Breakfast Cheese", "Brebis du Lavort", "Brebis du Lochois", "Brebis du Puyfaucon",
"Bresse Bleu", "Brick", "Brie", "Brie de Meaux", "Brie de Melun", "Brillat-Savarin",
"Brin", "Brin d' Amour", "Brin d'Amour", "Brinza (Burduf Brinza)",
"Briquette de Brebis", "Briquette du Forez", "Broccio", "Broccio Demi-Affine",
"Brousse du Rove", "Bruder Basil", "Brusselae Kaas (Fromage de Bruxelles)", "Bryndza",
"Buchette d'Anjou", "Buffalo", "Burgos", "Butte", "Butterkase", "Button (Innes)",
"Buxton Blue", "Cabecou", "Caboc", "Cabrales", "Cachaille", "Caciocavallo", "Caciotta",
"Caerphilly", "Cairnsmore", "Calenzana", "Cambazola", "Camembert de Normandie",
"Canadian Cheddar", "Canestrato", "Cantal", "Caprice des Dieux", "Capricorn Goat",
"Capriole Banon", "Carre de l'Est", "Casciotta di Urbino", "Cashel Blue", "Castellano",
"Castelleno", "Castelmagno", "Castelo Branco", "Castigliano", "Cathelain",
"Celtic Promise", "Cendre d'Olivet", "Cerney", "Chabichou", "Chabichou du Poitou",
"Chabis de Gatine", "Chaource", "Charolais", "Chaumes", "Cheddar",
"Cheddar Clothbound", "Cheshire", "Chevres", "Chevrotin des Aravis", "Chontaleno",
"Civray", "Coeur de Camembert au Calvados", "Coeur de Chevre", "Colby", "Cold Pack",
"Comte", "Coolea", "Cooleney", "Coquetdale", "Corleggy", "Cornish Pepper",
"Cotherstone", "Cotija", "Cottage Cheese", "Cottage Cheese (Australian)",
"Cougar Gold", "Coulommiers", "Coverdale", "Crayeux de Roncq", "Cream Cheese",
"Cream Havarti", "Crema Agria", "Crema Mexicana", "Creme Fraiche", "Crescenza"
};
public static final String[] NAMES = new String[] { "宋江", "卢俊义", "吴用",
"公孙胜", "关胜", "林冲", "秦明", "呼延灼", "花荣", "柴进", "李应", "朱仝", "鲁智深",
"武松", "董平", "张清", "杨志", "徐宁", "索超", "戴宗", "刘唐", "李逵", "史进", "穆弘",
"雷横", "李俊", "阮小二", "张横", "阮小五", " 张顺", "阮小七", "杨雄", "石秀", "解珍",
" 解宝", "燕青", "朱武", "黄信", "孙立", "宣赞", "郝思文", "韩滔", "彭玘", "单廷珪",
"魏定国", "萧让", "裴宣", "欧鹏", "邓飞", " 燕顺", "杨林", "凌振", "蒋敬", "吕方",
"郭 盛", "安道全", "皇甫端", "王英", "扈三娘", "鲍旭", "樊瑞", "孔明", "孔亮", "项充",
"李衮", "金大坚", "马麟", "童威", "童猛", "孟康", "侯健", "陈达", "杨春", "郑天寿",
"陶宗旺", "宋清", "乐和", "龚旺", "丁得孙", "穆春", "曹正", "宋万", "杜迁", "薛永", "施恩",
"周通", "李忠", "杜兴", "汤隆", "邹渊", "邹润", "朱富", "朱贵", "蔡福", "蔡庆", "李立",
"李云", "焦挺", "石勇", "孙新", "顾大嫂", "张青", "孙二娘", " 王定六", "郁保四", "白胜",
"时迁", "段景柱" };
}
5.utils类
public class Utils {
public static Toast mToast;
public static void showToast(Context mContext, String msg) {
if (mToast == null) {
mToast = Toast.makeText(mContext, "", Toast.LENGTH_SHORT);
}
mToast.setText(msg);
mToast.show();
}
/**
* dip 转换成 px
* @param dip
* @param context
* @return
*/
public static float dip2Dimension(float dip, Context context) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, displayMetrics);
}
/**
* @param dip
* @param context
* @param complexUnit {@link TypedValue#COMPLEX_UNIT_DIP} {@link TypedValue#COMPLEX_UNIT_SP}}
* @return
*/
public static float toDimension(float dip, Context context, int complexUnit) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return TypedValue.applyDimension(complexUnit, dip, displayMetrics);
}
/** 获取状态栏高度
* @param v
* @return
*/
public static int getStatusBarHeight(View v) {
if (v == null) {
return 0;
}
Rect frame = new Rect();
v.getWindowVisibleDisplayFrame(frame);
return frame.top;
}
public static String getActionName(MotionEvent event) {
String action = "unknow";
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_DOWN:
action = "ACTION_DOWN";
break;
case MotionEvent.ACTION_MOVE:
action = "ACTION_MOVE";
break;
case MotionEvent.ACTION_UP:
action = "ACTION_UP";
break;
case MotionEvent.ACTION_CANCEL:
action = "ACTION_CANCEL";
break;
case MotionEvent.ACTION_SCROLL:
action = "ACTION_SCROLL";
break;
case MotionEvent.ACTION_OUTSIDE:
action = "ACTION_SCROLL";
break;
default:
break;
}
return action;
}
}
5.MyLinearlayout类,这个类是对打开状态下,拦截子listView中的滑动事件
public class MyLinearlayout extends LinearLayout{
private Draglayout draglayout;
public MyLinearlayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyLinearlayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyLinearlayout(Context context) {
super(context);
}
public void setDraglayout(Draglayout draglayout){
this.draglayout = draglayout;
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//如果不是关闭状态,则拦截掉,不让其处理滑动事件,否则原先处理
if(( draglayout).getStatus()==Status.Close){
return super.onInterceptTouchEvent(ev);
}else{
return true;
}
}
public boolean onTouchEvent(MotionEvent event) {
//如果是关闭状态,原先处理
if(( draglayout).getStatus()==Status.Close){
return super.onTouchEvent(event);
}else{//否则当手指抬起的时候就关闭
if(event.getAction()==MotionEvent.ACTION_UP){
draglayout.close();
}
return true;
}
}
}