android 打造炫酷导航栏(仿UC头条)
来自: http://blog.csdn.net/qq_16064871/article/details/50682388
年后开始上班甚是清闲,所以想捣鼓一些东西。在翻阅大神杰作Android 教你打造炫酷的ViewPagerIndicator 不仅仅是高仿MIUI 的时候看到下面有一条评论说,如果导航栏能滑动就更好了。我就想我可以去改一下就可以。然后又想感觉有点像UC的头条的界面。于是就往里面加东西,调试写代码。弄了两天有点效果了,写出来看看了。
项目下载地址:http://download.csdn.net/detail/qq_16064871/9434291
1、先看效果
用咔咔大师录屏的gif效果,有点失真。
这是第一个版本的效果。因为我不断往里面加东西,所以有几个版本了。
2、这个版本基本代码
实现思路就是自定义绘制了。主要有两层,第一层是ViewPagerIndicator。主要负责导航栏的三角形指示器的绘制,以及页面滑动的回调,控制。当然这里需要android.support.v4.view.ViewPager这东西配合使用。
第二层是,导航栏的滑动效果,以及最左、最右有 反弹的效果。这个效果我是从以前一篇博文改动到了这里来。链接:android 滚动条下拉反弹的效果(类似微信朋友圈)。这里效果是垂直,改为横向就行了。还有这两层同时使用需要处理就是滑动不要冲突就可以了。
xml布局代码:
<com.ucnew.view.BounceScrollView android:id="@+id/id_scrollview" android:layout_width="0dp" android:layout_height="45dp" android:layout_weight="1" android:focusableInTouchMode="false" android:scrollbars="none" > <com.ucnew.view.ViewPagerIndicator android:id="@+id/id_indicator" android:layout_width="match_parent" android:layout_height="45dp" android:orientation="horizontal" mmsx:item_count="4" > </com.ucnew.view.ViewPagerIndicator> </com.ucnew.view.BounceScrollView>
3、自定义view的java代码
ViewPagerIndicator
package com.ucnew.view; import java.util.List; import com.ucnew.activity.R; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.CornerPathEffect; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; 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.WindowManager; import android.widget.LinearLayout; import android.widget.TextView; public class ViewPagerIndicator extends LinearLayout { //绘制三角形的画笔 private Paint mPaint; //path构成一个三角形 private Path mPath; //三角形的宽度 private int mTriangleWidth; // 三角形的高度 private int mTriangleHeight; //三角形的宽度为单个Tab的1/6 private static final float RADIO_TRIANGEL = 1.0f / 6; // 三角形的最大宽度 private final int DIMENSION_TRIANGEL_WIDTH = (int) (getScreenWidth() / 3 * RADIO_TRIANGEL); //初始时,三角形指示器的偏移量 private int mInitTranslationX; // 手指滑动时的偏移量 private float mTranslationX; // tab数量 private int mTabVisibleCount; // tab上的内容 private List<String> mTabTitles; // 与之绑定的ViewPager public ViewPager mViewPager; // 标题正常时的颜色 private static final int COLOR_TEXT_NORMAL = 0x77FFFFFF; //标题选中时的颜色 private static final int COLOR_TEXT_HIGHLIGHTCOLOR = 0xFFFFFFFF; public ViewPagerIndicator(Context context) { this(context, null); } public ViewPagerIndicator(Context context, AttributeSet attrs) { super(context, attrs); // 获得自定义属性,tab的数量 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ViewPagerIndicator); mTabVisibleCount = a.getInt(R.styleable.ViewPagerIndicator_item_count,4); if (mTabVisibleCount < 0) mTabVisibleCount = 4; a.recycle(); // 初始化画笔 mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setColor(Color.parseColor("#ffffffff")); mPaint.setStyle(Style.FILL); mPaint.setPathEffect(new CornerPathEffect(3)); } //绘制指示器 @Override protected void dispatchDraw(Canvas canvas) { canvas.save(); // 画笔平移到正确的位置 canvas.translate(mInitTranslationX + mTranslationX, getHeight() + 1); canvas.drawPath(mPath, mPaint); canvas.restore(); super.dispatchDraw(canvas); } //初始化三角形的宽度 @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mTriangleWidth = (int) (w / mTabVisibleCount * RADIO_TRIANGEL);// 1/6 of // width mTriangleWidth = Math.min(DIMENSION_TRIANGEL_WIDTH, mTriangleWidth); // 初始化三角形 initTriangle(); // 初始时的偏移量 mInitTranslationX = getScreenWidth() / mTabVisibleCount / 2 - mTriangleWidth / 2; } //设置可见的tab的数量 public void setVisibleTabCount(int count) { this.mTabVisibleCount = count; } //设置tab的标题内容 可选,生成textview加入布局,灵活处理 public void setTabItemTitles(List<String> datas) { // 如果传入的list有值,则移除布局文件中设置的view if (datas != null && datas.size() > 0) { this.removeAllViews(); this.mTabTitles = datas; for (String title : mTabTitles) { // 添加view addView(generateTextView(title)); } // 设置item的click事件 setItemClickEvent(); } } //根据标题生成我们的TextView private TextView generateTextView(String text) { TextView tv = new TextView(getContext()); LinearLayout.LayoutParams lp = new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); lp.width = getScreenWidth() / mTabVisibleCount; tv.setGravity(Gravity.CENTER); tv.setTextColor(COLOR_TEXT_NORMAL); tv.setText(text); tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); tv.setLayoutParams(lp); return tv; } //对外的ViewPager的回调接口 public interface PageChangeListener { public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); public void onPageSelected(int position); public void onPageScrollStateChanged(int state); } // 对外的ViewPager的回调接口 private PageChangeListener onPageChangeListener; // 对外的ViewPager的回调接口的设置 public void setOnPageChangeListener(PageChangeListener pageChangeListener) { this.onPageChangeListener = pageChangeListener; } // 设置关联的ViewPager,以及传入 BounceScrollView,进行设置滚动 public void setViewPager(ViewPager mViewPager, final BounceScrollView scrollView, int pos) { this.mViewPager = mViewPager; mViewPager.setOnPageChangeListener(new OnPageChangeListener() { @Override public void onPageSelected(int position) { // 设置字体颜色高亮 resetTextViewColor(); highLightTextView(position); // 回调 if (onPageChangeListener != null) { onPageChangeListener.onPageSelected(position); } } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // 滚动 scroll(scrollView,position, positionOffset); // 回调 if (onPageChangeListener != null) { onPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); } } @Override public void onPageScrollStateChanged(int state) { // 回调 if (onPageChangeListener != null) { onPageChangeListener.onPageScrollStateChanged(state); } } }); // 设置当前页 mViewPager.setCurrentItem(pos); // 高亮 highLightTextView(pos); } //高亮文本 protected void highLightTextView(int position) { View view = getChildAt(position); if (view instanceof TextView) { ((TextView) view).setTextColor(COLOR_TEXT_HIGHLIGHTCOLOR); } } //重置文本颜色 private void resetTextViewColor() { for (int i = 0; i < getChildCount(); i++) { View view = getChildAt(i); if (view instanceof TextView) { ((TextView) view).setTextColor(COLOR_TEXT_NORMAL); } } } // 设置点击事件 public void setItemClickEvent() { int cCount = getChildCount(); for (int i = 0; i < cCount; i++) { final int j = i; View view = getChildAt(i); view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mViewPager.setCurrentItem(j); } }); } } // 初始化三角形指示器 private void initTriangle() { mPath = new Path(); mTriangleHeight = (int) (mTriangleWidth / 2 / Math.sqrt(2)); mPath.moveTo(0, 0); mPath.lineTo(mTriangleWidth, 0); mPath.lineTo(mTriangleWidth / 2, -mTriangleHeight); mPath.close(); } //指示器跟随手指滚动,以及容器滚动 public void scroll(BounceScrollView scrollView,int position, float offset) { // 不断改变偏移量,invalidate mTranslationX = getScreenWidth() / mTabVisibleCount * (position + offset); int tabWidth = getScreenWidth() / mTabVisibleCount; // 容器滚动,当移动到倒数最后一个的时候,开始滚动 if (offset > 0 && position >= (mTabVisibleCount - 1) && getChildCount() > mTabVisibleCount) { if (mTabVisibleCount != 1) { //下面注释掉,是滚动ViewPagerIndicator这个LinearLayout // this.scrollTo((position - (mTabVisibleCount - 1)) * tabWidth + (int) (tabWidth * offset), 0); //只滚动滚动条,禁止滚动lineayout scrollView.setScrolledTo((position - (mTabVisibleCount - 1)) * tabWidth + (int) (tabWidth * offset), 0); } else // 为count为1时 的特殊处理 { this.scrollTo(position * tabWidth + (int) (tabWidth * offset), 0); // scrollView.setScrolledTo(position * tabWidth + (int) (tabWidth * offset), 0); } } //处理特殊情况 else if (offset > 0 && position <= mTabVisibleCount - 1) { scrollView.setScrolledTo(0, 0); } invalidate(); } //设置布局中view的一些必要属性;如果设置了setTabTitles,布局中view则无效 @Override protected void onFinishInflate() { super.onFinishInflate(); int cCount = getChildCount(); if (cCount == 0) return; for (int i = 0; i < cCount; i++) { View view = getChildAt(i); LinearLayout.LayoutParams lp = (LayoutParams) view .getLayoutParams(); lp.weight = 0; lp.width = getScreenWidth() / mTabVisibleCount; view.setLayoutParams(lp); } // 设置点击事件 setItemClickEvent(); } //获得屏幕的宽度 public int getScreenWidth() { WindowManager wm = (WindowManager) getContext().getSystemService( Context.WINDOW_SERVICE); DisplayMetrics outMetrics = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(outMetrics); return outMetrics.widthPixels ; } }
BounceScrollView
package com.ucnew.view; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.animation.TranslateAnimation; import android.widget.HorizontalScrollView; /** * ScrollView反弹效果的实现 */ public class BounceScrollView extends HorizontalScrollView { // 孩子View private View inner; // 点击时x坐标 private float x; // 矩形(这里只是个形式,只是用于判断是否需要动画 private Rect normal = new Rect(); // 是否开始计算 private boolean isCount = false; private RotatImageView mRotatImageView; public BounceScrollView(Context context, AttributeSet attrs) { super(context, attrs); } /*** * 根据 XML 生成视图工作完成.该函数在生成视图的最后调用,在所有子视图添加完之后. 即使子类覆盖了 onFinishInflate * 方法,也应该调用父类的方法,使该方法得以执行. */ @Override protected void onFinishInflate() { if (getChildCount() > 0) { inner = getChildAt(0); } } //手动需要设置滚动位置 public void setScrolledTo(int position, float positionOffset) { this.scrollTo(position,(int) positionOffset); } //监听touch @Override public boolean onTouchEvent(MotionEvent ev) { if (inner != null) { commOnTouchEvent(ev); } return super.onTouchEvent(ev); } //触摸事件 public void commOnTouchEvent(MotionEvent ev) { int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_UP: // 手指松开. if (isNeedAnimation()) { animation(); isCount = false; } break; /*** * 排除出第一次移动计算,因为第一次无法得知y坐标, 在MotionEvent.ACTION_DOWN中获取不到, * 因为此时是MyScrollView的touch事件传递到到了LIstView的孩子item上面.所以从第二次计算开始. * 然而我们也要进行初始化,就是第一次移动的时候让滑动距离归0. 之后记录准确了就正常执行. */ case MotionEvent.ACTION_MOVE: final float preX = x;// 按下时的y坐标 float nowX = ev.getX();// 时时y坐标 int deltaX = (int) (preX - nowX);// 滑动距离 if (!isCount) { deltaX = 0; // 在这里要归0. } x = nowX; // 当滚动到最上或者最下时就不会再滚动,这时移动布局 if (isNeedMove()) { // 初始化头部矩形 if (normal.isEmpty()) { // 保存正常的布局位置 normal.set(inner.getLeft(), inner.getTop(), inner.getRight(), inner.getBottom()); } // 移动布局 inner.layout(inner.getLeft() - deltaX / 4, inner.getTop(), inner.getRight() - deltaX / 4, inner.getBottom()); //图片加号旋转,如果不需要这个直接删了就行 if (mRotatImageView != null) { mRotatImageView.setRotationLeft(); } } isCount = true; break; default: break; } } //回缩动画 public void animation() { // 开启移动动画 TranslateAnimation ta = new TranslateAnimation(0, 0, inner.getTop(), normal.top); ta.setDuration(200); inner.startAnimation(ta); // 设置回到正常的布局位置 inner.layout(normal.left, normal.top, normal.right, normal.bottom); normal.setEmpty(); } //设置图片加号旋转 public void setRotatImageView(RotatImageView rotatImageView){ this.mRotatImageView = rotatImageView; } // 是否需要开启动画 public boolean isNeedAnimation() { return !normal.isEmpty(); } /*** * 是否需要移动布局 inner.getMeasuredHeight():获取的是控件的总高度 * getHeight():获取的是屏幕的高度 */ public boolean isNeedMove() { int offset = inner.getMeasuredWidth() - getWidth(); int scrollX = getScrollX(); // 0是顶部反弹 //是底部反弹加上 if (scrollX == 0 || scrollX == offset) { return true; } return false; } }
都有很详细的注释,以及测试需要说明。
那么就直接看下一个版本。
4、看下一个版本效果
多了一个图标,这个图标可以用来监听,等等。
我后面捣鼓加了一个旋转动画,动画效果可能不是很好。但也可以看看。
5、xml代码
有一些自定义属性以及效果的代码就不贴,感兴趣下载源码。看不懂的,看我前几篇文章,自定义控件使用。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:mmsx="http://schemas.android.com/apk/res/com.ucnew.activity" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#ffffffff" android:orientation="vertical" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#8000bf5f" > <com.ucnew.view.BounceScrollView android:id="@+id/id_scrollview" android:layout_width="0dp" android:layout_height="45dp" android:layout_weight="1" android:focusableInTouchMode="false" android:scrollbars="none" > <com.ucnew.view.ViewPagerIndicator android:id="@+id/id_indicator" android:layout_width="match_parent" android:layout_height="45dp" android:orientation="horizontal" mmsx:item_count="4" > </com.ucnew.view.ViewPagerIndicator> </com.ucnew.view.BounceScrollView> <com.ucnew.view.RotatImageView android:id="@+id/id_rotat_imageView" android:layout_width="50dp" android:layout_height="wrap_content" android:paddingLeft="30dp" android:layout_gravity="center" mmsx:src="@drawable/add" > </com.ucnew.view.RotatImageView> </LinearLayout> <android.support.v4.view.ViewPager android:id="@+id/id_vp" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" > </android.support.v4.view.ViewPager> </LinearLayout>
6、增加以及这里主要主要需要改的代码。
ViewPagerIndicator类的这个函数
//获得屏幕的宽度 public int getScreenWidth() { WindowManager wm = (WindowManager) getContext().getSystemService( Context.WINDOW_SERVICE); DisplayMetrics outMetrics = new DisplayMetrics(); wm.getDefaultDisplay().getMetrics(outMetrics); //获取是整个屏幕的宽度,我试过自定义宽度测量宽度,不行。因为本身还没内容,是后面添加的。所以需要后面加东西 //需要这里减去宽度就行。其中这里减去120就是60dp的宽度 return outMetrics.widthPixels -100; }
还有一个图片的旋转类RotatImageView
package com.ucnew.view; import com.ucnew.activity.R; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PaintFlagsDrawFilter; import android.util.AttributeSet; import android.util.Log; import android.view.View; public class RotatImageView extends View { private Paint paint = null; // 画笔 private Bitmap mbitmap = null; // 图片位图 private Bitmap bitmapDisplay = null; private Matrix matrix = null; private int mWidth = 0; // 图片的宽度 private int mHeight = 0; // 图片的高度 private float fAngle = 180.0f; // 图片旋转 private PaintFlagsDrawFilter mDrawFilter; public RotatImageView(Context context) { super(context); } public RotatImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * 初始化一些自定义的参数 * * @param context * @param attrs * @param defStyle */ public RotatImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.RotatImageView, defStyle, 0); int n = a.getIndexCount(); for (int i = 0; i < n; i++) { int attr = a.getIndex(i); switch (attr) { // 原始图片,在布局里面获取 case R.styleable.RotatImageView_src: mbitmap = BitmapFactory.decodeResource(getResources(),a.getResourceId(attr, 0)); bitmapDisplay = mbitmap; break; } } a.recycle(); mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG|Paint.FILTER_BITMAP_FLAG); paint = new Paint(); paint.setFlags(Paint.ANTI_ALIAS_FLAG); matrix = new Matrix(); } /** * 计算控件的高度和宽度 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 设置宽度 int specMode = MeasureSpec.getMode(widthMeasureSpec); int specSize = MeasureSpec.getSize(widthMeasureSpec); //match_parent或者设置的精确值获取 //MeasureSpec.EXACTLY if (specMode == MeasureSpec.EXACTLY) { mWidth = specSize; } else { // 由图片决定的宽 //getPaddingLeft(),getPaddingRight()这两个值是控件属性的向内偏移的距离值,所以的一起计算 //区别于layout_marginLeft,两个控件的左间距值设置 int desireByImg = getPaddingLeft() + getPaddingRight() + mbitmap.getWidth(); // wrap_content if (specMode == MeasureSpec.AT_MOST) { //所以最小的值,宽度的话是左右内偏移距离之和 mWidth = Math.min(desireByImg, specSize); } else mWidth = desireByImg; } // 设置高度,部分解释同上 specMode = MeasureSpec.getMode(heightMeasureSpec); specSize = MeasureSpec.getSize(heightMeasureSpec); //match_parent或者设置的精确值获取 //MeasureSpec.EXACTLY if (specMode == MeasureSpec.EXACTLY) { mHeight = specSize; } else { int desire = getPaddingTop() + getPaddingBottom() + mbitmap.getHeight(); // wrap_content if (specMode == MeasureSpec.AT_MOST) { mHeight = Math.min(desire, specSize); } else mHeight = desire; } //计算好的宽度以及高度是值,设置进去 setMeasuredDimension(mWidth, mHeight); } // 向左旋转 public void setRotationLeft() { fAngle = fAngle - 20; setAngle(); } // 向右旋转 public void setRotationRight() { fAngle = fAngle + 20; setAngle(); } private boolean isRoate = false; // 设置旋转比例 private void setAngle() { Log.i("Show", String.valueOf(fAngle)); isRoate = true; //设置图片的旋转中心,即绕(X,Y)这点进行中心旋转 要旋转的角度 matrix.preRotate(fAngle, (float)mbitmap.getWidth()/2, (float)mbitmap.getHeight()/2); invalidate(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //消除锯齿, 图片旋转后的锯齿消除不成功,实在不行图片边缘加一些白色像素点 canvas.setDrawFilter(mDrawFilter); if (isRoate) { canvas.drawBitmap(bitmapDisplay, matrix, paint); isRoate = false; }else { canvas.drawBitmap(bitmapDisplay, 0, 0, paint); } } }
activity调用以及初始化这些自定义控件
package com.ucnew.activity; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.ucnew.activity.R; import com.ucnew.fragment.VpSimpleFragment; import com.ucnew.view.BounceScrollView; import com.ucnew.view.RotatImageView; import com.ucnew.view.ViewPagerIndicator; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.view.ViewPager; import android.view.Window; public class MainActivity extends FragmentActivity { private List<Fragment> mTabContents = new ArrayList<Fragment>(); private FragmentPagerAdapter mAdapter; private ViewPager mViewPager; private List<String> mDatas = Arrays.asList("页面1", "页面2", "页面3", "页面4", "页面5", "页面6", "页面7", "页面8", "页面9"); private ViewPagerIndicator mIndicator; private BounceScrollView mScrollView; private RotatImageView mRotatImageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.vp_indicator); initView(); initDatas(); //设置Tab上的标题 mIndicator.setTabItemTitles(mDatas); mViewPager.setAdapter(mAdapter); //设置关联的ViewPager mIndicator.setViewPager(mViewPager,mScrollView,0); //设置关联的图片旋转,根据需要设置,效果不是很好 mScrollView.setRotatImageView(mRotatImageView); } private void initDatas() { for (String data : mDatas) { VpSimpleFragment fragment = VpSimpleFragment.newInstance(data); mTabContents.add(fragment); } mAdapter = new FragmentPagerAdapter(getSupportFragmentManager()) { @Override public int getCount() { return mTabContents.size(); } @Override public Fragment getItem(int position) { return mTabContents.get(position); } }; } private void initView() { mScrollView = (BounceScrollView) findViewById(R.id.id_scrollview); mViewPager = (ViewPager) findViewById(R.id.id_vp); mIndicator = (ViewPagerIndicator) findViewById(R.id.id_indicator); mRotatImageView = (RotatImageView)findViewById(R.id.id_rotat_imageView); } }
还有部分代码没贴。自已下载查看吧。
7、最后效果图
录屏不是很清晰,来一张截图,够清晰的
项目下载地址:http://download.csdn.net/detail/qq_16064871/9434291
用到知识
Android 教你打造炫酷的ViewPagerIndicator 不仅仅是高仿MIUI