canvas教程

Android 高仿华为手机Tab页滑动导航效果

字号+ 作者:H5之家 来源:H5之家 2016-04-06 18:01 我要评论( )

Android 高仿华为手机Tab页滑动导航效果

首先带大家看一下实现效果,用了两种实现方式:
1.基于LinearLayout实现,导航栏不可响应手指滑动
2.基于HorizontalScrollView实现,导航栏可响应手指滑动

实现方式虽然不一样,但是使用的是一样的,因为我接口封装的一模一样,下面看实现效果。
基于LinearLayout的实现:

基于LinearLayout实现的滑动导航

基于HorizontalScrollView的实现:

基于HorizontalScrollView实现的滑动导航

两者效果一样,区别就在于导航条可否随用户操作滑动。

下面只说明LinearLayout实现,HorizontalScrollView仅仅是套了一层LinearLayout,因为它只允许套一个子控件。

需求开发点:
1.自定义控件的编写(通用方法:构造函数、init()、onMeasure、onLayout、onDraw、dispatchDraw)
2.白色圆点的偏移量的计算
3.当Tab页超过5个的时候,白点和LinearLayout内容的组合滑动,达到白点不动,而文字移动的效果。

实现这三点,这个控件就算完事,当然一些细节方面的,可以根据自己的需求去调优。

1.自定义控件的编写
1) 这里主要是onMeasure方法的复写,不能用默认的onMeasure,需要我们根据Tab的数量去计算,下图是1-6个Tab的measure结果

这里写图片描述


这个就是根据Tab的数量平分屏幕的宽,当数量超过5个的时候,为了保持效果,只显示5个,超出的内容将其追加到屏幕外面,屏幕是有界的,视图是无界的,你可以无限往右边添加,只要内存能容下,可以用个for循环无限添加看看发生什么。

2) 复写dispatchDraw方法,这里dispatchDraw方法的作用就是画一个白色的圆点。

白色圆点的偏移量的计算
这个得靠ViewPager的OnPageChangeListener,通过它onPageScrolled的回调去计算,计算好了调用invalidate()去重绘。

当Tab页超过5个的时候,白点和LinearLayout内容的组合滑动,达到白点不动,而文字移动的效果。
当滑动到第5个Tab,如果只是白色圆点移动就会移动到屏幕外面去,这个时候也需要让LinearLayout内容联动,通过scrollTo方法,往与白点相反的方向移动内容,使其和白点的移动达到平衡,这样白点不动,上面的标题内容移动。

下面分析,写该控件需要的一些变量,先看截图:

这里写图片描述

1.需要白点是吧,我们用画笔画,所以Paint需要
2.白点画多大?所以需要一个白点的半径变量radius
3.白点的偏移量,我们定义一个变量mOffset,根据ViewPager的滑动动态计算
4.上面还有一排文字,用TextView实现,这个TextView是根据Tab的数量动态添加的,数量不可控,我这里并没有定义成变量。但是文字内容对应的是一个Fragment的标题,也就是说标题和Fragment是一一对应的关系。OK用Map。

/** * 存放Tab的集合 Key:导航栏的标题 Fragment:导航页 */ private Map<String, Fragment> mTabs;

TextView就是根据mTabs的数量动态添加的。
5.我们怎么去计算滑动偏移量呢?这个得靠ViewPager的OnPageChangeListener,通过它onPageScrolled的回调去计算mOffset,就是通过这种计算圆点的偏移,这里我持有了一个ViewPager的引用,所以有变量mViewPager,其实要不要关系不大.
6.屏幕最大可显示的Tab数量MAX_VISIBLE_TAB_COUNTS,总不能有100个你就显示100个吧.
7.实际屏幕上可显示的Tab数量mRealVisibleTabCounts
8.屏幕宽度mScreenWidth辅助onMeasure计算的

下面是实现代码:

package com.csm.hwtab; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Cap; import android.graphics.Paint.Style; import android.support.v4.app.Fragment; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.View.OnClickListener; import android.widget.LinearLayout; import android.widget.TextView; /** * @author Rander.C */ @SuppressLint("NewApi") public class TabLinearLayout extends LinearLayout implements OnClickListener, OnPageChangeListener { /** * 最大可显示的Tab数,可写成自定义属性在xml里面配置 */ private static final int MAX_VISIBLE_TAB_COUNTS = 5; /** * 实际可见的Tab数,比如有8个Tab,超过8个,为了保证效果,最多显示5个 * 此时mRealVisibleTabCounts为5 * 如果只有3个tab小于5个,则mRealVisibleTabCounts为3 * 并根据这个去计算每个tab的宽度,屏幕宽度除以mRealVisibleTabCounts嘛 */ private int mRealVisibleTabCounts; /** * 小圆点滑动偏移量 */ private float mOffset; /** * 绘制小圆点画笔 */ private Paint mPaint; /** * 小圆点半径 */ private int mCircleRadius; /** * 屏幕宽度 */ private int mScreenWidth; /** * 存放Tab的集合 Key:导航栏的标题 Fragment:导航页 */ private Map<String, Fragment> mTabs; /** * View中有getContext()方法,用变量就会 * 多一个Context引用,会增加一丁点虚拟机的负担吗? * 如果不用变量,但是每次都使用getContext(),是不是都要多一层 * 调用栈,这个怎么权衡,求答案,还是我想多了? */ private Context mContext; private ViewPager mViewPager; public TabLinearLayout(Context context, AttributeSet attrs) { this(context, attrs, -1); } public TabLinearLayout(Context context) { this(context, null); } public TabLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mContext = getContext(); setOrientation(HORIZONTAL); mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setDither(true); mPaint.setStyle(Style.FILL); mPaint.setColor(Color.WHITE); mPaint.setStrokeCap(Cap.ROUND); DisplayMetrics metrics = getResources().getDisplayMetrics(); mScreenWidth = metrics.widthPixels; mCircleRadius = (int) getResources().getDimension(R.dimen.radius); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int tabCount = getChildCount(); if (tabCount == 0) { return; } /** 不管默认测量结果怎么样,我们都要重新给其宽度赋值*/ mRealVisibleTabCounts = MAX_VISIBLE_TAB_COUNTS < tabCount ? MAX_VISIBLE_TAB_COUNTS : tabCount; // 为每一个子view重新分配mScreenWidth / mRealVisibleTabCounts大小的宽度 int averageWidth = mScreenWidth / mRealVisibleTabCounts; for (int i = 0; i < tabCount; i++) { View view = getChildAt(i); LinearLayout.LayoutParams params = (android.widget.LinearLayout.LayoutParams) view.getLayoutParams(); params.weight = 0; params.width = averageWidth; view.setLayoutParams(params); } } /** * 获得Tabs列表 * @return */ public List<Fragment> getTabs() { return new ArrayList(mTabs.values()); } /** * 添加一个Tab项,添加一个tab的时候同时给 * 该视图添加一个TextView * @param title * @param tab */ public void addTab(String title, Fragment tab) { if (mTabs == null) { mTabs = new LinkedHashMap<>(); } mTabs.put(title, tab); addView(createTabItem(title, mTabs.size() - 1)); } /** * 设置ViewPager * 同时给其设置OnPageChangeListener监听器。 * @param viewPager */ public void setViewPager(ViewPager viewPager) { mViewPager = viewPager; mViewPager.setOnPageChangeListener(this); } /** * 创建一个Tab视图 * @param title Fragment对应的标题 * @param index 该Fragment对应的索引下标 * @return 已经构造好的tab视图 */ private View createTabItem(String title, int index) { TextView tv_title = new TextView(mContext); tv_title.setText(title); tv_title.setTag(index); tv_title.setGravity(Gravity.CENTER); tv_title.setTextColor(Color.WHITE); tv_title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18); tv_title.setOnClickListener(this); return tv_title; } @Override public void onClick(View v) { int tabIndex = (Integer) v.getTag(); if (onTabOperatorListener != null) { onTabOperatorListener.onTabClick(tabIndex); } mViewPager.setCurrentItem(tabIndex); } @Override public void onPageScrollStateChanged(int state) { if (null != onTabOperatorListener) { onTabOperatorListener.onPageScrollStateChanged(state); } } @Override public void onPageScrolled(int sourcePosition, float positionOffset, int positionOffsetPixels) { mRealVisibleTabCounts = MAX_VISIBLE_TAB_COUNTS < getChildCount() ? MAX_VISIBLE_TAB_COUNTS : getChildCount(); int tabWidth = getWidth() / mRealVisibleTabCounts; // 计算小圆点的偏移量,这个可以画图理解 mOffset = (int) (tabWidth / 2 + sourcePosition * tabWidth + positionOffset * tabWidth ); // 如果滚动到最后一个,则同时要向左滚动tab内容,这种情况是:原点在向右移动,同时整体又向做滑动,两个滑动相互抵消,相当于原点没有移动 if (getChildCount() > mRealVisibleTabCounts && positionOffset > 0 && sourcePosition >= mRealVisibleTabCounts - 1) { scrollTo((int) ((sourcePosition + 1 - mRealVisibleTabCounts) * tabWidth + tabWidth * positionOffset), 0); } invalidate(); if (null != onTabOperatorListener) { onTabOperatorListener.onTabScrolled(sourcePosition, positionOffset, positionOffsetPixels); } } @Override public void onPageSelected(int index) { if (null != onTabOperatorListener) { onTabOperatorListener.onTabSelected(index); } /** * 这里不要多想,只是为了保证选中的tab为第一个时,让布局重置到原点 */ if(index == 0) { scrollTo(0, 0); } } /** * Tab操作相关的监听器 * @author rander */ public interface OnTabOperatorListener { void onTabClick(int tabIndex); void onTabSelected(int tabIndex); void onTabScrolled(int sourcePosition, float positionOffset, int positionOffsetPixels); void onPageScrollStateChanged(int state); } public OnTabOperatorListener onTabOperatorListener; public void setOnTabItemClickListener(OnTabOperatorListener onTabOperatorListener) { this.onTabOperatorListener = onTabOperatorListener; } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); //如果只有一个Tab则,不绘制圆点 if (getChildCount() <= 1) { return; } //根据mOffset绘制偏移量 canvas.drawCircle(mOffset, getHeight() - mCircleRadius * 2, mCircleRadius, mPaint); } }

关于偏移量的计算画图理解:
偏移量计算的表达式:

mOffset = (int) (tabWidth / 2 + sourcePosition * tabWidth + positionOffset * tabWidth );

图解:

这里写图片描述

 

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

相关文章
  • Canvas与ValueAnimator

    Canvas与ValueAnimator

    2017-04-28 18:00

  • Android Bitmap和Canvas学习笔记(转)

    Android Bitmap和Canvas学习笔记(转)

    2017-04-28 17:00

  • 21天学习android开发教程之SurfaceView与多线程的混搭

    21天学习android开发教程之SurfaceView与多线程的混搭

    2017-04-27 12:00

  • Android画图学习免费下载

    Android画图学习免费下载

    2017-04-27 11:01

网友点评