本文为转载文章 http://blog.csdn.net/zjws23786/article/details/51692774
现在讲一下今天要完成和前面android中字母导航和PinnedHeaderListView(listview头部固定)功能差不多,今天实现的功能是上篇的另一种实现方式,有兴趣的可以看一下
千言万语抵不过一张效果图:
上面右侧字母效果图有没有眼前一亮,如果没接触过这方面的童鞋,都不知道从哪里入手,下面一起来看一下,上面右侧字母的效果图是怎样实现的
讲一下滑动字母里面相关效果
1、有明显字体大小变化
2、字母有明显渐变效果
3、字母在指定的区域内左右滑动,字母有明显伸缩变化
相关技术
主要使用贝塞尔曲线二阶函数,运行效果图如下:
二阶贝塞尔曲线公式:
解释一下公式里面变量相关含义
B(t)表示t时间时点的坐标值(比如x值 或 y值)
P0为起点
P1为控制点
P2为终点
结合贝塞尔曲线二阶函数和下面那张图一起来看代码,这个就不难理解了
上图描述是右侧字母效果图运动的整个轨迹,里面都标的比较清楚了,如果上图看不清楚,可以下载下来再看,这个就比较清楚了。结合工程中LetterIndexer这个类理解起来比较容易了,下面是LetterIndexer类代码
- public class LetterIndexer extends View {
- public interface OnTouchLetterChangedListener {
- void onTouchLetterChanged(String s, int index);
- void onTouchActionUp(String s);
- }
- private Context mContext;
- // 向右偏移多少画字符, default 30
- private float mWidthOffset = 30.0f;
- // 最小字体大小
- private int mMinFontSize = 24;
- // 最大字体大小
- private int mMaxFontSize = 48;
- // 提示字体大小
- private int mTipFontSize = 52;
- // 提示字符的额外偏移
- private float mAdditionalTipOffset = 20.0f;
- // 贝塞尔曲线控制的高度
- private float mMaxBezierHeight = 150.0f;
- // 贝塞尔曲线单侧宽度
- private float mMaxBezierWidth = 240.0f;
- // 贝塞尔曲线单侧模拟线量
- private int mMaxBezierLines = 32;
- // 列表字符颜色
- private int mFontColor = 0xffffffff; //白色
- // 提示字符颜色
- // int mTipFontColor = 0xff3399ff;
- int mTipFontColor = 0xffd33e48; //金
- private OnTouchLetterChangedListener mListener;
- private String[] constChar = {"#", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"
- , "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
- private int mConLength = 0;
- private int mChooseIndex = -1;
- private Paint mPaint = new Paint();
- private PointF mTouch = new PointF();
- private PointF[] mBezier1;
- private PointF[] mBezier2;
- private float mLastOffset[]; // 记录每一个字母的x方向偏移量, 数字<=0
- private Scroller mScroller;
- //正在动画
- private boolean mAnimating = false;
- //动画的偏移量
- private float mAnimationOffset;
- //动画隐藏
- private boolean mHideAnimation = false;
- //手指是否抬起
- private boolean isUp = false;
- private int mAlpha = 255;
- /**
- * 控制距离顶部的距离、底部距离
- */
- private int paddingTop = 0;
- private int paddingBottom = 0;
- Handler mHideWaitingHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- if (msg.what == 1) {
- // mScroller.startScroll(0, 0, 255, 0, 1000);
- mHideAnimation = true;
- mAnimating = false; //动画mAnimating=false onDraw触发
- LetterIndexer.this.invalidate();
- return;
- }
- super.handleMessage(msg);
- }
- };
- public LetterIndexer(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- initData(context, attrs);
- }
- public LetterIndexer(Context context, AttributeSet attrs) {
- super(context, attrs);
- initData(context, attrs);
- }
- public LetterIndexer(Context context) {
- super(context);
- initData(null, null);
- }
- private void initData(Context context, AttributeSet attrs) {
- if (context != null && attrs != null) {
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LetterIndexer, 0, 0);
- mWidthOffset = a.getDimension(R.styleable.LetterIndexer_widthOffset, mWidthOffset);
- mMinFontSize = a.getInteger(R.styleable.LetterIndexer_minFontSize, mMinFontSize);
- mMaxFontSize = a.getInteger(R.styleable.LetterIndexer_maxFontSize, mMaxFontSize);
- mTipFontSize = a.getInteger(R.styleable.LetterIndexer_tipFontSize, mTipFontSize);
- mMaxBezierHeight = a.getDimension(R.styleable.LetterIndexer_maxBezierHeight, mMaxBezierHeight);
- mMaxBezierWidth = a.getDimension(R.styleable.LetterIndexer_maxBezierWidth, mMaxBezierWidth);
- mMaxBezierLines = a.getInteger(R.styleable.LetterIndexer_maxBezierLines, mMaxBezierLines);
- mAdditionalTipOffset = a.getDimension(R.styleable.LetterIndexer_additionalTipOffset, mAdditionalTipOffset);
- mFontColor = a.getColor(R.styleable.LetterIndexer_fontColor, mFontColor);
- mTipFontColor = a.getColor(R.styleable.LetterIndexer_tipFontColor, mTipFontColor);
- a.recycle();
- }
- this.mContext = context;
- mScroller = new Scroller(getContext());
- mTouch.x = 0;
- mTouch.y = -10 * mMaxBezierWidth;
- mBezier1 = new PointF[mMaxBezierLines];
- mBezier2 = new PointF[mMaxBezierLines];
- commonData(0, 0);
- }
- /**
- * 需 注意的是,传值单位是sp
- * @param top 距离顶部的距离
- * @param bottom 距离底部的距离
- */
- private void commonData(int top, int bottom) {
- paddingTop = DisplayUtils.convertDIP2PX(mContext,top);
- paddingBottom = DisplayUtils.convertDIP2PX(mContext,bottom);
- mConLength = constChar.length;
- mLastOffset = new float[mConLength];
- calculateBezierPoints();
- }
- public void setConstChar(String[] constChar,int top, int bottom) {
- this.constChar = constChar;
- commonData(top,bottom);
- }
- @Override
- protected void onDraw(Canvas canvas) {
- // 控件宽高
- int height = getHeight() - paddingTop - paddingBottom;
- int width = getWidth();
- // 单个字母高度
- float singleHeight = height / (float) constChar.length;
- int workHeight = paddingTop;
- if (mAlpha == 0)
- return;
- //恢复画笔的默认设置。
- mPaint.reset();
- /**
- * 遍历所以字母内容
- */
- for (int i = 0; i < mConLength; i++) {
- mPaint.setColor(mFontColor);
- mPaint.setAntiAlias(true);
- float xPos = width - mWidthOffset; // 字母在 x 轴的位置 基本保持不变
- float yPos = workHeight + singleHeight / 2; //字母在 y 轴的位置 该值一直在变化
- // 根据当前字母y的位置计算得到字体大小
- int fontSize = adjustFontSize(i, yPos);
- mPaint.setTextSize(fontSize);
- mAlpha = 255 - fontSize*4;
- mPaint.setAlpha(mAlpha);
- if (i == mChooseIndex){
- mPaint.setColor(Color.parseColor("#F50527"));
- }
- // 添加一个字母的高度
- workHeight += singleHeight;
- // 绘制字母
- drawTextInCenter(canvas, constChar[i], xPos + ajustXPosAnimation(i, yPos), yPos);
- // 如果手指抬起
- if (isUp) {
- mListener.onTouchActionUp(constChar[mChooseIndex]);
- isUp = false;
- }
- mPaint.reset();
- }
- }
- /**
- * @param canvas 画板
- * @param string 被绘制的字母
- * @param xCenter 字母的中心x方向位置
- * @param yCenter 字母的中心y方向位置
- */
- private void drawTextInCenter(Canvas canvas, String string, float xCenter, float yCenter) {
- FontMetrics fm = mPaint.getFontMetrics();
- float fontHeight = mPaint.getFontSpacing();
- float drawY = yCenter + fontHeight / 2 - fm.descent;
- if (drawY < -fm.ascent - fm.descent)
- drawY = -fm.ascent - fm.descent;
- if (drawY > getHeight())
- drawY = getHeight();
- mPaint.setTextAlign(Align.CENTER);
- canvas.drawText(string, xCenter, drawY, mPaint);
- }
- private int adjustFontSize(int i, float yPos) {
- // 根据水平方向偏移量计算出一个放大的字号
- float adjustX = Math.abs(ajustXPosAnimation(i, yPos));
- int adjustSize = (int) ((mMaxFontSize - mMinFontSize) * adjustX / mMaxBezierHeight) + mMinFontSize;
- return adjustSize;
- }
- /**
- * x 方向的向左偏移量
- *
- * @param i 当前字母的索引
- * @param yPos y方向的初始位置 会变化
- * @return
- */
- private float ajustXPosAnimation(int i, float yPos) {
- float offset;
- if (this.mAnimating || this.mHideAnimation) {
- // 正在动画中或在做隐藏动画
- offset = mLastOffset[i];
- if (offset != 0.0f) {
- offset += this.mAnimationOffset;
- if (offset > 0)
- offset = 0;
- }
- } else {
- // 根据当前字母y方向位置, 计算水平方向偏移量
- offset = adjustXPos(yPos);
- // 当前触摸的x方向位置
- float xPos = mTouch.x;
- float width = getWidth() - mWidthOffset;
- width = width - 60;
- // 字母绘制时向左偏移量 进行修正, offset需要是<=0的值
- if (offset != 0.0f && xPos > width){
- offset += (xPos - width);
- }
- if (offset > 0){
- offset = 0;
- }
- mLastOffset[i] = offset;
- }
- return offset;
- }
- private float adjustXPos(float yPos) {
- float dis = yPos - mTouch.y; // 字母y方向位置和触摸时y值坐标的差值, 距离越小, 得到的水平方向偏差越大
- if (dis > -mMaxBezierWidth && dis < mMaxBezierWidth) {
- // 在2个贝赛尔曲线宽度范围以内 (一个贝赛尔曲线宽度是指一个山峰的一边)
- // 第一段 曲线
- if (dis > mMaxBezierWidth / 4) {
- for (int i = mMaxBezierLines - 1; i > 0; i--) {
- // 从下到上, 逐个计算
- if (dis == -mBezier1[i].y) // 落在点上
- return mBezier1[i].x;
- // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量
- if (dis > -mBezier1[i].y && dis < -mBezier1[i - 1].y) {
- return (dis + mBezier1[i].y) * (mBezier1[i - 1].x - mBezier1[i].x) / (-mBezier1[i - 1].y + mBezier1[i].y) + mBezier1[i].x;
- }
- }
- return mBezier1[0].x;
- }
- // 第三段 曲线, 和第一段曲线对称
- if (dis < -mMaxBezierWidth / 4) {
- for (int i = 0; i < mMaxBezierLines - 1; i++) {
- // 从上到下
- if (dis == mBezier1[i].y) // 落在点上
- return mBezier1[i].x;
- // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量
- if (dis > mBezier1[i].y && dis < mBezier1[i + 1].y) {
- return (dis - mBezier1[i].y) * (mBezier1[i + 1].x - mBezier1[i].x) / (mBezier1[i + 1].y - mBezier1[i].y) + mBezier1[i].x;
- }
- }
- return mBezier1[mMaxBezierLines - 1].x;
- }
- // 第二段 峰顶曲线
- for (int i = 0; i < mMaxBezierLines - 1; i++) {
- if (dis == mBezier2[i].y)
- return mBezier2[i].x;
- // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量
- if (dis > mBezier2[i].y && dis < mBezier2[i + 1].y) {
- return (dis - mBezier2[i].y) * (mBezier2[i + 1].x - mBezier2[i].x) / (mBezier2[i + 1].y - mBezier2[i].y) + mBezier2[i].x;
- }
- }
- return mBezier2[mMaxBezierLines - 1].x;
- }
- return 0.0f;
- }
- @Override
- public boolean dispatchTouchEvent(MotionEvent event) {
- final int action = event.getAction();
- final float y = event.getY();
- final int oldmChooseIndex = mChooseIndex;
- final OnTouchLetterChangedListener listener = mListener;
- /**
- * 计算除去paddingTop后,用户点击不同位置对应的字母索引
- */
- final int c = (int) ((y-paddingTop) / (getHeight()-paddingTop-paddingBottom) * constChar.length);
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- if (this.getWidth() > mWidthOffset) {
- if (event.getX() < this.getWidth() - mWidthOffset)
- return false;
- }
- if (y < paddingTop || c<0 || y > getHeight()-paddingBottom){
- return false;
- }
- mHideWaitingHandler.removeMessages(1);
- mScroller.abortAnimation();
- mAnimating = false;
- mHideAnimation = false;
- mAlpha = 255;
- mTouch.x = event.getX();
- mTouch.y = event.getY();
- if (oldmChooseIndex != c && listener != null) {
- if (c > 0 && c < constChar.length) {
- listener.onTouchLetterChanged(constChar[c],c);
- mChooseIndex = c;
- }
- }
- invalidate();
- break;
- case MotionEvent.ACTION_MOVE:
- mTouch.x = event.getX();
- mTouch.y = event.getY();
- invalidate();
- if (oldmChooseIndex != c && listener != null) {
- if (c >= 0 && c < constChar.length) {
- listener.onTouchLetterChanged(constChar[c],c);
- mChooseIndex = c;
- }
- }
- break;
- case MotionEvent.ACTION_UP:
- mTouch.x = event.getX();
- mTouch.y = event.getY();
- isUp = true;
- mScroller.startScroll(0, 0, (int) mMaxBezierHeight, 0, 2000);
- mAnimating = true;
- postInvalidate();
- break;
- }
- return true;
- }
- @Override
- public void computeScroll() {
- super.computeScroll();
- if (mScroller.computeScrollOffset()) {
- if (mAnimating) {
- float x = mScroller.getCurrX();
- mAnimationOffset = x;
- } else if (mHideAnimation) {
- mAlpha = 255 - (int) mScroller.getCurrX();
- }
- invalidate();
- } else if (mScroller.isFinished()) {
- if (mAnimating) {
- mHideWaitingHandler.sendEmptyMessage(1);
- } else if (mHideAnimation) {
- mHideAnimation = false;
- this.mChooseIndex = -1;
- mTouch.x = -10000;
- mTouch.y = -10000;
- }
- }
- }
- public void setOnTouchLetterChangedListener(OnTouchLetterChangedListener listener) {
- this.mListener = listener;
- }
- /**
- * 计算出所有贝塞尔曲线上的点
- * 个数为 mMaxBezierLines * 2 = 64
- */
- private void calculateBezierPoints() {
- PointF mStart = new PointF(); // 开始点
- PointF mEnd = new PointF(); // 结束点
- PointF mControl = new PointF(); // 控制点
- // 计算第一段红色部分 贝赛尔曲线的点
- // 开始点
- mStart.x = 0.0f;
- mStart.y = -mMaxBezierWidth;
- // 控制点
- mControl.x = 0.0f;
- mControl.y = -mMaxBezierWidth / 2;
- // 结束点
- mEnd.x = -mMaxBezierHeight / 2;
- mEnd.y = -mMaxBezierWidth / 4;
- mBezier1[0] = new PointF();
- mBezier1[mMaxBezierLines - 1] = new PointF();
- mBezier1[0].set(mStart);
- mBezier1[mMaxBezierLines - 1].set(mEnd);
- for (int i = 1; i < mMaxBezierLines - 1; i++) {
- mBezier1[i] = new PointF();
- mBezier1[i].x = calculateBezier(mStart.x, mEnd.x, mControl.x, i / (float) mMaxBezierLines);
- mBezier1[i].y = calculateBezier(mStart.y, mEnd.y, mControl.y, i / (float) mMaxBezierLines);
- }
- // 计算第二段蓝色部分 贝赛尔曲线的点
- mStart.y = -mMaxBezierWidth / 4;
- mStart.x = -mMaxBezierHeight / 2;
- mControl.y = 0.0f;
- mControl.x = -mMaxBezierHeight;
- mEnd.y = mMaxBezierWidth / 4;
- mEnd.x = -mMaxBezierHeight / 2;
- mBezier2[0] = new PointF();
- mBezier2[mMaxBezierLines - 1] = new PointF();
- mBezier2[0].set(mStart);
- mBezier2[mMaxBezierLines - 1].set(mEnd);
- for (int i = 1; i < mMaxBezierLines - 1; i++) {
- mBezier2[i] = new PointF();
- mBezier2[i].x = calculateBezier(mStart.x, mEnd.x, mControl.x, i / (float) mMaxBezierLines);
- mBezier2[i].y = calculateBezier(mStart.y, mEnd.y, mControl.y, i / (float) mMaxBezierLines);
- }
- }
- /**
- * 贝塞尔曲线核心算法
- *
- * @param start
- * @param end
- * @param control
- * @param val
- * @return 公式及动图, 维基百科: https://en.wikipedia.org/wiki/B%C3%A9zier_curve
- * 中文可参考此网站: http://blog.csdn.net/likendsl/article/details/7852658
- */
- private float calculateBezier(float start, float end, float control, float val) {
- float t = val;
- float s = 1 - t;
- float ret = start * s * s + 2 * control * s * t + end * t * t;
- return ret;
- }
- }
上面代码是实现字母右侧滑动效果的核心类,里面都有注释了,记得这个类要结合上图来看,会发现哦原来是这样子,很溜吧。所以学好数学是很必要的
attrs.xml文件内容
- <?xml version="1.0" encoding="utf-8"?>
- <resources>
- <declare-styleable name="LetterIndexer">
- <attr name="widthOffset" format="dimension" />
- <attr name="minFontSize" format="integer" />
- <attr name="maxFontSize" format="integer" />
- <attr name="tipFontSize" format="integer" />
- <attr name="maxBezierHeight" format="dimension" />
- <attr name="maxBezierWidth" format="dimension" />
- <attr name="maxBezierLines" format="integer" />
- <attr name="additionalTipOffset" format="dimension" />
- <attr name="fontColor" format="color" />
- <attr name="tipFontColor" format="color" />
- </declare-styleable>
- </resources>
- public class MainActivity extends Activity {
- private PinnedHeaderListView pinnedHeaderListView;
- private ArrayList<Person> persons; //英雄好汉列表数据集 (除过分组标签,列表所有数据,无序)
- private LinkedHashMap<String, List<Person>> personMpas; //英雄好汉列表 分组标签对应的数据集合(有序)
- private PinnedHeaderListViewAdapter<Person> adapter; //英雄好汉列表适配器
- private LetterIndexer letterIndexer;
- private TextView tv_index_center;
- private Handler mHandler = new Handler();
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- initView();
- lister();
- //初始化数据
- initData();
- }
- private void initView() {
- pinnedHeaderListView = (PinnedHeaderListView)findViewById(R.id.pinnedheader_listview);
- pinnedHeaderListView.setPinnedHeaderView(this.getLayoutInflater().inflate(
- R.layout.pinnedheaderlistview_header_layout, pinnedHeaderListView, false));
- letterIndexer = (LetterIndexer) findViewById(R.id.letter_index);
- tv_index_center = (TextView) findViewById(R.id.tv_index_center);
- }
- private void lister() {
- letterIndexer.setOnTouchLetterChangedListener(new LetterIndexer.OnTouchLetterChangedListener() {
- @Override
- public void onTouchLetterChanged(String letter,int index) {
- // 从集合中查找第一个拼音首字母为letter的索引, 进行跳转
- for (int i = 0; i < persons.size(); i++) {
- Person person = persons.get(i);
- String s = person.getLetter();
- if(TextUtils.equals(s, letter)){
- // 匹配成功, 中断循环, 将列表移动到指定的位置
- pinnedHeaderListView.setSelection(i);
- break;
- }
- }
- }
- @Override
- public void onTouchActionUp(String letter) {
- showLetter(letter);
- }
- });
- }
- /**
- * 显示字母提示
- *
- * @param letter
- */
- protected void showLetter(String letter) {
- tv_index_center.setVisibility(View.VISIBLE);
- tv_index_center.setText(letter);
- mHandler.removeCallbacksAndMessages(null);
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- // 隐藏
- tv_index_center.setVisibility(View.GONE);
- }
- }, 2000);
- }
- protected void initData() {
- InternalStorageUtils.asynReadInternalFile(this, "names.config", new AsyncResonseHandler() {
- @Override
- protected void onSuccess(String content) {
- super.onSuccess(content);
- try {
- Gson gson = new Gson();
- HeroPerson hero = gson.fromJson(content,
- new TypeToken<HeroPerson>() {
- }.getType());
- List<PersonList> personList = hero.getSections();
- persons = new ArrayList<Person>();
- personMpas = new LinkedHashMap<String, List<Person>>();
- //特殊字符
- List<Person> specialChar = new ArrayList<Person>();
- Person charPerson = null;
- for (int i=0; i<5; i++){
- charPerson = new Person();
- charPerson.setName("#特殊字符"+i);
- charPerson.setLetter("#");
- specialChar.add(charPerson);
- }
- personMpas.put("特殊字符", specialChar);
- persons.addAll(specialChar);
- //得到右侧字母索引的内容
- int letterLength = personList.size()+ personMpas.size();
- String[] constChar = new String[letterLength];
- constChar[0] = "#";
- List<Person> personItems;
- for (int i = 0; i < personList.size(); i++) {
- personItems = personList.get(i).getPersons();
- persons.addAll(personItems);
- personMpas.put(personList.get(i).getIndex(), personItems);
- constChar[i+1] = personList.get(i).getIndex();
- }
- adapter = new PinnedHeaderListViewAdapter<Person>(MainActivity.this, personMpas, pinnedHeaderListView,
- letterIndexer,constChar, 20, 20);
- pinnedHeaderListView.setOnScrollListener(adapter);
- pinnedHeaderListView.setAdapter(adapter);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- });
- }
- }
activity_main.xml
- <?xml version="1.0" encoding="utf-8"?>
- <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:huahua="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical"
- android:background="@color/bg_gray_main" >
- <TextView
- android:id="@+id/tv_title"
- android:layout_width="match_parent"
- android:layout_height="50dp"
- android:gravity="center"
- android:background="#96B1B4"
- android:text="标题内容"/>
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="0dp"
- android:layout_weight="1">
- <include layout="@layout/person_pinned_header_listview"
- android:id="@+id/pinnedheader_listview"/>
- <cn.com.huahua.pinnedheaderlistview.ui.LetterIndexer
- android:id="@+id/letter_index"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- huahua:widthOffset="15dip"
- huahua:minFontSize="32"
- huahua:maxFontSize="77"
- huahua:tipFontSize="72"
- huahua:maxBezierHeight="150dip"
- huahua:maxBezierWidth="180dip"
- huahua:additionalTipOffset="40dip"
- huahua:fontColor="#2278BF" />
- <TextView
- android:id="@+id/tv_index_center"
- android:visibility="gone"
- android:layout_width="100dp"
- android:layout_height="100dp"
- android:layout_centerInParent="true"
- android:textColor="#FFFFFF"
- android:gravity="center"
- android:textSize="36sp"
- android:background="@drawable/alpha_center_corner"
- android:text="A" />
- </RelativeLayout>
- </LinearLayout>
下面提供一下源码
参考资料
http://blog.csdn.NET/likendsl/article/details/7852658
http://www.jcodecraeer.com/a/opensource/2015/1104/3656.html