1)需求:根据点击事件,在不同的界面控件的底部显示一张解析卡。
不管需求是否需要解析卡片跟随文章滑动而滑动,我们只需要在创建的布局节点上添加一张解析卡就行,而不需要像下面一样创建多张解析卡实例:
if(ObjectUtils.isEmpty(analysisCardView)) {
analysisCardView = AnalysisCardView(context, object : AnalysisCardView.ListenerUp {
override fun onArrowUp() {
mCoordinatorLayout.post{
analysisCardView?.visibility = View.GONE
topAnalysisCardView?.visibility = View.GONE
// mLlContentContainer.removeView(analysisCardView)
// mCoordinatorLayout?.removeView(topAnalysisCardView)
}
}
})
var transition = LayoutTransition()
mLlContentContainer?.layoutTransition = transition
}
if(ObjectUtils.isEmpty(topAnalysisCardView)) {
topAnalysisCardView = AnalysisCardView(context, object : AnalysisCardView.ListenerUp {
override fun onArrowUp() {
mLlContentContainer.post {
analysisCardView?.visibility = View.GONE
topAnalysisCardView?.visibility = View.GONE
// mLlContentContainer.removeView(analysisCardView)
// mConsLayoutContent?.removeView(topAnalysisCardView)
}
}
})
var transition = LayoutTransition()
mConsLayoutContent?.layoutTransition = transition
}
添加解析卡到根布局之后,不能频繁的addView()和removeView()操作, 因为添加View和removeView()方法并不是同步,频繁操作容易报如下错误:
Attempt to read from field 'int android.view.View.mViewFlags' on a null object reference
在做动画是碰到这个错误,后面发现是我在加动画图片这边也在移除动画。
重构解决方案:
根据需求,如果解析卡需要跟随文章滑动,我们就必须把解析卡添加到Activity的RootView节点,然后跟随手势滑动一起滑动。添加方式可以是在布局文件里面添加或者通过代码动态添加。如果解析卡不需要跟随文章滑动而滑动,那么我们会发现Popwindow可以设置显示在控件的固定某个位置,然后当用户滑动的时候显示,完整实现需求。
if(!isFinishing) {
mAnalysisCardPop?.showAsDropDown(mTvReadParsingCard)
}
创建解析卡的时候可以设置解析卡的入场动画和出场动画。
mAnalysisCardPop = AnalysisCardPop(applicationContext)
mAnalysisCardPop?.animationStyle = R.style.articleAnalysisAnimation
2)需求:在文章详情页面实现一键机读和变速播放功能。
变速播放是在安卓6.0之后才有的功能,在安卓6.0一下实现变速播放需要使用第三方代码实现。前期项目用的是腾讯播放器实现播放功能,开发完成后发现腾讯云的播放器需要集成一个非常大的libs文件,文件大小大概是25MB。代码实现是硬搬腾讯云SDK的实现代码,根本没有考虑实际需求是否需要那么复杂的代码需求。 第二点就是变速播放用的是腾讯云的播放器SDK,而一键机读用的是百度的语音合成,两个播放器的功能实现完全不一样却被杂糅在了同一个AudiaManager类里面,播放是通过绑定服务,然后在绑定服务的时候这个AudiaManager里面实现一个IPlayer接口,这个接口定义了所有播放器的功能,然后AudiaManager拿到了百度语音合成和一键机读文章的功能代码实现来实现具体的播放功能。
然后Activity界面拿到的直接播放器对象就是AuidaManager,通过这个对象实现的播放器方法来实现具体的播放功能,这样的耦合已经不能说是复杂了。
当我们发现只要实现IPlayer这个接口就可以把一个类包装成一个播放器的具体实现,我们就可以通过让百度语音的具体实现类和实现变速播放的实现各自实现这个播放器包装接口来直接把这两个功能实现包装成一个播放器,然后具体业务来调用各自播放器的功能方法,包装接口如下:
/**
* 播放器包装接口
* @author guotianhui
*/
public interface IPlayerWapper {
void initPlayer(Context context);
void prepareDataSource(String mediaSourece,Long srouceDuration);
void setPlaySpeed(float speed);
void startPlay();
void pause();
void stop();
void resumePlay();
boolean isPlaying();
boolean isResumePlaying();
long getPlayerPosition();
void seekToPlayerPosition(long progress);
void releasePlayer();
}
里面变速播放用的是谷歌的ExoPlay,具体实现代码。
重写变速播放器删除的实现代码:
3)变速播放中,更新播放进度和更新显示进度框的进度条时间。
变速播放器界面显示为一个自定义控件,这个自定义控件里面也自定义了一个自定义进度条,进度条显示的实现是播放进度的时间。目前是把进度条显示进度的逻辑耦合到自定义播放器控件的界面里面。
private class ProgressCallback implements Runnable {
private boolean isPlaying;
@Override
public void run() {
if (ObjectUtils.isEmpty(mDifferentSpeedPlayer)) return;
// long duration = mDifferentSpeedPlayer.getPlayerPosition();
int duration = 0;
refreshSeekBarView(++duration);
if (isPlaying()) {
getHandler().postDelayed(this, UPDATE_PROGRESS_INTERVAL);
}
}
public boolean isPlaying() {
return isPlaying;
}
public void setPlaying(boolean playing) {
isPlaying = playing;
}
}
发送Handler消息代码逻辑是用的一个接口回调,从Activity里面获取到Handler:
private CallBack mCallBack;
public void setCallBack(CallBack callBack) {
mCallBack = callBack;
}
public interface CallBack {
void showNotificationUseMobileNet(AudioPlayerLayout.PlayType paramPlayingType);
Handler getHandler();
}
然后移除Handler消息不仅仅在自定义播放器界面移除了消息,而且还在自定义播放器控件里面写了一个获取线程实现类,然后不仅仅移除线程实现类,还需要移除消息。
public Runnable getProgressCallback() {
return mProgressCallback;
}
public Handler getHandler() {
Handler handler = null;
if (ObjectUtils.isNotEmpty(mCallBack)) {
handler = mCallBack.getHandler();
}
if (ObjectUtils.isNotEmpty(handler)){
handler = new Handler();
}
return handler;
}
Handler消息是可以自己实现Handler的handlerMessage方法,然后拿到发送的message做相应的处理,但是现在是新创建了一个线程,然后通过Handler来开启这个更新播放进度的线程类:
getHandler().post(mProgressCallback);
优化方案:首先,我们先把自定义变速播放器这个界面的功能实现,然后拿到视频的总长度,然后把视频总长度传递给自定义进度条,进度条里面去实现具体的更改显示进度和进度条值的逻辑,提供快进15秒和后退15秒的方法,通过调用方法来实现更改界面,然后我们发现播放时间和视频进度显示是不同步的,所以我们需要拿到总视频长度,让总视频长度除以100就是没一个进度值所代表的时间,比如视频总长度是226.除以100就是2.26,就是自定义进度条没走一小格所代表的进度。
/**
* 播放器播放进度条
* @author guotianhui
*/
public class HorizontalSeekBar extends AppCompatSeekBar {
private String mTime;
private int thumbColorId;
private int textWidth;
private int mRealWidth;
private int mShowProgress;
private int mAverageProgress;
private int mPlayProgress = 0;
private int mPlayTotalDuration;
private Paint mPaint = new Paint();
private final int DEFAULT_TEXT_SIZE = 9;//sp
private final int DEFAULT_TEXT_COLOR = 0xFFFC00D1;
private final int DEFAULT_COLOR_UNREACH = 0xFFD3D6DA;
private final int DEFAULT_HEIGHT_UNREACH = 2;
private final int DEFAULT_COLOR_REACH = DEFAULT_TEXT_COLOR;
private final int DEFAULT_HEIGHT_REACH = 2;//dp
private final int DEFAULT_TEXT_OFFSET = 0;//dp
private int mTextSize = sp2px(DEFAULT_TEXT_SIZE);
private int mTextColor = DEFAULT_TEXT_COLOR;
private int mUnReachColor = DEFAULT_COLOR_UNREACH;
private int mUnReachHeight = dp2px(DEFAULT_HEIGHT_UNREACH);
private int mReachColorStart = DEFAULT_COLOR_REACH;
private int mReachColorEnd = DEFAULT_COLOR_REACH;
private int mReachHeight = dp2px(DEFAULT_HEIGHT_REACH);
private int mTextOffset = dp2px(DEFAULT_TEXT_OFFSET);
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case 100:
++mShowProgress;
mPlayProgress = mAverageProgress+ mPlayProgress;
Log.e(">>>>>>>>>>>","handleMessage:"+mPlayProgress);
setProgress(mPlayProgress);
startPlaying();
invalidate();
break;
default:{
}
break;
}
}
};
public HorizontalSeekBar(Context context) {
this(context, null);
}
public HorizontalSeekBar(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HorizontalSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
obtainStyledAttrs(attrs);
}
/**
* 获取自定义属性
* @param attrs
*/
private void obtainStyledAttrs(AttributeSet attrs) {
TypedArray ta=getContext().obtainStyledAttributes(attrs, R.styleable.HorizontalSeekBar);
mTextSize= (int) ta.getDimension(R.styleable.HorizontalSeekBar_progress_text_size, mTextSize);
mTextColor=ta.getColor(R.styleable.HorizontalSeekBar_progress_text_color, mTextColor);
mTextOffset= (int) ta.getDimension(R.styleable.HorizontalSeekBar_progress_text_offset, mTextOffset);
mUnReachColor=ta.getColor(R.styleable.HorizontalSeekBar_progress_unreach_color, mUnReachColor);
mUnReachHeight= (int) ta.getDimension(R.styleable.HorizontalSeekBar_progress_unreach_height, mUnReachHeight);
mReachColorStart =ta.getColor(R.styleable.HorizontalSeekBar_progress_reach_color, mReachColorStart);
mReachHeight= (int) ta.getDimension(R.styleable.HorizontalSeekBar_progress_reach_height, mReachHeight);
ta.recycle();
mPaint.setTextSize(mTextSize);
}
@Override
protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthVal=MeasureSpec.getSize(widthMeasureSpec);
int height=measureHeight(heightMeasureSpec);
setMeasuredDimension(widthVal,height);
mRealWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
}
private int measureHeight(int heightMeasureSpec) {
int result;
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
if(mode == MeasureSpec.EXACTLY) {
result = size;
} else {
int textHeight= (int) (mPaint.descent() - mPaint.ascent());
result = getPaddingTop() + getPaddingBottom()
+Math.max(Math.max(mReachHeight,mUnReachHeight),
Math.abs(textHeight));
if(mode == MeasureSpec.AT_MOST)
{
result = Math.min(result,size);
}
}
return result;
}
@Override
protected synchronized void onDraw(Canvas canvas) {
//锁画布(为了保存之前的画布状态)
Log.e(">>>>>>>>>>>","onDraw");
try {
canvas.save();
//把当前画布的原点移到(x, y),后面的操作都以(x, y)作为参照点,默认原点为(0,0)
canvas.translate(getPaddingLeft(), getHeight() / 2);
boolean noNeedUnreach = false;
String text = mTime;
if (ObjectUtils.isNotEmpty(text)) {
//draw reach bar
if (textWidth <= 0) {
textWidth = (int) mPaint.measureText(text);
}
}
float radio = getProgress() * 1.0f / getMax();
float progressX = radio * mRealWidth;
if (progressX + textWidth > mRealWidth) {
progressX = mRealWidth - textWidth;
noNeedUnreach = true;
}
float endX = progressX - mTextOffset / 2;
if (endX > 0) {
mPaint.setColor(getColor_(mReachColorStart));
mPaint.setStrokeWidth(mReachHeight);
Shader mShader = new LinearGradient(0, 0, endX, 0, new int[]{getColor_(mReachColorStart),
getColor_(mReachColorEnd)}, null, Shader.TileMode.CLAMP);
mPaint.setShader(mShader);
canvas.drawLine(0, 0, endX, 0, mPaint);
}
//draw unreach bar
if (!noNeedUnreach) {
float start = progressX + mTextOffset / 2;
mPaint.setColor(getColor_(mUnReachColor));
mPaint.setStrokeWidth(mUnReachHeight);
mPaint.setShader(null);
canvas.drawLine(start, 0, mRealWidth, 0, mPaint);
}
//draw text
mPaint.setColor(getColor_(mTextColor));
float y = (-(mPaint.descent() + mPaint.ascent()) / 2);
drawRoundRect(canvas, progressX, -(getHeight() / 2));
float px = progressX + (dp2px(64) - textWidth) / 2;
mPaint.setShader(null);
canvas.drawText(text, px, y, mPaint);
//把当前画布返回(调整)到上一个save()状态之前
canvas.restore();
}catch (Exception e){
Log.e(">>>>>>>>>>","e:"+e.getMessage());
}
}
private void drawRoundRect(Canvas canvas, float left, float top) {
//新建一只画笔,并设置为绿色属性
Paint _paint = new Paint();
//新建矩形r1
RectF r1 = new RectF();
r1.left = left;
r1.top = top;
r1.right = left + dp2px(64);
r1.bottom = top + getHeight();
//画出圆角矩形r2
_paint.setColor(getColor_(thumbColorId));
canvas.drawRoundRect(r1, 10, 10, _paint);
}
public void setPlayDuration(int playDuration){
mPlayTotalDuration = playDuration;
mAverageProgress = playDuration /100;
}
public void startPlaying(){
if(mPlayTotalDuration >=mPlayProgress){
setPrograssDurationText();
mHandler.sendEmptyMessageDelayed(100,mAverageProgress* 1000); //延迟更新进度条
setPrograssDurationText();
}
}
private void setPrograssDurationText() {
mTime = DateUtils.formatDurationS(mShowProgress).concat(" / ")
.concat( DateUtils.formatDurationS(mPlayTotalDuration));
}
public void stopPlaying(){
mHandler.removeMessages(100);
}
public void resetPlayPrograss(){
setProgress(0);
}
public void fastForwordPrograss(int fastTime){
if(mAverageProgress >0) {
int currentPrograss = getProgress();
setProgress(currentPrograss + fastTime);
mPlayProgress = getProgress();
setPrograssDurationText();
}
}
public void backAwayPrograss(int backTime){
if(mAverageProgress >0) {
int currentPrograss = getProgress();
setProgress(currentPrograss - backTime);
mPlayProgress = getProgress();
setPrograssDurationText();
}
}
public void setText(String time) {
mTime = time;
invalidate();
}
public void setTextColor(int colorId) {
mTextColor = colorId;
invalidate();
}
public void setThumbColor(int thumbColorId) {
this.thumbColorId = thumbColorId;
invalidate();
}
public void setReachColorStart(int reachColorStart, int reachColorEnd) {
mReachColorStart = reachColorStart;
mReachColorEnd = reachColorEnd;
}
public void setUnReachColor(int unReachColor) {
mUnReachColor = unReachColor;
}
/**
* 封装获取颜色
* @return
*/
public int getColor_(int colorId) {
return ContextCompat.getColor(getContext(), colorId);
}
protected int dp2px(int dpval){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dpval,
getResources().getDisplayMetrics());
}
protected int sp2px(int spValue){
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,spValue,
getResources().getDisplayMetrics());
}
@Override
public void setProgress(int progress) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setProgress(progress, true);
} else {
super.setProgress(mPlayProgress);
}
}
}