您的位置:首页 > 移动开发 > Android开发

Android仿人人客户端(v5.7.1)——采用ViewGroup做父容器,实现左侧滑动菜单(三)

2013-04-05 17:31 726 查看
转载请标明出处:/article/7738562.html
前面已实现以滑动的方式显示或隐藏左侧菜单,采用的父容器是自定义类继承自RelativeLayout来实现的。看到有网友留言说,采用父容器自定义类继承自ViewGroup去实现,这篇我就讲解下采用继承自ViewGroup的方式去如何实现。

自定义类让其继承自ViewGroup类后,编译器强制添加一个带Context类型参数的构造方法和onLayout()方法,代码如下:

package com.everyone.android.widget;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;

/**
* 功能描述:手指在屏幕上左右滑动时,该类的实例负责让其子View根据用户的手势左右偏移(滚动)
* 父容器采用ViewGroup
* @author android_ls
*/
public class HorizontalScrollerContainer extends ViewGroup {

public HorizontalScrollerContainer(Context context) {
super(context);
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {        }

}

通过阅读上面的代码片段,引发的思考,ViewGroup类有无参构造方法吗?答案是没有;至于onLayout()方法,肯定是一个抽象方法了,那么ViewGroup类肯定是抽象类。在Java的继承体系中,父类(ViewGroup)中有抽象方法,子类继承父类必须(强制性的)去实现父类的抽象方法或者把自己也定义成抽象的。既然onLayout()方法是抽象方法,我们自定义的类去继承了ViewGroup类,最后自定义的类是要用来诞生对象的,那肯定不能也定义成抽象的。我们就去实现它,代码如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
}
}

既然容器有了,我们就向其中添加子View,在Activity的onCreate()方法添加如下代码:

// 父容器
HorizontalScrollerContainer mSlideContainer = new HorizontalScrollerContainer(mContext);

// 左侧面板
LeftPanelLayout mLeftPanelLayout = new LeftPanelLayout(mContext);
LayoutParams layoutParams = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
// 添加子View
mSlideContainer.addView(mLeftPanelLayout, layoutParams);

// 设置要显示的视图
setContentView(mSlideContainer);

我们运行程序,效果图如下:



看到这张效果图,有人可能要说,我们自定义的父容器HorizontalScrollerContainer类的对象,到底有没有设置到Window(哪来的Window对象呢?查看过Android源码的朋友肯定知道,没翻过源码的见这行: getWindow().setContentView(view),这不是重点。)上,我们为父容器添加一张背景图,代码如下:

public HorizontalScrollerContainer(Context context) {
super(context);
this.setBackgroundResource(R.drawable.v5_3_0_guide_pic1);
}

运行效果图:



通过上面的验证,说明我们的父容器View是已添加到PhoneWindow上了,只是其中的子View没有显示出来。如何才能让父容器中的子View显示出来呢? 我们还得去重写父容器ViewGroup的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,代码如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
}

运行看效果:



这次明显达到了我们的预期(目的),效果是实现了,到底为什么之前没达到预期效果呢,应用框架层明明强制我(自定义类继承自ViewGroup类)去实现的只有onLayout()方法,我按你(应用框架)的要求去做了,为什么达不到预期效果呢?

当你为一个Activty 的PhoneWindow设置一个可见的View(ViewGroup),并且运行这个Activty时,应用框架层的调用(控制反转)ViewGroup的显示视图相关方法的顺序:onAttachedToWindow-->onMeasure-->onSizeChanged-->onLayout(注:其它相关的方法我们不关注,这里就没列出来)。下面来解释下这几个我们关注的方法的含义:

1、onAttachedToWindow:当View附加到一个窗体上时,应用框架层调用此方法。

2、onMeasure:View自己调用此方法,测量自己及其子View的实际宽度和高度。

3、onSizeChanged:当View大小改变时,调用此方法

4、onLayout:View自己调用此方法,为所有子View分配显示空间的大小,可能会与子View定义时设置的大小不一样。比如:父容器是LinearLayout,里面的子View排列方向是竖直方式,我向父容器中添加第一个子View(A),设置宽度为fill_parent,高度为50dip;我再向父容器中添加第二个子View(B),设置宽度为fill_parent,高度也为fill_parent。在这种情况下,子View(B)的高度为多少?
是fill_parent吗?肯定不是啦,是父容器的高度减去子View(A)的高度。(可能会与子View定义时设置的大小不一样)

我的个人观点:既然编写自定义类去继承ViewGroup后,子类必须去实现onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,为什么不把onMeasure()方法在ViewGroup类中定义成抽象的呢?翻看源码,会发现:

onMeasure()是定义在View类中的,源码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

ViewGroup类完全可以重写父类的onMeasure()方法,将其修改成抽象方法。那么当我们想自定义类去继承ViwGroup类,都会去实现onMeasure()方法,就不会出现按应用框架要求的去做,而没达到预期的,我们想要的结果。

编写两个类,进行测试,TestA类源码:

package com.everyone.android.widget;

public class TestA {

public void test(){
System.out.println("Hello World");
}

}

TestB类源码:

package com.everyone.android.widget;

public abstract class TestB extends TestA{
public abstract void test();
}

通过上面的测试,大家看到这么写,子类是可以修改父类的方法修饰词的。

假如:Android应用框架里,ViewGroup类里重写了父类的onMeasure()方法,在ViewGroup类中添加如下代码:

protected abstract void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

我们自已的类继承ViewGroup后,我们实现onMeasure()方法的代码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);

int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
}

至于Android应用框架层为什么没按我们预想的那样去实现,自然有他们的道理,呵呵。我个人觉的若在子类中必须重写父类的某个方法后,才既能满足框架层又能让使用框架的达到预期的效果。干嘛不把该方法定义成抽象的呢?何乐而不为。

好了,到这里这一篇的重点聊完了。其它的关于用户触摸屏幕事件的分发、拦截和处理(响应)与前面的采用RelativeLayout做父容器时是一样的。

采用ViewGroup做父容器,实现水平滑动显示或隐藏左侧菜单的完整代码如下:

package com.everyone.android.widget;

import android.content.Context;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
* 功能描述:手指在屏幕上左右滑动时,该类的实例负责让其子View根据用户的手势左右偏移(滚动)
* 父容器采用ViewGroup
* @author android_ls
*/
public class HorizontalScrollerContainer extends ViewGroup {

private static final String TAG = "ScrollerContainer";

private Scroller mScroller;

private VelocityTracker mVelocityTracker;

/**
* 手柄(手把)的宽度
*/
private int mHandlebarWidth;

/**
* 在偏移过程中,动画持续的时间
*/
private static final int ANIMATION_DURATION_TIME = 300;

/**
* 记录当前的滑动结束后的状态,左侧面板是否可见
* true  向右滑动(左侧面板处于可见)
* false 向左滑动(左侧面板处于不可见)
*/
private boolean mPanelInvisible;

/**
* 是否已滑动结束
*/
private boolean mFinished;

/**
* 是否允许滚动
* 满足的条件:
*     左侧面板可见,当前手指按下的坐标x值 ,是在手柄宽度范围内;
*     左侧面板不可见,当前手指按下的坐标x值 < 手柄宽度
*/
private boolean mAllowScroll;

/**
* 是否满足响应单击事件的条件
* 满足的条件:左侧面板可见,当前手指按下的坐标x值 ,是在手柄宽度范围内
*/
private boolean isClick;

public HorizontalScrollerContainer(Context context) {
super(context);
// this.setBackgroundResource(R.drawable.v5_3_0_guide_pic1);

mScroller = new Scroller(context);
mHandlebarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 70, getResources().getDisplayMetrics());
}

// 测量自己及其子View的实际宽度和高度
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
}

/*
* 为所有子View分配显示空间的大小 ,可能会与子View定义时设置的大小不一样。
* 比如:父容器是LinearLayout,里面的子View排列方向是竖直方式,我向父容器中添加第一个子View(A),
* 设置宽度为fill_parent,高度为50dip;我再向父容器中添加第二个子View(B),设置宽度为fill_parent,高度也为fill_parent。
* 在这种情况下,子View(B)的高度为多少? 是fill_parent吗?肯定不是啦,是父容器的高度减去子View(A)的高度。
* (可能会与子View定义时设置的大小不一样)
* @see android.view.ViewGroup#onLayout(boolean, int, int, int, int)
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
}
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.e(TAG, "dispatchTouchEvent()");

switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "dispatchTouchEvent():  ACTION_DOWN");

mFinished = mScroller.isFinished();
if(mFinished){
int x = (int) ev.getX();
int width = getWidth();

if(mPanelInvisible)// 左侧面板可见
{
if(x > (width - mHandlebarWidth)){ // 当前手指按下的坐标x值 ,是在手柄宽度范围内
isClick = true;
mAllowScroll = true;
return true;
} else {
isClick = false;
mAllowScroll = false;
}
} else { // 左侧面板不可见
if(x < mHandlebarWidth ){ // 当前手指按下的坐标x值 < 手柄宽度 (也就是说在手柄宽度范围内,是可以相应用户的向右滑动手势)
mAllowScroll = true;
}else{
mAllowScroll = false;
}
}

} else {
// 当前正在滚动子View,其它的事不响应
return false;
}

break;

case MotionEvent.ACTION_MOVE:
Log.i(TAG, "dispatchTouchEvent():  ACTION_MOVE");
int margin = getWidth() - (int) ev.getX();
if (margin < mHandlebarWidth && mAllowScroll) {

Log.e(TAG, "dispatchTouchEvent ACTION_MOVE margin = " + margin + "\t mHandlebarWidth = " + mHandlebarWidth);
return true;
}

break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "dispatchTouchEvent():  ACTION_UP");

if (isClick && mPanelInvisible && mAllowScroll) {
isClick = false;
mPanelInvisible = false;

int scrollX = getChildAt(1).getScrollX();
mScroller.startScroll(scrollX, 0, -scrollX, 0, ANIMATION_DURATION_TIME);
invalidate();

return true;
}

break;
case MotionEvent.ACTION_CANCEL:
Log.i(TAG, "dispatchTouchEvent():  ACTION_CANCEL");
break;
default:
break;
}

return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.e(TAG, "onInterceptTouchEvent()");

switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onInterceptTouchEvent():  ACTION_DOWN");
mFinished = mScroller.isFinished();
if(!mFinished){
return false;
}

break;
case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onInterceptTouchEvent():  ACTION_MOVE");

mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);

// 一秒时间内移动了多少个像素
mVelocityTracker.computeCurrentVelocity(1000, ViewConfiguration.getMaximumFlingVelocity());
float velocityValue = Math.abs(mVelocityTracker.getXVelocity()) ;
Log.d(TAG, "onInterceptTouchEvent():  mVelocityValue = " + velocityValue);

if (velocityValue > 300 && mAllowScroll) {
return true;
}

break;
case MotionEvent.ACTION_UP:
Log.i(TAG, "onInterceptTouchEvent():  ACTION_UP");

if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}

break;
case MotionEvent.ACTION_CANCEL:
Log.i(TAG, "onInterceptTouchEvent():  ACTION_CANCEL");
break;
default:
break;
}

return super.onInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.e(TAG, "onTouchEvent()");

float x = event.getX();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i(TAG, "onTouchEvent():  ACTION_DOWN");
mFinished = mScroller.isFinished();
if(!mFinished){
return false;
}
break;

case MotionEvent.ACTION_MOVE:
Log.i(TAG, "onTouchEvent():  ACTION_MOVE");
getChildAt(1).scrollTo(-(int)x, 0);
break;

case MotionEvent.ACTION_UP:
Log.i(TAG, "onTouchEvent():  ACTION_UP");

if(!mAllowScroll){
break;
}

float width = getWidth();
// 响应滚动子View的临界值,若觉得响应过于灵敏,可以将只改大些。
// 比如:criticalWidth = width / 3或criticalWidth = width / 2,看情况而定,呵呵。
float criticalWidth = width / 5;

Log.i(TAG, "onTouchEvent():  ACTION_UP x = " + x + "\t criticalWidth = " + criticalWidth);

int scrollX = getChildAt(1).getScrollX();

if ( x < criticalWidth) {
Log.i(TAG, "onTouchEvent():  ACTION_UP 向左滑动");

mPanelInvisible = false;

mScroller.startScroll(scrollX, 0, -scrollX, 0, ANIMATION_DURATION_TIME);
invalidate();
} else if ( x > criticalWidth){
Log.i(TAG, "onTouchEvent():  ACTION_UP 向右滑动");

mPanelInvisible = true;

int toX = (int)(width - Math.abs(scrollX) - mHandlebarWidth);
mScroller.startScroll(scrollX, 0, -toX, 0, ANIMATION_DURATION_TIME);
invalidate();
}

break;
case MotionEvent.ACTION_CANCEL:
Log.i(TAG, "onTouchEvent():  ACTION_CANCEL");
break;
default:
break;
}

return super.onTouchEvent(event);
}

@Override
public void computeScroll() {
// super.computeScroll();

if(mScroller.computeScrollOffset()){
this.getChildAt(1).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
this.postInvalidate();
}
}

/**
* 向右滑动View,让左侧操作面饭可见
*/
public void slideToRight() {
mFinished = mScroller.isFinished();
if(mFinished && !mPanelInvisible){
mPanelInvisible = true;

float width = getWidth();
int scrollX = getChildAt(1).getScrollX();
int toX = (int)(width - Math.abs(scrollX) - mHandlebarWidth);

mScroller.startScroll(scrollX, 0, -toX, 0, ANIMATION_DURATION_TIME);
invalidate();
}
}

/**
* View滑动事件监听器
* @author android_ls
*/
public interface OnSlideListener {
/**
* 向左滑动子View
*/
public abstract void toLeft();

/**
* 向右滑动子View
*/
public abstract void toRight();
}

}

测试的Activity源码如下:

package com.everyone.android.ui;

import android.os.Bundle;
import android.view.ViewGroup.LayoutParams;

import com.everyone.android.AppBaseActivity;
import com.everyone.android.widget.FreshNewsLayout;
import com.everyone.android.widget.HorizontalScrollerContainer;
import com.everyone.android.widget.HorizontalScrollerContainer.OnSlideListener;
import com.everyone.android.widget.LeftPanelLayout;

/**
* 功能描述:应用主界面
* @author android_ls
*
*/
public class EveryoneActivity extends AppBaseActivity implements OnSlideListener {

/**
* 滚动(滑动)容器
*/
private HorizontalScrollerContainer mSlideContainer;

/**
* 左侧面板
*/
private LeftPanelLayout mLeftPanelLayout;

/**
* 新鲜事
*/
private FreshNewsLayout mFreshNewsLayout;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(mSlideContainer);

}

@Override
protected int getLayoutId() {
return 0;
}

@Override
protected void setupView() {
mSlideContainer = new HorizontalScrollerContainer(mContext);

mLeftPanelLayout = new LeftPanelLayout(mContext);
mFreshNewsLayout = new FreshNewsLayout(mContext);
mFreshNewsLayout.setOnSlideListener(this);

LayoutParams layoutParams = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
mSlideContainer.addView(mLeftPanelLayout, layoutParams);
mSlideContainer.addView(mFreshNewsLayout, layoutParams);
}

@Override
protected void initialized() {
// TODO Auto-generated method stub

}

@Override
public void toLeft() {
// TODO Auto-generated method stub

}

@Override
public void toRight() {
mSlideContainer.slideToRight();
}

}

运行效果图:



有网友问这篇博文中的某些变量是哪来的,贴出来的代码有验证过没。这里我把 AppBaseActivity再贴一次,方便大家阅读。(这个系列是连载,有看不明白请翻阅前面的博文)

package com.everyone.android;

import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.widget.Toast;

import com.everyone.android.net.AsyncBaseRequest;
import com.everyone.android.net.DefaultThreadPool;

/**
* 功能描述:应用中界面(Activity)的基类
* 对原有的Activity类进行扩展
* @author android_ls
*/
public abstract class AppBaseActivity extends Activity {

/**
* 当前activity所持有的所有网络请求
*/
protected List<AsyncBaseRequest> mAsyncRequests;

/**
* 当前activity所持有的Handler
*/
protected Handler mHandler;

/**
* 当前activity所持有的线程池对象
*/
protected DefaultThreadPool mDefaultThreadPool;

protected Context mContext;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

int layoutId = getLayoutId();
if (layoutId != 0) {
setContentView(getLayoutId());
}

mContext = this.getApplicationContext();
((EveryoneApplication) this.getApplication()).addActivity(this);

mHandler = new Handler();
mAsyncRequests = new ArrayList<AsyncBaseRequest>();
mDefaultThreadPool = DefaultThreadPool.getInstance();

// 初始化组件
setupView();
// 初始化数据
initialized();
}

public List<AsyncBaseRequest> getAsyncRequests() {
return mAsyncRequests;
}

public Handler getHandler() {
return mHandler;
}

public DefaultThreadPool getDefaultThreadPool() {
return mDefaultThreadPool;
}

public Context getContext() {
return mContext;
}

/**
* 布局文件ID
* @return
*/
protected abstract int getLayoutId();

/**
* 初始化组件
*/
protected abstract void setupView();

/**
* 初始化数据
*/
protected abstract void initialized();

@Override
protected void onPause() {
/**
* 在activity暂停的时候,同时设置终止标识,终止异步线程
*/
cancelAllRequest();
super.onPause();
}

@Override
protected void onDestroy() {
/**
* 在activity销毁的时候,同时设置终止标识,终止异步线程
*/
cancelAllRequest();
super.onDestroy();
}

/**
* 取消当前Activity相关的网络请求
*/
private void cancelAllRequest() {
if (mAsyncRequests != null && mAsyncRequests.size() > 0) {
try {
int size = mAsyncRequests.size();
for (int i = 0; i < size; i++) {
AsyncBaseRequest request = mAsyncRequests.get(i);
Thread thread = new Thread(request);
if (thread.isAlive() || !Thread.interrupted()) {
request.setInterrupted(true);
}

HttpURLConnection conn = request.getRequestConn();
if (conn != null) {
// conn.disconnect();
System.err.println("onDestroy disconnect URL: " + conn.getURL());
}

mAsyncRequests.remove(request);
size = mAsyncRequests.size();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

/**
* 显示Toast形式的提示信息
* @param message
*/
protected void show(String message) {
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐