晃晃时间过得真快,马上就到年底了。回首2017年也写了不少博客,其中不少是关于UI的。不久前写过一篇《仿夸克浏览器底部工具栏》,不过还是觉得有点不够,于是这次仿写下我觉得很好用的手势操作吧。银河系惯例,先上下效果图:
可以看到整体非常简洁,除了标题栏其他全部是内容区域了
可以看到前进、回退、回到主页、下拉刷新这些功能都是用手势来完成的,用过夸克浏览器的同学想必对此是很熟悉的。先来分析下页面构成:所有操作都在一个Activiy
中,上面是两个Fragment
:显示主页的Fragment
和显示网页的Fragment
,下面是标题栏。Fragment
外面包裹了一个自定义的ViewGroup
,用来实现手势操作,并把结果回调给Activity
,并由Activity
处理Fragment
间切换和网页内切换的逻辑。接下来我们就开始着手实现这几个功能。
手势操作ViewGroup
这部分是用来捕获手势操作的组件,并把手势的操作回调给外层Activity,本身不处理任何功能逻辑。在上面的效果图我们可以看到,要实现这个效果,我们要处理的手势还是蛮复杂的,如果完全自己写,就要在onTouchEvent
、onInterceptTouchEvent
写很多的条件判断代码,而且需要自己去处理:事件冲突、边缘检测等。不仅代码量很大而且逻辑太复杂。所以这里我就又采用了自定义ViewGroup
神器 -ViewDragHelper
。在里面实现边界检测的功能。
我们先来创建一个ViewDragHelper
:
ViewDragHelper.create(this, 1.0f, new GestureDragCallback());
然后定义需要检测的边界:
mViewDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_RIGHT | ViewDragHelper.EDGE_BOTTOM);
因为下拉刷新是网页内自己的逻辑不涉及两个网页或两个Fragment,而且不需要触摸到边界才触发,所以这里只实现前进,后退和返回主页的三个边界手势。先来定义下需要回调的接口:
public interface GestureListener {
//开始触摸边界时回调,edgeFlags是边界的编号,返回值是否允许捕获这个手势
boolean dragStartedEnable(int edgeFlags, ImageView view);
//当图片移动大最大值时回调
void onViewMaxPositionArrive(int edgeFlags, ImageView view);
//当图片移动大最大值时然后手指松开时回调
void onViewMaxPositionReleased(int edgeFlags, ImageView view);
}
接下来就要实现GestureDragCallback
里的内容了。这里我们以后退功能为例:
private class GestureDragCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//mEdgeTrackerView禁止直接移动
return false;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy){
//禁止竖直滑动,mLeftPos记录了leftRefreshView的初始纵坐标。
return mLeftPos.y;
}
public int clampViewPositionHorizontal(View child, int left, int dx) {
//横向滑动范围就是自身宽度:-child.getWidth~0
int leftBound = 0;
int rightBound = 0;
if (child == leftRefreshView) {
leftBound = -child.getWidth();
}
return Math.min(Math.max(left, leftBound), rightBound);
}
//在边界拖动时回调
@Override
public void onEdgeDragStarted(int edgeFlags, int pointerId) {
if (mGestureListener != null && mGestureListener.dragStartedEnable(edgeFlags, leftRefreshView)) {
mViewDragHelper.captureChildView(leftRefreshView, pointerId);
}
}
//拖动期间不断回调
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
if (mGestureListener != null) {
if (changedView == leftRefreshView && leftRefreshView.getLeft() == 0) {
mGestureListener.onViewMaxPositionArrive(ViewDragHelper.EDGE_LEFT, leftRefreshView);
}
}
}
//手指释放的时候回调
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (mGestureListener != null&&releasedChild == leftRefreshView) {
if (leftRefreshView.getLeft() == 0) {
mGestureListener.onViewMaxPositionReleased(ViewDragHelper.EDGE_LEFT, leftRefreshView);
}
//松手后回到原位置
mViewDragHelper.settleCapturedViewAt(mLeftPos.x, mLeftPos.y);
invalidate();
}
}
}
其他两个功能也是类似的处理,这里就不再赘述了。
Fragment间切换
处理手势的自定义ViewGroup写好了,就可以把Fragment包起来了:
<com.renny.simplebrowser.GestureLayout
android:id="@+id/gesture_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="44dp">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</com.renny.simplebrowser.GestureLayout>
中间的FrameLayout用来承载Fragment。上面提到过,我们需要两个Fragment:显示主页的HomePageFragment
和显示网页的WebViewFragment
。
private void goHomePage() {
if (mHomePageFragment == null) {
mHomePageFragment = new HomePageFragment();
mHomePageFragment.setGoPageListener(new HomePageFragment.goPageListener() {
@Override
public void onGopage(String url) {
goWebView(url);
}
});
mFragmentManager.beginTransaction().add(R.id.container, mHomePageFragment).commit();
} else {
mFragmentManager.beginTransaction().replace(R.id.container,
mHomePageFragment).commit();
}
setTitle("主页");
isOnHomePage = true;
}
private void goWebView(String url) {
if (webViewFragment == null || !TextUtils.isEmpty(url)) {
webViewFragment = new WebViewFragment();
Bundle args = new Bundle();
args.putString("url", url);
webViewFragment.setArguments(args);
}
mFragmentManager.beginTransaction().replace(R.id.container,
webViewFragment).commit();
isOnHomePage = false;
WebView webView = webViewFragment.getWebView();
if (webView != null) {
setTitle(webView.getTitle());
}
}
isOnHomePage
用来标识是否在主页,如果在主页,那么返回主页功能就触发不了了。
网页间切换
这里就是用来处理WebView的前进后退了,主要用到WebView自己的方法:
webView.canGoBack()
、webView.goBack()
和webView.canGoForward()
、webView.goForward()
。
//回退
private void returnLastPage() {
if (fromBack && isOnHomePage) {
goWebView(null);
} else {
WebView webView = webViewFragment.getWebView();
if (webView.canGoBack()) {
webView.goBack();
setTitle(webView.getTitle());
} else {
goHomePage();
}
}
}
//前进
private void goNextPage() {
WebView webView = webViewFragment.getWebView();
if (webView.canGoForward()) {
webView.goForward();
setTitle(webView.getTitle());
}
}
下拉刷新:
这个就是单纯当前页面的操作了,只要将WebView包裹进一个下拉刷新的ViewGroup就行了,这里我采用了一个目前很流行的下拉刷新库:SmartRefreshLayout,当然你也完全可以替换成其它的。
在下拉刷新开始时重载WebView
refreshLayout.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh(RefreshLayout refreshlayout) {
mWebView.reload();
}
});
复写WebViewClient
,在onPageFinished
结束刷新:
@Override
public void onPageFinished(WebView webView, String s) {
super.onPageFinished(webView, s);
refreshLayout.finishRefresh();
}
感谢以下三方库:
然后贴下本项目github地址:
仿夸克浏览器手势控制