前面一段時(shí)間呢,仿照最新版的QQ分別實(shí)現(xiàn)了幾個(gè)自定義控件
Android自定義控件:類QQ抽屜效果
Android自定義控件:類QQ未讀消息拖拽效果
Android自定義控件:打造自己的QQ空間主頁(yè)
今天繼續(xù)再實(shí)現(xiàn)一個(gè)仿照QQ的側(cè)滑菜單欄效果,先看看我們最終實(shí)現(xiàn)的效果:
分步來(lái)看,我們需要實(shí)現(xiàn)以下效果:
- 透明狀態(tài)欄
- View的拖動(dòng)效果
- 主界面View黑色遮幕效果
- menu打開時(shí),menu的圖片自動(dòng)滾動(dòng)
實(shí)現(xiàn)原理
透明狀態(tài)欄
首先說(shuō)說(shuō)透明狀態(tài)欄,透明狀態(tài)欄呢,上一篇文章Android自定義控件:打造自己的QQ空間主頁(yè)已經(jīng)講過(guò),這里就不再多說(shuō),如果有疑問,參考上篇文章或者直接看源碼都是可以的,實(shí)現(xiàn)起來(lái)并不難。
基本實(shí)現(xiàn)原理
網(wǎng)上其實(shí)有各式各樣的實(shí)現(xiàn)方式,其實(shí)都是大同小異,我們這里采用自定義FramLayout的方式來(lái)實(shí)現(xiàn),也就是一個(gè)底層menu layout上面蓋一層main layout,然后通過(guò)ViewDragHelper來(lái)處理子view的拖動(dòng)事件,動(dòng)態(tài)的更新兩個(gè)view的位置,就可以實(shí)現(xiàn)拖拽效果了,實(shí)現(xiàn)起來(lái)其實(shí)還是挺簡(jiǎn)單的,代碼注釋也比較全,這里直接上代碼:
main布局
<com.zyw.horrarndoo.slidemenu.view.SlideMenu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/slide_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="com.zyw.horrarndoo.slidemenu.MainActivity">
<include layout="@layout/layout_menu"/>
<include layout="@layout/layout_main"/>
</com.zyw.horrarndoo.slidemenu.view.SlideMenu>
public class SlideMenu extends FrameLayout {
private View menuView;//菜單view
private SlideFrameLayout mainView;//主界面view
private ImageView iv_main_src;
private ViewDragHelper viewDragHelper;
private float dragRange;//最大拖拽范圍,mainView的最大left
private IntEvaluator intEvaluator;//int的計(jì)算器
private GestureDetectorCompat mGestureDetector;
private boolean isTouchDrag;//是否touch拖拽SlideMenu,因?yàn)榉攀种蟠嬖谝粋€(gè)彈回去的過(guò)程
//此時(shí)onViewPositionChanged一樣會(huì)回調(diào),需要區(qū)分自動(dòng)彈回去還是touch拖動(dòng)
public SlideMenu(Context context) {
this(context, null);
}
public SlideMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 定義狀態(tài)常量
*/
public enum DragState {
Open, Close
}
private DragState currentState = DragState.Close;//默認(rèn)關(guān)閉
private void init(Context context) {
viewDragHelper = ViewDragHelper.create(this, callback);
intEvaluator = new IntEvaluator();
//通過(guò)手勢(shì)判斷器判斷touch事件是否消費(fèi)掉
mGestureDetector = new GestureDetectorCompat(context, new GestureDetector
.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float
distanceY) {
//滑動(dòng)的時(shí)候 只有x坐標(biāo)改變值>=y坐標(biāo)改變值時(shí),才消費(fèi)事件
return Math.abs(distanceX) >= Math.abs(distanceY);
}
});
}
/**
* 獲取當(dāng)前的狀態(tài)
*
* @return
*/
public DragState getCurrentState() {
return currentState;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 2) {
throw new IllegalArgumentException("SlideMenu only can 2 children!");
}
menuView = getChildAt(0);
mainView = (SlideFrameLayout) getChildAt(1);
iv_main_src = (ImageView) mainView.getChildAt(1);
}
/**
* onMeasure執(zhí)行完之后執(zhí)行
* 初始化自己和子View的寬高
*
* @param w
* @param h
* @param oldw
* @param oldh
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
dragRange = getMeasuredWidth() * 0.8f;
}
private float eventX;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//由viewDragHelper和手勢(shì)判斷器判斷是否需要攔截touch事件
return viewDragHelper.shouldInterceptTouchEvent(ev) & mGestureDetector.onTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//由viewDragHelper處理touch事件
viewDragHelper.processTouchEvent(event);
//消費(fèi)掉事件
return true;
}
private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
/**
* 用于判斷是否捕獲當(dāng)前child的觸摸事件
* @param child
* 當(dāng)前觸摸的子View
* @param pointerId
* @return
* true:捕獲并解析
* false:不處理
*/
@Override
public boolean tryCaptureView(View child, int pointerId) {
isTouchDrag = true;
return child == menuView || child == mainView;
}
/**
* 獲取view水平方向的拖拽范圍,不能限制拖拽范圍
* @param child
* 拖拽的child view
* @return
* 拖拽范圍
*/
public int getViewHorizontalDragRange(View child) {
return (int) dragRange;
}
/**
* 控制child在水平方向的移動(dòng)
* @param child
* 控制移動(dòng)的view
* @param left
* ViewDragHelper判斷當(dāng)前child的left改變的值
* @param dx
* 本次child水平方向移動(dòng)的距離
* @return
* child最終的left值
*/
public int clampViewPositionHorizontal(View child, int left, int dx) {
if (child == mainView) {
if (left < 0)
left = 0;//限制mainView的左邊
if (left > dragRange)
left = (int) dragRange;//限制mainView的右邊
}
return left;
}
/**
* child位置改變的時(shí)候執(zhí)行,一般用來(lái)做其它子View的伴隨移動(dòng)
* @param changedView
* 位置改變的view
* @param left
* child當(dāng)前最新的left
* @param top
* child當(dāng)前最新的top
* @param dx
* 本次水平移動(dòng)的距離
* @param dy
* 本次垂直移動(dòng)的距離
*/
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if (changedView == menuView) {
//固定住menuView
menuView.layout(0, 0, menuView.getMeasuredWidth(), menuView.getMeasuredHeight());
//讓mainView移動(dòng)起來(lái)
int newLeft = mainView.getLeft() + dx;
if (newLeft < 0)
newLeft = 0;//限制mainView的左邊
if (newLeft > dragRange) {
newLeft = (int) dragRange;//限制mainView的右邊
}
mainView.layout(newLeft, mainView.getTop() + dy, newLeft + mainView
.getMeasuredWidth(), mainView.getBottom() + dy);
}
//1.計(jì)算滑動(dòng)的百分比
float fraction = mainView.getLeft() / dragRange;
//2.執(zhí)行伴隨動(dòng)畫
executeAnim(fraction);
//只有touch拖動(dòng)view導(dǎo)致child位置變化時(shí)才回調(diào)onDrag
if(isTouchDrag) {
//將drag的fraction暴漏給外界
if (listener != null) {
listener.onDrag(fraction);
}
}
}
/**
* 手指抬起的時(shí)候執(zhí)行
* @param releasedChild
* 當(dāng)前抬起的child view
* @param xvel
* x方向移動(dòng)的速度 負(fù):向做移動(dòng) 正:向右移動(dòng)
* @param yvel
* y方向移動(dòng)的速度
*/
public void onViewReleased(View releasedChild, float xvel, float yvel) {
isTouchDrag = false;
if (mainView.getLeft() < dragRange / 2) {
//在左半邊
close();
} else {
//在右半邊
open();
}
//處理用戶的稍微滑動(dòng)
if (xvel > 200 && currentState != DragState.Open) {
open();
} else if (xvel < -200 && currentState != DragState.Close) {
close();
}
}
};
/**
* 打開菜單
*/
public void close() {
//更改狀態(tài)為關(guān)閉,并回調(diào)關(guān)閉的方法
if (listener != null) {
listener.onClose();
currentState = DragState.Close;
}
viewDragHelper.smoothSlideViewTo(mainView, 0, mainView.getTop());
ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
}
/**
* 打開菜單
*/
public void open() {
//更改狀態(tài)為打開,并回調(diào)打開的方法
if (listener != null) {
listener.onOpen();
currentState = DragState.Open;
}
viewDragHelper.smoothSlideViewTo(mainView, (int) dragRange, mainView.getTop());
ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
}
/**
* 執(zhí)行伴隨動(dòng)畫
*
* @param fraction
*/
private void executeAnim(float fraction) {
//移動(dòng)menuView
ViewHelper.setTranslationX(menuView, intEvaluator.evaluate(fraction, -menuView
.getMeasuredWidth() / 2, 0));
}
public void computeScroll() {
if (viewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(SlideMenu.this);
}
}
private OnDragStateChangeListener listener;
public void setOnDragStateChangeListener(OnDragStateChangeListener listener) {
this.listener = listener;
}
/**
* 拖拽監(jiān)聽接口
*/
public interface OnDragStateChangeListener {
/**
* 打開回調(diào)
*/
void onOpen();
/**
* 關(guān)閉回調(diào)
*/
void onClose();
/**
* 拖拽中回調(diào)
*
* @param fraction
*/
void onDrag(float fraction);
}
public void destory(){
try {
iv_main_src.getBackground().clearColorFilter();//清除黑色過(guò)濾效果
}catch (Exception e){
e.printStackTrace();
}
}
}
兩個(gè)子布局的代碼比較簡(jiǎn)單,就不貼出來(lái)了,根據(jù)代碼我們可以看到,由于main layout中有一個(gè)子listview,所以肯定存在事件判斷,我們這里通過(guò)GestureDetectorCompat和ViewDragHelper結(jié)合判斷用戶的左右滑動(dòng)和上下滑動(dòng),從而判斷自定義slideMenu是否攔截消費(fèi)touch事件。還有一點(diǎn)就是,拖動(dòng)main layout的時(shí)候,menu layout并不是不動(dòng)的,而是跟著main layout有一個(gè)拖拽視差效果,我們通過(guò)一個(gè)屬性動(dòng)畫來(lái)實(shí)現(xiàn)。
至于TitleBar顏色變化呢,我們這里通過(guò)一個(gè)接口回調(diào),將slideMenu的狀態(tài)告知給Activity,然后在Activity中設(shè)置TitleBar的背景色就可以了,Activity的具體代碼如下。
public class MainActivity extends AppCompatActivity {
private SlideMenu slideMenu;
private SlideFrameLayout sll_layout;
private ListView lv_main;
private LinearLayout ll_title;
private TextView tv_title;
private List<String> list = new ArrayList<>();
private MovingImageView miv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initState();
initView();
initData();
}
private void initData() {
slideMenu.setOnDragStateChangeListener(new SlideMenu.OnDragStateChangeListener() {
@Override
public void onOpen() {
Log.e("tag", "onOpen");
if(miv.getMovingState() == MovingState.stop) {
miv.startMoving();
}else if(miv.getMovingState() == MovingState.pause){
miv.resumeMoving();
}
setOpenTitle();
}
@Override
public void onDrag(float fraction) {
//Log.e("tag", "onDrag fraction:" + fraction);
if(fraction >= 0.6f){
setOpenTitle();
}else{
setCloseTitle();
}
miv.pauseMoving();
}
@Override
public void onClose() {
Log.e("tag", "onClose");
miv.stopMoving();
setCloseTitle();
}
});
sll_layout.setSlideMenu(slideMenu);
}
private void initView() {
setContentView(R.layout.activity_main);
slideMenu = (SlideMenu) findViewById(R.id.slide_menu);
sll_layout = (SlideFrameLayout) findViewById(R.id.sll_layout);
ll_title = (LinearLayout) findViewById(R.id.ll_title);
tv_title = (TextView) findViewById(R.id.tv_title);
lv_main = (ListView) findViewById(R.id.lv_main);
miv = (MovingImageView) findViewById(R.id.miv_menu);
initList();
lv_main.setAdapter(new SlideMainAdapter(this, list));
lv_main.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(MainActivity.this, "item - " + position + " is clicked.", Toast
.LENGTH_SHORT).show();
}
});
}
private void setOpenTitle(){
ll_title.setBackgroundColor(Color.WHITE);
tv_title.setTextColor(Color.parseColor("#00ccff"));
}
private void setCloseTitle(){
ll_title.setBackgroundColor(Color.parseColor("#00ccff"));
tv_title.setTextColor(Color.WHITE);
}
private void initList() {
for (int i = 0; i < 50; i++) {
list.add("content - " + i);
}
}
/**
* 初始化狀態(tài)欄狀態(tài)
* 設(shè)置Activity狀態(tài)欄透明效果
* 隱藏ActionBar
*/
private void initState() {
//將狀態(tài)欄設(shè)置成透明色
UIUtils.setBarColor(this, Color.TRANSPARENT);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
}
@Override
protected void onDestroy() {
if(slideMenu != null){
slideMenu.destory();
}
super.onDestroy();
}
}
先來(lái)看看我們以上代碼實(shí)現(xiàn)的效果。
黑色遮幕效果
黑色遮幕效果是指menu打開時(shí),main layout表層的一層半透明黑色背景。
最先想到的方法是通過(guò)設(shè)置View的ColorFilter,及設(shè)置view的顏色過(guò)濾器,在我們執(zhí)行伴隨動(dòng)畫的時(shí)候,根據(jù)fraction動(dòng)態(tài)的設(shè)置View 的ColorFilter值。但是這里要一定要注意一點(diǎn),由于這里是設(shè)置View背景的ColorFilter,這就會(huì)導(dǎo)致一個(gè)問題,那就是View的子控件是沒有設(shè)置ColorFilter的,也就是說(shuō)直接設(shè)置main layout 的ColorFilter的話,會(huì)導(dǎo)致只有main layout本身有遮幕效果,而所有的子控件依然是原來(lái)的樣子。
最后我是通過(guò)在整個(gè)main layout表層蓋一個(gè)透明背景的view,然后設(shè)置這個(gè)表層view的ColorFilter,這樣實(shí)現(xiàn)整個(gè)main layout的黑色遮幕效果。這個(gè)表層的view必須要設(shè)置背景,不然無(wú)法設(shè)置view的ColorFilter,因?yàn)槲覀冊(cè)O(shè)置的是View 的background的ColorFilter。
實(shí)現(xiàn)遮幕效果的方式當(dāng)然不止一種,設(shè)置ColorFilter的方式是比較推薦的一種,還有一種更為簡(jiǎn)單粗暴的方式,那就是直接設(shè)置表層View的背景色,至于具體代碼如下。
main layout布局
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sll_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/main_background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/title_bar_layout"/>
<ListView
android:id="@+id/lv_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
</LinearLayout>
<!--為了實(shí)現(xiàn)主界面變暗效果,給整個(gè)界面先加一層透明背景的view-->
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/transparent"/>
</FrameLayout>
/**
* 執(zhí)行伴隨動(dòng)畫
*
* @param fraction
*/
private void executeAnim(float fraction) {
//移動(dòng)menuView
ViewHelper.setTranslationX(menuView, intEvaluator.evaluate(fraction, -menuView
.getMeasuredWidth() / 2, 0));
//給mainView設(shè)置黑色的遮罩效果
//實(shí)際上是設(shè)置mainView的表層ImageView顏色過(guò)濾效果,達(dá)到設(shè)置整個(gè)view黑色遮罩效果
// iv_main_src.setBackgroundColor((Integer) ColorUtil.evaluateColor(fraction, Color
// .TRANSPARENT,
// Color.parseColor("#33000000")));
iv_main_src.getBackground().setColorFilter((Integer) ColorUtil.evaluateColor(fraction, Color
.TRANSPARENT,
Color.parseColor("#33000000")), PorterDuff.Mode.SCREEN);
}catch (Exception e){
e.printStackTrace();
}
}
然后再看看我們的效果
menu圖片自動(dòng)滾動(dòng)
這個(gè)效果是將github上一個(gè)開源項(xiàng)目做了簡(jiǎn)單的修改之后實(shí)現(xiàn)的:https://github.com/AlbertGrobas/MovingImageView,其實(shí)最基本的思想也比較簡(jiǎn)單,就是通過(guò)測(cè)量出背景圖片的寬高和imageView寬高的比值,然后填充imageView,根據(jù)比值來(lái)判斷是滾動(dòng)方式(上下/左右/對(duì)角),滾動(dòng)的具體方法則是通過(guò)屬性動(dòng)畫來(lái)實(shí)現(xiàn)。其實(shí)注釋也比較全,這里只是簡(jiǎn)單的說(shuō)一下實(shí)現(xiàn)的基本思路。
最后我們的效果就是,打開的時(shí)候暫停滾動(dòng),關(guān)閉的時(shí)候停止?jié)L動(dòng),手松開的時(shí)候根據(jù)menu狀態(tài)判斷是繼續(xù)滾動(dòng)還是重新開始滾動(dòng)。
最后附上完整demo地址:https://github.com/Horrarndoo/SlideMenu