现在讲一下今天要完成和前面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