闲着无事,见到目前比较多的应用都用到了"左边菜单右边内容页"这样的形式展示数据,于是也着手写了一个。
照例先上运行效果图:
源代码下载地址:
下面是结构:
首先介绍HorizontalMenuView这个View,这是一个继承ViewGroup的View,也是最主要的一部分,由于这个类代码比较长,就只捡核心点的列出来。
HorizontalMenuView里有两个控件,都是由代码创建的,分别是一个ListView(用于放置菜单项)和一个LinearLayout(用于放置内容页)。
- lv_menu = new ListView(context);
- LayoutParams params = new LayoutParams(childWidths[0], LayoutParams.FILL_PARENT);
- lv_menu.setLayoutParams(params);
- lv_menu.setCacheColorHint(Color.TRANSPARENT);
- lv_menu.setBackgroundColor(Color.WHITE);
- lv_menu.setFocusable(false);
- addView(lv_menu);
- ll_content = new LinearLayout(context);
- params = new LayoutParams(childWidths[1], LayoutParams.FILL_PARENT);
- ll_content.setOrientation(LinearLayout.HORIZONTAL);
- ll_content.setLayoutParams(params);
- ll_content.setBackgroundColor(Color.GRAY);
- addView(ll_content);
至于宽度是根据屏幕的宽度所设置的。
另外,必须实现onLayout()和onMeasure(),否则这个View将无法正常显示。这两个方法用于计算子View的宽高度及所画的位置。
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- int childLeft = 0;
- int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- View childView = getChildAt(i);
- if (childView.getVisibility() != View.GONE) {
- int childWidth = childWidths[i];
- childView.layout(childLeft, 0, childLeft + childWidth,
- childView.getMeasuredHeight());
- childLeft += childWidth;
- }
- }
- }
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- int count = getChildCount();
- for (int i = 0; i < count; i++) {
- getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
- }
- }
实现了显示之后,就要令view能够滚动了,主要是实现onTouchEvent()和computeScrollI()方法,当然,还需要一个scroller对象。
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- // 如果这个方法return true, 那么MotionEvent事件将不会往下传递
- // 如果这个方法return false, 那么MotionEvent事件将会往下传递
- int action = event.getAction();
- float x = event.getX();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- if (velocityTracker == null) {
- velocityTracker = VelocityTracker.obtain();
- velocityTracker.addMovement(event);
- }
- if (!scroller.isFinished()) {
- scroller.abortAnimation();
- }
- mLastMotionX = x;
- break;
- case MotionEvent.ACTION_MOVE:
- int deltaX = (int) (mLastMotionX - x);
- if (isCanMove(deltaX)) {
- if (velocityTracker != null) {
- velocityTracker.addMovement(event);
- }
- scrollBy(deltaX, 0);
- }
- // 越界判断
- if (getScrollX() < 0) {
- scrollTo(0, 0);
- }
- if (getScrollX() > childWidths[0]) {
- scrollTo(childWidths[0], 0);
- }
- mLastMotionX = x;
- break;
- case MotionEvent.ACTION_UP:
- int velocityX = 0;
- if (velocityTracker != null) {
- velocityTracker.addMovement(event);
- velocityTracker.computeCurrentVelocity(1000);
- velocityX = (int) velocityTracker.getXVelocity();
- }
- if (velocityX > SNAP_VELOCITY) {
- snapToScreen(MENU_PAGE);
- } else if (velocityX < -SNAP_VELOCITY) {
- snapToScreen(CONTENT_PAGE);
- } else {
- snapToDestination();
- }
- if (velocityTracker != null) {
- velocityTracker.recycle();
- velocityTracker = null;
- }
- break;
- }
- return true;
- }
- @Override
- public void computeScroll() {
- if (scroller.computeScrollOffset()) {
- scrollTo(scroller.getCurrX(), scroller.getCurrY());
- postInvalidate();
- }
- }
其中,snapToScreen()方法就实现了滚动动画,从而在手指抬起的时候,根据判断会滚动到相应的位置。
- /**
- * 跳到指定页
- * @param whichScreen
- */
- private void snapToScreen(int whichScreen) {
- if ((whichScreen == MENU_PAGE && getScrollX() != 0)
- || (whichScreen == CONTENT_PAGE && getScrollX() != childWidths[0])) {
- int delta = 0;
- if (whichScreen == MENU_PAGE) {
- delta = 0 - getScrollX();
- } else {
- delta = childWidths[0] - getScrollX();
- }
- scroller.startScroll(getScrollX(), 0, delta, 0, Math.abs(delta) * 2);
- currentPage = whichScreen;
- invalidate();
- }
- }
这样,大概就实现一大部分了,然后就是当Menu的ListView点击时,打开相应的内容页。
- lv_menu.setOnItemClickListener(new OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView<?> parent, View view,
- int position, long id) {
- menu_selected = position;
- for (int i = 0, count = parent.getChildCount(); i < count; i++) {
- int textColor = Color.GRAY;
- if (menu_selected == i) {
- textColor = Color.DKGRAY;
- }
- ((TextView) parent.getChildAt(i)).setTextColor(textColor);
- }
- openContentPage();
- snapToScreen(CONTENT_PAGE);
- }
- });
- /**
- * 打开内容页
- */
- private void openContentPage() {
- if (menuData != null) {
- Intent intent = menuData.get(menu_selected).getIntent();
- if (intent != null && context instanceof ActivityGroup) {
- ll_content.removeAllViews();
- destroyActivityFromGroup(context, "content");
- Window contentActivity = ((ActivityGroup) context)
- .getLocalActivityManager().startActivity("content",
- intent);
- LayoutParams params = new LayoutParams(
- LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
- ll_content.addView(contentActivity.getDecorView(), params);
- invalidate();
- }
- }
- }
这里注意下,由于要加入内容页,内容页为Activity的View,所以这个View需要在ActivityGroup中使用。
接着我们的ActivityGroup就可以使用这个自定义的View了:
- package com.lxb.horizontalmenu;
- import java.util.ArrayList;
- import java.util.List;
- import android.app.ActivityGroup;
- import android.content.Intent;
- import android.content.res.Configuration;
- import android.os.Bundle;
- import android.util.DisplayMetrics;
- import android.view.ViewGroup.LayoutParams;
- import com.lxb.horizontalmenu.testActivity.Activity1;
- import com.lxb.horizontalmenu.testActivity.Activity2;
- import com.lxb.horizontalmenu.testActivity.Activity3;
- import com.lxb.horizontalmenu.testActivity.Activity4;
- public class HorizontalMenuActivity extends ActivityGroup {
- private HorizontalMenuView horizontalMenuView;
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- DisplayMetrics metric = new DisplayMetrics();
- getWindowManager().getDefaultDisplay().getMetrics(metric);
- int width = metric.widthPixels; // 屏幕宽度(像素)
- int height = metric.heightPixels; // 屏幕高度(像素)
- List<MenuItem> menuItem = new ArrayList<MenuItem>();
- menuItem.add(new MenuItem("菜单1", new Intent(this, Activity1.class)));
- menuItem.add(new MenuItem("菜单2", new Intent(this, Activity2.class)));
- menuItem.add(new MenuItem("菜单3", new Intent(this, Activity3.class)));
- menuItem.add(new MenuItem("菜单4", new Intent(this, Activity4.class)));
- menuItem.add(new MenuItem("菜单5", null));
- menuItem.add(new MenuItem("菜单6", null));
- menuItem.add(new MenuItem("菜单7", null));
- menuItem.add(new MenuItem("菜单8", null));
- menuItem.add(new MenuItem("菜单9", null));
- menuItem.add(new MenuItem("菜单10", null));
- menuItem.add(new MenuItem("菜单11", null));
- menuItem.add(new MenuItem("菜单12", null));
- menuItem.add(new MenuItem("菜单13", null));
- menuItem.add(new MenuItem("菜单14", null));
- menuItem.add(new MenuItem("菜单15", null));
- menuItem.add(new MenuItem("菜单16", null));
- menuItem.add(new MenuItem("菜单17", null));
- menuItem.add(new MenuItem("菜单18", null));
- menuItem.add(new MenuItem("菜单19", null));
- menuItem.add(new MenuItem("菜单20", null));
- horizontalMenuView = new HorizontalMenuView(this, width, height, menuItem);
- horizontalMenuView.setLayoutParams(new LayoutParams(
- LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
- setContentView(horizontalMenuView);
- }
- }
MenuItem内容如下:
- package com.lxb.horizontalmenu;
- import android.content.Intent;
- public class MenuItem {
- private String title; // 菜单项的标题
- private Intent intent; // 菜单项的Intent
- public MenuItem(String title, Intent intent) {
- this.title = title;
- this.intent = intent;
- }
- public String getTitle() {
- return title;
- }
- public void setTitle(String title) {
- this.title = title;
- }
- public Intent getIntent() {
- return intent;
- }
- public void setIntent(Intent intent) {
- this.intent = intent;
- }
- }
以上就是最初我完成的效果,但是后来发现还是有很多地方是不完善的:
1. 内容页是ListView时,ListView无法滑动了。
2. 当屏幕方向改变时,View被重画或者没被重画但是宽度显示不正确。
于是又加入了一些处理:
1. 这点是由于ViewGroup的onTouch事件问题,内容页有ListView,但是由于它的onTouch无法获取到,一直被我们的自定义View控制着的原因。
这里用到onInterceptTouchEvent()与onTouchEvent(),onInterceptTouchEvent()会比onTouchEvent()先调用,我们这里起拦截作用,还有其返回值的问题,如果这个方法return true,那么MotionEvent事件将不会往下传递,反之则会向下传递。
因些我们只需要在onInterceptTouchEvent()方法中判断下我们的操作是倾向左右滑动还是上下滑动,如果是上下滑动,则把MotionEvent传去给listView处理,如果是左右滑动则还是留给自己处理。
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- // 如果这个方法return true, 那么MotionEvent事件将不会往下传递
- // 如果这个方法return false, 那么MotionEvent事件将会往下传递
- int action = ev.getAction();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- if (velocityTracker == null) {
- velocityTracker = VelocityTracker.obtain();
- velocityTracker.addMovement(ev);
- }
- if (!scroller.isFinished()) {
- scroller.abortAnimation();
- }
- lastInterceptX = ev.getX();
- lastInterceptY = ev.getY();
- mLastMotionX = ev.getX();
- deliver = false;
- break;
- case MotionEvent.ACTION_MOVE:
- float x = ev.getX();
- float y = ev.getY();
-
- float dx = x - lastInterceptX;
- float dy = y - lastInterceptY;
-
- if (Math.abs(dx) - Math.abs(dy) > 0 && Math.abs(dx) > 5) {
- deliver = true;
- } else {
- deliver = false;
-
- if (velocityTracker != null) {
- velocityTracker.recycle();
- velocityTracker = null;
- }
- }
- case MotionEvent.ACTION_UP:
- lastInterceptX = 0;
- lastInterceptY = 0;
- }
-
- return deliver;
- }
2. 这个处理问题处理的话,首先要让ActivityGroup在屏幕方向转换的时候不会重新new,也就是我们在注册Activity时加入
- android:configChanges="orientation|keyboardHidden|navigation"
在ActivityGroup中实现onConfigurationChanged()
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- if (horizontalMenuView != null) {
- horizontalMenuView.changeOrientation();
- }
- }
在我们自定义View HorizontalMenuView中加入这个方法:
- /**
- * 改变方向
- */
- public void changeOrientation() {
- int temp = screenWidth;
- screenWidth = screenHeight;
- screenHeight = temp;
- childWidths[0] = screenWidth / 3 + 50;
- childWidths[1] = screenWidth;
- snapToScreen(currentPage);
- }
OK, 大功告成!!
最后说明下,这里介绍的是我写这个Demo时候的思路,以及一些核心,有点乱,被菜鸟误导请勿怪罪。
源代码下载地址: