Android 实战之酷云(一)

前言

大家好啊!好久没有见到我了吧。为什么呢!当然是由于开学啦,这学期非常多课,身为部长实验室也也非常多活动和一堆小师弟。同一时候还有蓝桥杯和华为软件开发大赛。并且近期在做一个综合性比較高的作品,没错了,就是酷云,一款仿网易云音乐的在线播放器。当然了,如今我还没有完毕这个作品,并且仅仅是刚刚開始写而已,只是也遇到了非常多坑。所以写下这篇文章来记录一下这次开发中各种各样的坑,希望对各位有所帮助。文章会随着我的开发进度而不定期更新,各位有问题也能够给我发私信,我们一起讨论解决。

至于为什么选择网易云音乐我觉得主要是由于它不仅有着 93% 的主流音乐版权,能够找到最齐全的主流音乐。出色的用户界面和体验。让用户感觉舒适。还有人性化的体验,如生日推荐音乐。


自己定义 View 控件

不知道各位有没有体验过网易云音乐 APP ,通过我近期的体验。发现它有着非常好的用户界面。对各种操作都有着非常好的响应,大家能够先去体验一下。以下给出几张网易云音乐的 UI。

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

接下来放上酷云相相应的页面

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvamltX19jaGFybGVz/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="这里写图片描写叙述" title="">

说实话从開始接触自己定义 View 到如今也不久,学的不多,可是比較喜欢挑战,因此便有了这个项目的開始。好了,相信大家也已经体验过网易云的用户界面了。就算没有,接下来我也会一点点为你解答。


自己定义 SplashScreen 实现启动动画

/**
 * Created by JimCharles on 2017/3/7.
 */

import android.app.Activity;
import android.app.Dialog;
import android.graphics.Color;
import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.LinearLayout;

import com.androxue.coolcloud.R;

public class SplashScreen {

    public final static int SLIDE_LEFT = 1;
    public final static int SLIDE_UP = 2;
    public final static int FADE_OUT = 3;

    private Dialog splashDialog;

    private Activity activity;

    public SplashScreen(Activity activity) {
        this.activity = activity;
    }

    public void show(final int imageResource, final int animation) {
        Runnable runnable = new Runnable() {
            public void run() {
                // Get reference to display
                DisplayMetrics metrics = new DisplayMetrics();
//                Display display = activity.getWindowManager().getDefaultDisplay();

                // Create the layout for the dialog
                LinearLayout root = new LinearLayout(activity);
                root.setMinimumHeight(metrics.heightPixels);
                root.setMinimumWidth(metrics.widthPixels);
                root.setOrientation(LinearLayout.VERTICAL);
                root.setBackgroundColor(Color.BLACK);
                root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT, 0.0F));
                root.setBackgroundResource(imageResource);

                // Create and show the dialog
                splashDialog = new Dialog(activity, android.R.style.Theme_Translucent_NoTitleBar);
                // check to see if the splash screen should be full screen
                if ((activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN)
                        == WindowManager.LayoutParams.FLAG_FULLSCREEN) {
                    splashDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                            WindowManager.LayoutParams.FLAG_FULLSCREEN);
                }
                Window window = splashDialog.getWindow();
                switch (animation) {
                    case SLIDE_LEFT:
                        window.setWindowAnimations(R.style.dialog_anim_slide_left);
                        break;
                    case SLIDE_UP:
                        window.setWindowAnimations(R.style.dialog_anim_slide_up);
                        break;
                    case FADE_OUT:
                        window.setWindowAnimations(R.style.dialog_anim_fade_out);
                        break;
                }

                splashDialog.setContentView(root);
                splashDialog.setCancelable(false);
                splashDialog.show();

                // Set Runnable to remove splash screen just in case
                /*final Handler handler = new Handler();
                handler.postDelayed(new Runnable() {
                    public void run() {
                        removeSplashScreen();
                    }
                }, millis);*/
            }
        };
        activity.runOnUiThread(runnable);
    }

    public void removeSplashScreen() {
        if (splashDialog != null && splashDialog.isShowing()) {
            splashDialog.dismiss();
            splashDialog = null;
        }
    }

}

然后在 MianActivity 的onCreate()方法中加入以下两行代码就可以实现应用启动动画:

splashScreen = new SplashScreen(this);
        splashScreen.show(R.drawable.art_login_bg,
                SplashScreen.SLIDE_LEFT);

ViewPager+PagerTabStrip 实现滑动切屏

进去网易云后,主界面例如以下一个最大的特点就是滑动切屏了。这个效果既方便又帅气,所以网易云音乐中大量应用了这一效果,那么我们首先便来解说一下滑动切屏的实现。

首先构建在 activity_main.xml 中构建 ViewPager 和 PagerTabStrip。用来实现切屏效果,代码例如以下:

<android.support.v4.view.ViewPager
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:layout_gravity="center"
             android:gravity="center"
             android:id="@+id/vp">
             <android.support.v4.view.PagerTabStrip
                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:id="@+id/tap">
             </android.support.v4.view.PagerTabStrip>
    </android.support.v4.view.ViewPager>

然后构建切换的子 View,为了方便观察,这里仅仅是通过 ImageView 简单的设置显示不同的颜色,main_layout_1.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:id="@+id/view_1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="#76ff03"/>

</LinearLayout>

然后在 Activity 中进行设置,MainActivity.java

package com.androxue.coolcloud.activity;

import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.PagerTabStrip;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import com.androxue.coolcloud.R;

import java.util.ArrayList;

/**
 * Created by JimCharles on 2017/3/11.
 */

public class MainActivity extends AppCompatActivity {

    private ViewPager vp;
     //声明存储ViewPager下子视图的集合
     ArrayList<View> views = new ArrayList<>();
     //显示效果中每个视图的标题
     String[] titles={"私信", "评论", "@我", "通知"};

     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_message);
         vp = (ViewPager) findViewById(R.id.vp);
         initView();//调用初始化视图方法
         vp.setAdapter(new MyAdapter());//设置适配器
         ImageView back = (ImageView) findViewById(R.id.back);
         back.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Intent intent = new Intent(MessageActivity.this, MainActivity.class);
                 startActivity(intent);
             }
         });
     }

    //初始化视图的方法(通过布局填充器获取用于滑动的视图并存入相应的的集合)
    private void initView() {
        View v1 = getLayoutInflater().inflate(R.layout.main_layout_1, null);
        View v2 = getLayoutInflater().inflate(R.layout.main_layout_2, null);
        View v3 = getLayoutInflater().inflate(R.layout.main_layout_3, null);
        View v4 = getLayoutInflater().inflate(R.layout.main_layout_4, null);
        views.add(v1);
        views.add(v2);
        views.add(v3);
        views.add(v4);

        PagerTabStrip pagerTabStrip= (PagerTabStrip)findViewById(R.id.tap);
        pagerTabStrip.setDrawFullUnderline(false);//取消标题栏子View之间的切割线
        pagerTabStrip.setTabIndicatorColor(Color.RED);//改变指示器颜色为红色
        pagerTabStrip.setTextColor(Color.RED);//该变字体颜色为红色
    }

     private class MyAdapter extends PagerAdapter {

         @Override
         public int getCount() {
             return views.size();
         }

        @Override
         public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }

         //重写销毁滑动视图布局(将子视图移出视图存储集合(ViewGroup))
         @Override
         public void destroyItem(ViewGroup container, int position, Object object) {
             container.removeView(views.get(position));
         }

         //重写初始化滑动视图布局(从视图集合中取出相应视图,加入到ViewGroup)
         @Override
         public Object instantiateItem(ViewGroup container, int position) {
             View v = views.get(position);
             container.addView(v);
             return v;
         }

         @Override
         public CharSequence getPageTitle(int position) {
             return titles[position];
         }
    }
}

好了。一个滑动切屏的功能就这样完毕了。大家能够自行执行体验一下。


圆形头像

关于圆形头像的实如今之前Android 自己定义 View 之 draw 原理分析一文已经有讲到,这里简单的解说一下,顺便解说还有一种实现方式。两种方式都是通过自己定义View控件来实现的。

方式一:Shader+onDraw() 实现圆形头像

protected void onDraw(Canvas canvas) {
     super.onDraw(canvas);
     Paint paint = new Paint();
     paint.setAntiAlias(true);
     Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.girl);
     int radius = bitmap.getWidth()/2;
     BitmapShader bitmapShader = new BitmapShader(bitmap,Shader.TileMode.REPEAT,Shader.TileMode.REPEAT);
     paint.setShader(bitmapShader);
     canvas.translate(250,430);
     canvas.drawCircle(radius, radius, radius, paint);
}

和之前讲到的一样,甚至没有改代码,利用 BitmapShader 并重写 onDraw() 方法来实现圆形头像,只是这里有一点须要注意的是图片的像素大小才干完美的实现圆形头像。能够自行进行尝试。

方式二:自己定义 CircleImageView 实现圆形头像

package com.androxue.coolcloud.widget;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.util.AttributeSet;

import com.androxue.coolcloud.R;

/**
 * Created by JimCharles on 2017/3/9.
 */

public class CircleImageView extends android.support.v7.widget.AppCompatImageView {

    private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;

    private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
    private static final int COLORDRAWABLE_DIMENSION = 2;

    private static final int DEFAULT_BORDER_WIDTH = 0;
    private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
    private static final int DEFAULT_FILL_COLOR = Color.TRANSPARENT;
    private static final boolean DEFAULT_BORDER_OVERLAY = false;

    private final RectF mDrawableRect = new RectF();
    private final RectF mBorderRect = new RectF();

    private final Matrix mShaderMatrix = new Matrix();
    private final Paint mBitmapPaint = new Paint();
    private final Paint mBorderPaint = new Paint();
    private final Paint mFillPaint = new Paint();

    private int mBorderColor = DEFAULT_BORDER_COLOR;
    private int mBorderWidth = DEFAULT_BORDER_WIDTH;
    private int mFillColor = DEFAULT_FILL_COLOR;

    private Bitmap mBitmap;
    private BitmapShader mBitmapShader;
    private int mBitmapWidth;
    private int mBitmapHeight;

    private float mDrawableRadius;
    private float mBorderRadius;

    private ColorFilter mColorFilter;

    private boolean mReady;
    private boolean mSetupPending;
    private boolean mBorderOverlay;
    private boolean mDisableCircularTransformation;

    public CircleImageView(Context context) {
        super(context);

        init();
    }

    public CircleImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0);

        mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH);
        mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR);
        mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY);
        mFillColor = a.getColor(R.styleable.CircleImageView_civ_fill_color, DEFAULT_FILL_COLOR);

        a.recycle();

        init();
    }

    private void init() {
        super.setScaleType(SCALE_TYPE);
        mReady = true;

        if (mSetupPending) {
            setup();
            mSetupPending = false;
        }
    }

    @Override
    public ScaleType getScaleType() {
        return SCALE_TYPE;
    }

    @Override
    public void setScaleType(ScaleType scaleType) {
        if (scaleType != SCALE_TYPE) {
            throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
        }
    }

    @Override
    public void setAdjustViewBounds(boolean adjustViewBounds) {
        if (adjustViewBounds) {
            throw new IllegalArgumentException("adjustViewBounds not supported.");
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mDisableCircularTransformation) {
            super.onDraw(canvas);
            return;
        }

        if (mBitmap == null) {
            return;
        }

        if (mFillColor != Color.TRANSPARENT) {
            canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mFillPaint);
        }
        canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint);
        if (mBorderWidth > 0) {
            canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        setup();
    }

    @Override
    public void setPadding(int left, int top, int right, int bottom) {
        super.setPadding(left, top, right, bottom);
        setup();
    }

    @Override
    public void setPaddingRelative(int start, int top, int end, int bottom) {
        super.setPaddingRelative(start, top, end, bottom);
        setup();
    }

    public int getBorderColor() {
        return mBorderColor;
    }

    public void setBorderColor(@ColorInt int borderColor) {
        if (borderColor == mBorderColor) {
            return;
        }

        mBorderColor = borderColor;
        mBorderPaint.setColor(mBorderColor);
        invalidate();
    }

    /**
     * @deprecated Use {@link #setBorderColor(int)} instead
     */
    @Deprecated
    public void setBorderColorResource(@ColorRes int borderColorRes) {
        setBorderColor(getContext().getResources().getColor(borderColorRes));
    }

    /**
     * Return the color drawn behind the circle-shaped drawable.
     *
     * @return The color drawn behind the drawable
     *
     * @deprecated Fill color support is going to be removed in the future
     */
    @Deprecated
    public int getFillColor() {
        return mFillColor;
    }

    /**
     * Set a color to be drawn behind the circle-shaped drawable. Note that
     * this has no effect if the drawable is opaque or no drawable is set.
     *
     * @param fillColor The color to be drawn behind the drawable
     *
     * @deprecated Fill color support is going to be removed in the future
     */
    @Deprecated
    public void setFillColor(@ColorInt int fillColor) {
        if (fillColor == mFillColor) {
            return;
        }

        mFillColor = fillColor;
        mFillPaint.setColor(fillColor);
        invalidate();
    }

    /**
     * Set a color to be drawn behind the circle-shaped drawable. Note that
     * this has no effect if the drawable is opaque or no drawable is set.
     *
     * @param fillColorRes The color resource to be resolved to a color and
     *                     drawn behind the drawable
     *
     * @deprecated Fill color support is going to be removed in the future
     */
    @Deprecated
    public void setFillColorResource(@ColorRes int fillColorRes) {
        setFillColor(getContext().getResources().getColor(fillColorRes));
    }

    public int getBorderWidth() {
        return mBorderWidth;
    }

    public void setBorderWidth(int borderWidth) {
        if (borderWidth == mBorderWidth) {
            return;
        }

        mBorderWidth = borderWidth;
        setup();
    }

    public boolean isBorderOverlay() {
        return mBorderOverlay;
    }

    public void setBorderOverlay(boolean borderOverlay) {
        if (borderOverlay == mBorderOverlay) {
            return;
        }

        mBorderOverlay = borderOverlay;
        setup();
    }

    public boolean isDisableCircularTransformation() {
        return mDisableCircularTransformation;
    }

    public void setDisableCircularTransformation(boolean disableCircularTransformation) {
        if (mDisableCircularTransformation == disableCircularTransformation) {
            return;
        }

        mDisableCircularTransformation = disableCircularTransformation;
        initializeBitmap();
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        initializeBitmap();
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        initializeBitmap();
    }

    @Override
    public void setImageResource(@DrawableRes int resId) {
        super.setImageResource(resId);
        initializeBitmap();
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        initializeBitmap();
    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        if (cf == mColorFilter) {
            return;
        }

        mColorFilter = cf;
        applyColorFilter();
        invalidate();
    }

    @Override
    public ColorFilter getColorFilter() {
        return mColorFilter;
    }

    private void applyColorFilter() {
        if (mBitmapPaint != null) {
            mBitmapPaint.setColorFilter(mColorFilter);
        }
    }

    private Bitmap getBitmapFromDrawable(Drawable drawable) {
        if (drawable == null) {
            return null;
        }

        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        try {
            Bitmap bitmap;

            if (drawable instanceof ColorDrawable) {
                bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
            } else {
                bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG);
            }

            Canvas canvas = new Canvas(bitmap);
            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
            drawable.draw(canvas);
            return bitmap;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private void initializeBitmap() {
        if (mDisableCircularTransformation) {
            mBitmap = null;
        } else {
            mBitmap = getBitmapFromDrawable(getDrawable());
        }
        setup();
    }

    private void setup() {
        if (!mReady) {
            mSetupPending = true;
            return;
        }

        if (getWidth() == 0 && getHeight() == 0) {
            return;
        }

        if (mBitmap == null) {
            invalidate();
            return;
        }

        mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);

        mBitmapPaint.setAntiAlias(true);
        mBitmapPaint.setShader(mBitmapShader);

        mBorderPaint.setStyle(Paint.Style.STROKE);
        mBorderPaint.setAntiAlias(true);
        mBorderPaint.setColor(mBorderColor);
        mBorderPaint.setStrokeWidth(mBorderWidth);

        mFillPaint.setStyle(Paint.Style.FILL);
        mFillPaint.setAntiAlias(true);
        mFillPaint.setColor(mFillColor);

        mBitmapHeight = mBitmap.getHeight();
        mBitmapWidth = mBitmap.getWidth();

        mBorderRect.set(calculateBounds());
        mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f);

        mDrawableRect.set(mBorderRect);
        if (!mBorderOverlay && mBorderWidth > 0) {
            mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f);
        }
        mDrawableRadius = Math.min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f);

        applyColorFilter();
        updateShaderMatrix();
        invalidate();
    }

    private RectF calculateBounds() {
        int availableWidth  = getWidth() - getPaddingLeft() - getPaddingRight();
        int availableHeight = getHeight() - getPaddingTop() - getPaddingBottom();

        int sideLength = Math.min(availableWidth, availableHeight);

        float left = getPaddingLeft() + (availableWidth - sideLength) / 2f;
        float top = getPaddingTop() + (availableHeight - sideLength) / 2f;

        return new RectF(left, top, left + sideLength, top + sideLength);
    }

    private void updateShaderMatrix() {
        float scale;
        float dx = 0;
        float dy = 0;

        mShaderMatrix.set(null);

        if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
            scale = mDrawableRect.height() / (float) mBitmapHeight;
            dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
        } else {
            scale = mDrawableRect.width() / (float) mBitmapWidth;
            dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
        }

        mShaderMatrix.setScale(scale, scale);
        mShaderMatrix.postTranslate((int) (dx + 0.5f) + mDrawableRect.left, (int) (dy + 0.5f) + mDrawableRect.top);

        mBitmapShader.setLocalMatrix(mShaderMatrix);
    }

}

这样的方式是通过自己定义 View 来实现圆形头像的。代码非常easy


夜间模式

关于夜间模式的实现是在我开发过程中遇到的一个比較大坑,但我们都知道夜间模式是一款 APP 应该具备的,通过我近期的了解,总结出了四种实现方式。一是定义两套主题,对每个须要实现夜间模式的控件都另外定义一套夜间主题,但这样子project量大。我是一个人开发的。所以并没有採用这样的方式;二是利用 Google 官方提供的 UiModeManger;三是调用 NightModeHelper 实现夜间模式;四是利用 AppCompatDelegate 实现夜间模式。

Light_Style + Night_Style(即 setTheme)实现夜间模式

Light_Style + Night_Style 实现夜间模式的思路在于分别定义日间模式和夜间模式两套主题。然后再分别加入两套模式资源文件,也就是加入两个 module,生成 apk 文件。而模式的实现也就是对这两个 apk 文件进行处理,事实上也就是价格 .apk 后缀更改为 .skin 或者其他后缀,实现难度较大,不建议使用

UiModeManger 实现夜间模式()

UiModeManger 是谷歌官方给我们提供的实现夜间模式的方式,我们来看一下官方文档对它的描写叙述:

This class provides access to the system uimode services. These services allow applications to control UI modes of the device. It provides functionality to disable the car mode and it gives access to the night mode settings.

上面的意思就是:

这类提供对系统uimode服务的訪问。这些服务同意应用程序控制设备的UI模式。它提供了禁用汽车模式的功能,它同意訪问夜间模式设置。

看到这样的解释大家可能会有疑问。不是用来实现夜间模式的吗?怎么跑出了一个车载模式呢?细致看这个类的文档,发现有一个 setNightMode() 方法,这不就是设置夜间模式的意思吗?点进去看到这种方法的解释例如以下:

Sets the night mode. Changes to the night mode are only effective when the car or desk mode is enabled on a device.

意思例如以下:

设置夜间模式。对夜间模式的更改仅在设备上启用汽车或桌面模式时有效。

不得不说坑爹啊!

必须在开启车载模式或者桌面模式的条件下才干设置夜间模式,再细致看一下,发现UiModeManager 仅提供了方法设置车载模式的设置而没有Desk模式的设置方法,所以以下演示通过 UiModeManger 来实现夜间模式的方式。

首先在 res 包下创建 drawable-night-hdpi、drawable-night-mdpi、drawable-night-xhdpi、drawable-night-xxhdpi、drawable-night-xxxhdpi、values-night 目录,然后将相相应的资源放入新创建的相应的包下,并在 values-night 包下新建 colors.xml 资源文件,以下颜色仅供參考

<resources>

    <color name="night_mode_color ">#7f7f7f</color>
    <color name="night_mode_dark_color ">#d20000</color>
    <color name="night_mode_color ">#0a0a0a</color>    

</resources>

须要注意的一点是颜色名须要和 vales 包下的 colors.xml 文件里颜色名同样,但颜色值能够不同

然后在 values 包下的 styles.xml 文件里自己定义实现夜间模式的主题

<style name="Theme.Test" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="colorPrimary">@color/night_mode_color </item>
    <item name="colorPrimaryDark">@color/night_mode_dark_color </item>
    <item name="colorAccent">@color/night_mode_color </item>
    <item name="android:windowBackground">@color/dark</item>
</style>

好了。这样我们就自己定义了夜间模式的主题了,接下来在须要实现夜间模式的地方加上一下代码就能够实现夜间模式了

UiModeManager uiManager = (UiModeManager) getSystemService(Context.UI_MODE_SERVICE);
if (isNightMode) {
    uiManager.enableCarMode(0);
    uiManager.setNightMode(UiModeManager.MODE_NIGHT_YES);
} else {
    uiManager.disableCarMode(0);
    uiManager.setNightMode(UiModeManager.MODE_NIGHT_NO);
}

NightModeHelper 实现夜间模式()

import java.lang.ref.WeakReference;

import android.app.Activity;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.preference.PreferenceManager;

public class NightModeHelper {

    private static final String PREF_KEY = "nightModeState";

    private static int sUiNightMode = Configuration.UI_MODE_NIGHT_UNDEFINED;

    private WeakReference<Activity> mActivity;
    private SharedPreferences mPrefs;

    public NightModeHelper(Activity activity, int theme) {
        int currentMode = (activity.getResources().getConfiguration()
            .uiMode & Configuration.UI_MODE_NIGHT_MASK);
        mPrefs = PreferenceManager.getDefaultSharedPreferences(activity);
        init(activity, theme, mPrefs.getInt(PREF_KEY, currentMode));
    }

    public NightModeHelper(Activity activity, int theme, int defaultUiMode) {
        init(activity, theme, defaultUiMode);
    }

    private void init(Activity activity, int theme, int defaultUiMode) {
        mActivity = new WeakReference<Activity>(activity);
        if(sUiNightMode == Configuration.UI_MODE_NIGHT_UNDEFINED){
            sUiNightMode = defaultUiMode;
        }
        updateConfig(sUiNightMode);

    }

    private void updateConfig(int uiNightMode) {
        Activity activity = mActivity.get();
        if(activity == null){
            throw new IllegalStateException("Activity went away?

");
        }
        Configuration newConfig = new Configuration(activity.getResources().getConfiguration());
        newConfig.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;
        newConfig.uiMode |= uiNightMode;
        activity.getResources().updateConfiguration(newConfig, null);
        sUiNightMode = uiNightMode;
        if(mPrefs != null){
            mPrefs.edit()
                .putInt(PREF_KEY, sUiNightMode)
                .apply();
        }
    }

    public static int getUiNightMode() {
        return sUiNightMode;
    }

    public void toggle() {
        if(sUiNightMode == Configuration.UI_MODE_NIGHT_YES){
            notNight();
        }
        else{
            night();
        }
    }

    public void notNight() {
        updateConfig(Configuration.UI_MODE_NIGHT_NO);
        mActivity.get().recreate();
    }

    public void night() {
        updateConfig(Configuration.UI_MODE_NIGHT_YES);
        mActivity.get().recreate();
    }
}

用这样的方式实现夜间模式仅仅须要定义一个NightModeHelper.java类,然后在须要实现夜间模式的Activity的onCreate()方法setContentView()方法hou加上以下一行代码就可以:

mNightModeHelper = new NightModeHelper(this, R.style.AppTheme_Light);

AppCompatDelegate 实现夜间模式

//夜间模式
            SharedPreferences sp = this.getSharedPreferences("loonggg", MODE_PRIVATE);
            boolean isNight = sp.getBoolean("night", false);
            if (isNight) {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
                sp.edit().putBoolean("night", false).apply();
            } else {
                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
                sp.edit().putBoolean("night", true).apply();
            }
            recreate();

广告轮播的自己定义实现

在大多数应用 APP 的主页面上都会出现广告轮播,或是推广自己的产品。或是为其他公司做广告推销,但不得不说。这样的插入也让整个 UI 界面的风格别有一番风味。以下我们来看一看网易云音乐的广告轮播的实现方式吧。

首先新建一个 Info.java 类用来保存图片的网址、设置的标题等信息并实现 set 和 get 方法,代码例如以下:

public class Info {

  private String url;
  private String title;

  public Info(String title, String url) {
      this.url = url;
      this.title = title;
  }

  public String getUrl() {
      return url;
  }

  public void setUrl(String url) {
      this.url = url;
  }

  public String getTitle() {
      return title;
  }

  public void setTitle(String title) {
      this.title = title;
  }
}

然后自己定义轮播控件的布局。使用 ViewPager 来实现轮播效果,这里为了显示重叠的效果布局方式採用 RelativeLayout 即相对布局,然后嵌套两个 LinearLayout 分别用来存放指示器(在代码中动态加入)和显示标题。

详细代码例如以下:

<?

xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/white"
  android:orientation="vertical">
  <android.support.v4.view.ViewPager
      android:id="@+id/cycle_view_pager"
      android:layout_width="match_parent"
      android:layout_height="match_parent" />
  <LinearLayout
      android:id="@+id/cycle_indicator"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_alignParentBottom="true"
      android:layout_marginBottom="10dp"
      android:gravity="center"
      android:orientation="horizontal" />
  <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:layout_above="@id/cycle_indicator"
      android:orientation="vertical">
      <TextView
          android:id="@+id/cycle_title"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:layout_marginBottom="10dp"
          android:gravity="center"
          android:textColor="@android:color/white"
          android:textSize="20sp" />
  </LinearLayout>
</RelativeLayout>

上面说到要在代码中动态加入指示器,所以须要创建一个自己定义响应,新建 CycleViewPager.java 类

package com.androxue.coolcloud.widget;

import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

import com.androxue.coolcloud.R;
import com.androxue.coolcloud.activity.MainActivity;
import com.androxue.coolcloud.info.Info;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by JimCharles on 2017/3/19.
 */

public class CycleViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {

    private static final String TAG = "CycleViewPager";
    private Context mContext;

    private ViewPager mViewPager;//实现轮播图的ViewPager

    private TextView mTitle;//标题

    private LinearLayout mIndicatorLayout; // 指示器

    private Handler handler;//每几秒后执行下一张的切换

    private int WHEEL = 100; // 转动

    private int WHEEL_WAIT = 101; // 等待

    private List<View> mViews = new ArrayList<>(); //须要轮播的View。数量为轮播图数量+2

    private ImageView[] mIndicators;    //指示器小圆点

    private boolean isScrolling = false; // 滚动框是否滚动着

    private boolean isCycle = true; // 是否循环,默觉得true

    private boolean isWheel = true; // 是否轮播,默觉得true

    private int delay = 4000; // 默认轮播时间

    private int mCurrentPosition = 0; // 轮播当前位置

    private long releaseTime = 0; // 手指松开、页面不滚动时间,防止手机松开后短时间进行切换

    private ViewPagerAdapter mAdapter;

    private ImageCycleViewListener mImageCycleViewListener;

    private List<Info> infos;//数据集合

    private int mIndicatorSelected;//指示器图片,被选择状态

    private int mIndicatorUnselected;//指示器图片,未被选择状态

    final Runnable runnable = new Runnable() {

        @Override
        public void run() {
            if (mContext != null && isWheel) {
                long now = System.currentTimeMillis();
                // 检測上一次滑动时间与本次之间是否有触击(手滑动)操作,有的话等待下次轮播
                if (now - releaseTime > delay - 500) {
                    handler.sendEmptyMessage(WHEEL);
                } else {
                    handler.sendEmptyMessage(WHEEL_WAIT);
                }
            }
        }
    };

    public CycleViewPager(Context context) {
        this(context, null);
    }

    public CycleViewPager(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CycleViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.mContext = context;
        initView();
    }

    /**
     * 初始化View
     */
    private void initView() {
        LayoutInflater.from(mContext).inflate(R.layout.layout_circle_view, this, true);
        mViewPager = (ViewPager) findViewById(R.id.cycle_view_pager);
        mTitle = (TextView) findViewById(R.id.cycle_title);
        mIndicatorLayout = (LinearLayout) findViewById(R.id.cycle_indicator);

        handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if (msg.what == WHEEL && mViews.size() > 0) {
                    if (!isScrolling) {
                        //当前为非滚动状态,切换到下一页
                        int posttion = (mCurrentPosition + 1) % mViews.size();
                        mViewPager.setCurrentItem(posttion, true);
                    }
                    releaseTime = System.currentTimeMillis();
                    handler.removeCallbacks(runnable);
                    handler.postDelayed(runnable, delay);
                    return;

                }
                if (msg.what == WHEEL_WAIT && mViews.size() > 0) {
                    handler.removeCallbacks(runnable);
                    handler.postDelayed(runnable, delay);
                }
            }
        };
    }

    /**
     * 设置指示器图片。在setData之前调用
     * @param select   选中时的图片
     * @param unselect 未选中时的图片
     */
    public void setIndicators(int select, int unselect) {
        mIndicatorSelected = select;
        mIndicatorUnselected = unselect;
    }

    public void setData(List<Info> list, ImageCycleViewListener listener) {
        setData(list, listener, 0);
    }

    /**
     * 初始化viewpager
     * @param list         要显示的数据
     * @param showPosition 默认显示位置
     */
    public void setData(List<Info> list, ImageCycleViewListener listener, int showPosition) {

        if (list == null || list.size() == 0) {
            //没有数据时隐藏整个布局
            this.setVisibility(View.GONE);
            return;
        }

        mViews.clear();
        infos = list;

        if (isCycle) {
            //加入轮播图View。数量为集合数+2
            // 将最后一个View加入进来
            mViews.add(getImageView(mContext, infos.get(infos.size() - 1).getUrl()));
            for (int i = 0; i < infos.size(); i++) {
                mViews.add(getImageView(mContext, infos.get(i).getUrl()));
            }
            // 将第一个View加入进来
            mViews.add(getImageView(mContext, infos.get(0).getUrl()));
        } else {
            //仅仅加入相应数量的View
            for (int i = 0; i < infos.size(); i++) {
                mViews.add(getImageView(mContext, infos.get(i).getUrl()));
            }
        }

        if (mViews == null || mViews.size() == 0) {
            //没有View时隐藏整个布局
            this.setVisibility(View.GONE);
            return;
        }

        mImageCycleViewListener = listener;

        int ivSize = mViews.size();

        // 设置指示器
        mIndicators = new ImageView[ivSize];
        if (isCycle)
            mIndicators = new ImageView[ivSize - 2];
        mIndicatorLayout.removeAllViews();
        for (int i = 0; i < mIndicators.length; i++) {
            mIndicators[i] = new ImageView(mContext);
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
            lp.setMargins(10, 0, 10, 0);
            mIndicators[i].setLayoutParams(lp);
            mIndicatorLayout.addView(mIndicators[i]);
        }

        mAdapter = new ViewPagerAdapter();

        // 默认指向第一项。下方viewPager.setCurrentItem将触发又一次计算指示器指向
        setIndicator(0);

        mViewPager.setOffscreenPageLimit(3);
        mViewPager.setOnPageChangeListener(this);
        mViewPager.setAdapter(mAdapter);
        if (showPosition < 0 || showPosition >= mViews.size())
            showPosition = 0;
        if (isCycle) {
            showPosition = showPosition + 1;
        }
        mViewPager.setCurrentItem(showPosition);

        setWheel(true);//设置轮播
    }

    /**
     * 获取轮播图View
     */
    private View getImageView(Context context, String url) {
        return MainActivity.getImageView(context, url);
    }

    /**
     * 设置指示器。和文字内容
     * @param selectedPosition 默认指示器位置
     */
    private void setIndicator(int selectedPosition) {
        setText(mTitle, infos.get(selectedPosition).getTitle());
        try {

            for (int i = 0; i < mIndicators.length; i++) {
                mIndicators[i]
                        .setBackgroundResource(mIndicatorUnselected);
            }
            if (mIndicators.length > selectedPosition)
                mIndicators[selectedPosition]
                        .setBackgroundResource(mIndicatorSelected);
        } catch (Exception e) {
            Log.i(TAG, "指示器路径不对");
        }
    }

    /**
     * 页面适配器 返回相应的view
     */
    private class ViewPagerAdapter extends PagerAdapter {

        @Override
        public int getCount() {
            return mViews.size();
        }

        @Override
        public boolean isViewFromObject(View arg0, Object arg1) {
            return arg0 == arg1;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }

        @Override
        public View instantiateItem(ViewGroup container, final int position) {
            View v = mViews.get(position);
            if (mImageCycleViewListener != null) {
                v.setOnClickListener(new OnClickListener() {

                    @Override
                    public void onClick(View v) {
                        mImageCycleViewListener.onImageClick(infos.get(mCurrentPosition - 1), mCurrentPosition, v);
                    }
                });
            }
            container.addView(v);
            return v;
        }

        @Override
        public int getItemPosition(Object object) {
            return POSITION_NONE;
        }
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

    }

    @Override
    public void onPageSelected(int arg0) {
        int max = mViews.size() - 1;
        int position = arg0;
        mCurrentPosition = arg0;
        if (isCycle) {
            if (arg0 == 0) {

                //滚动到mView的1个(界面上的最后一个),将mCurrentPosition设置为max - 1
                mCurrentPosition = max - 1;
            } else if (arg0 == max) {
                //滚动到mView的最后一个(界面上的第一个)。将mCurrentPosition设置为1
                mCurrentPosition = 1;
            }
            position = mCurrentPosition - 1;
        }
        setIndicator(position);
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if (state == 1) { // viewPager在滚动
            isScrolling = true;
            return;
        } else if (state == 0) { // viewPager滚动结束

            releaseTime = System.currentTimeMillis();
            //跳转到第mCurrentPosition个页面(没有动画效果,实际效果页面上没变化)
            mViewPager.setCurrentItem(mCurrentPosition, false);

        }
        isScrolling = false;
    }

    /**
     * 为textview设置文字
     */
    public static void setText(TextView textView, String text) {
        if (text != null && textView != null) textView.setText(text);
    }

    /**
     * 为textview设置文字
     */
    public static void setText(TextView textView, int text) {
        if (textView != null) setText(textView, text + "");
    }

    /**
     * 是否循环,默认开启。必须在setData前调用
     */
    public void setCycle(boolean isCycle) {
        this.isCycle = isCycle;
    }

    /**
     * 是否处于循环状态
     */
    public boolean isCycle() {
        return isCycle;
    }

    /**
     * 设置是否轮播。默认轮播,轮播一定是循环的
     */
    public void setWheel(boolean isWheel) {
        this.isWheel = isWheel;
        isCycle = true;
        if (isWheel) {
            handler.postDelayed(runnable, delay);
        }
    }

    /**
     * 刷新数据。当外部视图更新后,通知刷新数据
     */
    public void refreshData() {
        if (mAdapter != null)
            mAdapter.notifyDataSetChanged();
    }

    /**
     * 是否处于轮播状态
     */
    public boolean isWheel() {
        return isWheel;
    }

    /**
     * 设置轮播暂停时间,单位毫秒(默认4000毫秒)
     * @param delay
     */
    public void setDelay(int delay) {
        this.delay = delay;
    }

    /**
     * 轮播控件的监听事件
     */
    public static interface ImageCycleViewListener {

        /**
         * 单击图片事件
         */
        public void onImageClick(Info info, int position, View imageView);
    }
}

然后再主页面中UI中引用自己定义的 CycleViewPager 控件并在 Activity 中进行相应操作就可以,详细代码例如以下:

<com.androxue.coolcloud.widget.CycleViewPager
        android:id="@+id/cycle_view"
        android:layout_width="395dp"
        android:layout_height="160dp"
        tools:layout_editor_absoluteY="0dp"
        tools:layout_editor_absoluteX="8dp" />
public class MainActivity extends AppCompatActivity {

  /**
   * 模拟请求后得到的数据
   */
  List<Info> mList = new ArrayList<>();

  /**
   * 轮播图
   */
  CycleViewPager mCycleViewPager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      initData();
      initView();
  }

  /**
   * 初始化数据
   */
  private void initData() {
      mList.add(new Info("标题1",
            "http://img2.3lian.com/2014/c7/25/d/40.jpg"));
      mList.add(new Info("标题2",
            "http://img2.3lian.com/2014/c7/25/d/41.jpg"));
      mList.add(new Info("标题3",
           "http://imgsrc.baidu.com/forum/pic/item/b64543a98226cffc8872e00cb9014a90f603ea30.jpg"));
      mList.add(new Info("标题4",
           "http://imgsrc.baidu.com/forum/pic/item/261bee0a19d8bc3e6db92913828ba61eaad345d4.jpg"));
  }

  /**
   * 初始化View
   */
  private void initView() {
      mCycleViewPager = (CycleViewPager) findViewById(R.id.cycle_view);
      //设置选中和未选中时的图片
      mCycleViewPager.setIndicators(R.mipmap.ad_select, R.mipmap.ad_unselect);
      //设置轮播间隔时间
      mCycleViewPager.setDelay(2000);
      mCycleViewPager.setData(mList, mAdCycleViewListener);
  }

  /**
   * 轮播图点击监听
   */
  private CycleViewPager.ImageCycleViewListener mAdCycleViewListener =
                new CycleViewPager.ImageCycleViewListener() {

      @Override
      public void onImageClick(Info info, int position, View imageView) {

          if (mCycleViewPager.isCycle()) {
              position = position - 1;
          }
          Toast.makeText(MainActivity.this, info.getTitle() +
               "选择了--" + position, Toast.LENGTH_LONG).show();
      }
  };
}

至此广告轮播功能已经实现了,有兴趣的朋友自己码一下。


时间: 2024-11-05 15:56:45

Android 实战之酷云(一)的相关文章

Android实战之酷云--&gt;仿网易云音乐开发

我的个人网站 Xuejianxin's Blog Google Blog Xuejianxin's Blog Android自定义View学习 Android自定义View之常用工具源码分析 Android自定义View之onMeasure()源码分析 Android自定义View之onLayout()源码分析 Android自定义View之对TouchEvent的处理 Android自定义View之draw原理分析 如果觉得我的文章还行的话,也可以关注我的公众号,里面也会第一时间更新,并且会有

跨平台:GN实践详解(ninja, 编译, windows/mac/android实战)

跨平台:GN实践详解(ninja, 编译, windows/mac/android实战)展开目录一.概览二.跨平台代码编辑器三.GN入门四.示范工程五.关键细节六.结语 [编译器选项] 其中前两部分是前缀部分,原本没有跨平台构建经验和知识的同学可以借助来帮助理解,后四部分则是讲述GN工程的基本结构.如何搭建一个GN构建的工程.以及关键的一些GN知识 一.概览如何开始这个话题是我比较在意的,因为对于部分人而言,真正从思维和理解上切入这篇文章真正要阐述的点是有困难的.这在于跨平台编译和开发这块,如果

android实战网址

10套Android实战经典资料分享 收集了10套Android学习资料,分享给大家,希望对学习这方面的朋友有帮助 1.实战Android手机客户端的家校通平台V1.0(SurfaceView实现统计图表)下载地址:http://pan.baidu.com/share/link?shareid=3121570264&uk=506606919&third=15 2.基于智能手机Android平台音乐播放器全程开发实战下载地址:http://pan.baidu.com/share/link?s

【Android实战】记录自学自定义GifView过程,能同时支持gif和其他图片!【实用篇】

之前写了一篇博客,<[Android实战]记录自学自定义GifView过程,详解属性那些事![学习篇]> 关于自定义GifView的,详细讲解了学习过程及遇到的一些类的解释,然后完成了一个项目,能通过在xml加入自定义 view (MyGifView)中加入自定义属性(my:gif_src = "@drawable/coffee"),达到播放gif图片的效果. 但是,有几个问题 1.gif_src 属性只支持 gif 图,并不支持其他类型的图片 2.只支持默认的引用图片,不

详解Android中那些酷炫返回方式的实现

Android手机都会有返回键,不管是实体键,还是虚拟键.Android用户主要也都是通过这个返回键操控页面返回方式的,不比IOS逼格甚高的只保留一个操作键.这种方式是最普遍的返回方式,还有一种也是比较常见的,那就是页面内部自己响应.绝大多数APP每个页面的设计图顶部左侧都会有一个返回键图标,偶尔也有奇葩的设计放在底部左侧,点击这个图标即finish掉当前页面.简单的介绍完了最常见的两种方式,下面为大家介绍两种更友好的交互方式. 拿大家比较常用的三款社交软件的交互来说.腾讯微博的返回方式除去上述

Android实战技巧之四十三:终止一个线程引起的

这是一道老牌面试题.通常面试官会问你对Java线程的了解,然后再问此问题. 从理论到实践,这是一条好路子. 线程是操作系统实现多任务的一种方式,可以理解为线程是一个任务的执行单元.比如Android系统中每个App都会有自己的主线程,同时还可以创建worker thread"并行"为我们工作. Java中创建新线程的方法 Java对线程(Thread)提供了语言级的支持(依托虚拟机吧).java.lang包下有Thread类和Runnable接口,都可以替你完成创建新线程的工作. 1.

【Android实战】记录自学自己定义GifView过程,能同一时候支持gif和其它图片!【有用篇】

之前写了一篇博客.<[Android实战]记录自学自己定义GifView过程,具体解释属性那些事! [学习篇]> 关于自己定义GifView的,具体解说了学习过程及遇到的一些类的解释,然后完毕了一个项目,能通过在xml增加自己定义 view (MyGifView)中增加自己定义属性(my:gif_src = "@drawable/coffee").达到播放gif图片的效果. 可是.有几个问题 1.gif_src 属性仅仅支持 gif 图,并不支持其它类型的图片 2.仅仅支持

Android实战技巧:深入解析AsyncTask

AsyncTask的介绍及基本使用方法 关于AsyncTask的介绍和基本使用方法可以参考官方文档和Android实战技巧:多线程AsyncTask这里就不重复. AsyncTask引发的一个问题 上周遇到了一个极其诡异的问题,一个小功能从网络上下载一个图片,然后放到ImageView中,是用AsyncTask来实现的,本身逻辑也很简单,仅是在doInBackground中用HTTP请求把图片的输入流取出,然后用BitmapFactory去解析,然后再把得到的Bitmap放到ImageView中

【Android语音合成TTS】云知声离线TTS使用详解

请尊重他人的劳动成果,转载请注明出处:[Android语音合成TTS]云知声离线TTS使用详解 PS. 云知声是2012年创立的,虽然只有1年多的发展历程,但其语音识别技术核心团队已经从业十余年,积累颇丰,这也是为什么能够短短一年时间能够在语音识别领域内做得风生水起的缘故.微语音插件.搜狗语音助手.乐视超级电视.老罗锤子操作系统.触宝输入法,还有网易+电信推出的的易信,用的是云知声的语音识别.2013年,云知声在资本市场的认可度非常高,极受追捧. 目前云知声提供免费的离线TTS,但API比较少,