Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android程序开发————ActionBar和ToolsBar

$
0
0

1. 什么是ActionBar:

 Google在Android3.0以后,为了避免开发人员总是仿照iOS界面去开发,并且要与iOS界面去抗衡;同时,为了给用户更多的空间,Google提出了一个新的设计理念: 将最常用的按钮,放到标题栏上面,这样用户可以快速的点击,所以形成了”ActionBar”。

2.使用

1. ActionBar 需要考虑使用哪一个版本;提供了 v7 的兼容版本(Android 2.1),和标准版本(Android 3.0 之后可用)
2. AppCompatActivity 或者是 ActionBarActivity 默认已经包含ActionBar的支持了
3. 通过 options menu,可以给ActionBar设置菜单和标题动作按钮
4. ActionBar 支持 Tab导航和下拉列表导航;通过代码来设置
5. ActionBar 使用 SearchView 可以实现在标题栏进行搜索
6. ActionBar 可以添加分享功能;

3. Menu item

1. 创建OptionsMenu,指定资源文件,在资源文件中,给指定的item设置 showAsAction 就可以添加ActionBar相应的按钮;
2. showAsAction 指定的属性,不能够100%确保期望的效果,因为ActionBar显示在标题上面的按钮,显示效果依赖于屏幕的宽度;
3. always 是总是显示在标题栏上面,当按钮非常多,会把菜单项挤出去;
4. ifRoom: 代表,当ActionBar的宽度还有剩余,能够继续放置菜单,那么这个时候,当前菜单项可能能够显示在ActionBar的标题上面;如果没有空间,那么当前的菜单就显示在 “三个点”代表的菜单项当中;对于有些手机,需要点 “Menu”按键,才会显示出来菜单。
4.1 never : 代表指定的Action永远不再ActionBar(标题栏)上面显示
5. withText  当ActionBar空间足够,能够同时显示标题,图标的时候,就会自动显示标题;如果没有空间,那么不显示标题

orderInCategory 属性:填写数字即可,数字会根据所有Action的值按照升序排列,数字 1 第一个显示,数字越大越在后面显示。
orderInCategory :没有设置这个属性的item始终按照xml的顺序在最前面,之后才是跟着有顺序的item
ActionBar 显示的顺序,先将所有的没有 orderInCategory属性的Action 按照 XML书写的顺序,进行显示,之后才会进行有 orderInCategory 属性的Action,进行排序,再显示。
先显示没有orderInCategory的菜单,之后才是有orderInCategory的菜单

4.代码解决一切

ActionBar的简单使用

menu文件

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/actionbar_settings"
          android:title="设置"
          app:showAsAction="always"
          android:orderInCategory = "1"/>
    <item android:id="@+id/actionbar_about"
          android:title="关于"
        android:orderInCategory="2"/>
    <item android:id="@+id/actionbar_search"
          android:title="搜索"
          android:icon="@drawable/ic_action_search"
          app:showAsAction="always"/>
</menu>

主类实现

package com.treasure_ct.android_xt.basedfunction.actionbar;

import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.treasure_ct.android_xt.R;

public class ActionBar_SimpleUse_Activity extends AppCompatActivity implements ActionBar.TabListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_basedfunction_actionbar_simpleuse);

        //ActionBar的基本使用
        //1.获取ActionBar,注意可能为null
        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null) {
            //2.设置
            //2.1设置后退,当被点击,相当一一个菜单点击方法,id =android.R.id.home
            actionBar.setDisplayHomeAsUpEnabled(true);//设置最左边的menu是否为后退
//            actionBar.setHomeAsUpIndicator(R.mipmap.icon_main1);//设置后退的图标
            actionBar.setDisplayShowTitleEnabled(false);//去掉标题的文字
            //2.2 导航模式,Tab模式,用于VP 和Fragment
            //1,。设置模式
            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
            //2.先创建Tab,设置监听器,添加Tab
            ActionBar.Tab tab = actionBar.newTab();
            tab.setTabListener(this);
            tab.setText("首页");
            actionBar.addTab(tab);

            tab = actionBar.newTab();
            tab.setTabListener(this);
            tab.setText("详情");
            actionBar.addTab(tab);

            tab = actionBar.newTab();
            tab.setTabListener(this);
            tab.setText("更多");
            actionBar.addTab(tab);

        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.actionbar_main_item,menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.actionbar_settings:
                Toast.makeText(ActionBar_SimpleUse_Activity.this, "设置biubiubiu", Toast.LENGTH_SHORT).show();
                break;
            case android.R.id.home:
                finish();
                break;
        }
        return true;
    }

    @Override
    public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
        //FragmentTransaction  参数不允许commit()调用就出错
        int position = tab.getPosition();
        Toast.makeText(ActionBar_SimpleUse_Activity.this, String.valueOf(position)+": "+tab.getText(), Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {

        //FragmentTransaction  参数不允许commit()调用就出错
    }

    @Override
    public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
        //FragmentTransaction  参数不允许commit()调用就出错
    }
}
效果:




做一个下拉列表。类似于日历上的  选择按月显示还是按周显示

package com.treasure_ct.android_xt.basedfunction.actionbar;

import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.Toast;

import com.treasure_ct.android_xt.R;

import java.util.ArrayList;

public class ActionBar_Spinner_Activity extends AppCompatActivity implements ActionBar.OnNavigationListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_basedfunction_actionbar_spinner);

        // 1. getSupportActionBar()

        // 2. 设置导航模式为列表

        // 3. 设置下拉列表导航的 Adapter

        // 4. 列表选中接口回调

        ActionBar actionBar = getSupportActionBar();
        if (actionBar != null){
            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
            actionBar.setDisplayShowTitleEnabled(false);//去掉标题栏的文字
            ArrayList<String> list = new ArrayList<>();
            list.add("按天");
            list.add("按周");
            list.add("按月");
            list.add("按年");
            ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,android.R.layout.simple_dropdown_item_1line,list);
            actionBar.setListNavigationCallbacks(adapter,this);
        }
    }

    @Override
    public boolean onNavigationItemSelected(int itemPosition, long itemId) {
        boolean ret = true;
        Toast.makeText(ActionBar_Spinner_Activity.this, "选中"+itemPosition, Toast.LENGTH_SHORT).show();
        return ret;
    }
}


效果:





3.在标题栏开发一个搜索界面

v4包下

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
<!--collapseActionView 代表当前MenuItem点击的时候能偶显示或隐藏一个actionView-->
    <!--V4包内部提供了SearchView的创建工具类,不需要actionViewClass-->
    <item android:id="@+id/actionbar_search"
      android:title="搜索"
      app:showAsAction="always|collapseActionView"
      android:icon="@drawable/ic_action_search"
        />
</menu>
package com.treasure_ct.android_xt.basedfunction.actionbar;

import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SearchViewCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.SearchView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

import com.treasure_ct.android_xt.R;

public class ActionBar_Search_Activity extends AppCompatActivity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_basedfunction_actionbar_search);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.actionbar_search_item,menu);
        //设置搜索输入框的步骤
        //1.查找指定的MenuItem
        MenuItem item = menu.findItem(R.id.actionbar_search);
        //2.设置SearchView
        View view = SearchViewCompat.newSearchView(this);
        item.setActionView(view);
        MenuItemCompat.setActionView(item, view);
        return true;
    }

}

V7包下



<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
    <!--collapseActionView 代表当前MenuItem点击的时候能偶显示或隐藏一个actionView-->
    <!--V4包内部提供了SearchView的创建工具类,不需要actionViewClass-->
    <!-- V4包内部提供了SearchView的创建工具类, V7包 提供了 android.support.v7.widget.SearchView -->
    <item
        android:id="@+id/actionbar_search"
        android:icon="@drawable/ic_action_search"
        android:title="搜索一下"
        app:actionViewClass="android.support.v7.widget.SearchView"
        app:showAsAction="always|collapseActionView"/>
</menu>
<pre name="code" class="java">package com.treasure_ct.android_xt.basedfunction.actionbar;

import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SearchViewCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.SearchView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;

import com.treasure_ct.android_xt.R;

public class ActionBar_Search_Activity extends AppCompatActivity implements SearchView.OnQueryTextListener {
    private SearchView searchView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_basedfunction_actionbar_search);

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.actionbar_search_item,menu);
        //设置搜索输入框的步骤
        //1.查找指定的MenuItem
        MenuItem item = menu.findItem(R.id.actionbar_search);
        //2.设置SearchView
//     2   View view = SearchViewCompat.newSearchView(this);
//      1  item.setActionView(view);
//     2   MenuItemCompat.setActionView(item, view);
        View view = MenuItemCompat.getActionView(item);
        if (view != null){
             searchView = (SearchView) view;

            //设置 SearchView 的查询回调接口
            searchView.setOnQueryTextListener(this);

//            //在搜索输入框没有显示的时候,点击Action,回调这个接口,冰洁显示输入框
//            searchView.setOnSearchClickListener();

            //档自动补全的 的内容被选中,回调接口
//            searchView.setOnSuggestionListener();

            //可以设置搜索的自动补全功能,也可以搜索历史数据
//            searchView.setSuggestionsAdapter();
        }
        return true;
    }

    /**
     * 当用户在输入法中点击搜索按钮时,调用这个方法,发起实际的搜索功能。
     * @param query
     * @return
     */
    @Override
    public boolean onQueryTextSubmit(String query) {
        Toast.makeText(ActionBar_Search_Activity.this, "su= b   "+query, Toast.LENGTH_SHORT).show();
        searchView.clearFocus();
        return true;
    }

    /**
     * 每一次输入字符都会回调这个方法,联想功能
     * @param newText
     * @return
     */
    @Override
    public boolean onQueryTextChange(String newText) {
        Toast.makeText(ActionBar_Search_Activity.this, "chhhhhhhh:  "+newText, Toast.LENGTH_SHORT).show();
        return true;
    }

}

效果






   新特性   ToolsBar


作者:treasureqian 发表于2016/9/19 20:33:50 原文链接
阅读:127 评论:0 查看评论

5CoordinatorLayout与AppBarLayout--嵌套滑动

$
0
0

5CoordinatorLayout与AppBarLayout–嵌套滑动

上文我们说了AppBarLayout的简单滑动,本篇主要介绍CoordinatorLayout下的嵌套滑动相关知识,本文对此做介绍

例子

按照惯例,先看效果,再谈原理。可以看到在向上滑动的时候,先滑动AppBarLayout,AppBarLayout完全消失之后,在滑动NestedScrollView。而在向下滑动的时候,依然是先滑动AppBarLayout,等AppBarLayout完全滑下来之后,再滑动NestedScrollView。

代码非常简单,如下所示,关键代码就2句,toolbar内的app:layout_scrollFlags=”scroll|enterAlways”以及NestedScrollView内的 app:layout_behavior=”@string/appbar_scrolling_view_behavior”

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.fish.behaviordemo.MainActivity">

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:theme="@style/AppTheme.AppBarOverlay"
        android:layout_height="wrap_content">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            app:popupTheme="@style/AppTheme.PopupOverlay"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:layout_scrollFlags="scroll|enterAlways" />
            <!--app:layout_scrollFlags="scroll" />-->


    </android.support.design.widget.AppBarLayout>


    <!--关键代码-->
    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
            android:id="@+id/linear"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>


    <android.support.design.widget.FloatingActionButton
        android:id="@+id/fab"

        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="@dimen/fab_margin"
        android:src="@android:drawable/ic_dialog_email"
        app:layout_behavior="com.fish.behaviordemo.fab.MyBehavior" />

</android.support.design.widget.CoordinatorLayout>

解决方案

在分析原理前,我们可以先想想,如果要自己实现该怎么实现?
这个例子要求是,Toolbar可滑时,滑动Toolbar,Toolbar滑完后,滑动NestedScrollView。滑动时Toolbar优先于NestedScrollView

简单想了下,至少有以下4种解决方案
1、最古老的实现方式就是,在Toolbar+ NestedScrollView的外层包一个NestedScrollView(简称p) ,重写p的onInterceptTouchEvent,如果p可滑动就拦截,若p不可滑动,就交给c处理,p的高度要设置为toolbar+content的高度。这个方式有个缺点,那就是在一次滑动过程中,如果p滑动了,那c不可能滑动,也就是说不可能在一次滑动过程中,既滑动了p也滑动了c,但是上面的例子是可以实现既滑动p又滑动c的
2、使用嵌套滑动,同样需要在Toolbar+ NestedScrollView的外层包一个NestedScrollView(简称p),在p的onNestedPreScroll里处理滑动事件,这个可以实现和例子一样的效果,其实该例子用的就是嵌套滑动的方法,只是更复杂一点
3、把NestedScrollView加一个header,刚好和Toolbar叠在一起,一开始上滑的时候,NestedScrollView其实已经在滑动了,但是我们看不出来,因为在滑head,而toolbar监视NestedScrollView的滑动,然后滑动自己。这种实现方式,本质是NestedScrollView一直在滑,而toolbar监视到p滑动之后自己再滑动,其实此时是2个view同时在滑动
4、把NestedScrollView加一个padding,和Toolbar一样大,然后设置android:clipToPadding=”false”,再由toolbar监视NestedScrollView的滑动,然后滑动自己,这个方法是方法3的一个变种,此时也是2个view同时在滑动

原理分析

先大概讲一下原理,不然看代码会晕。

初始时NestedScrollView上滑会触发CoordinatorLayout的onNestedPreScroll,在这里把滑动分发给AppBarLayout,触发AppBarLayout的上移,以及NestedScrollView 的上移,看起来好像是CoordinatorLayout在滚动一样。

然后AppBarLayout由于上移逐渐消失,那么AppBarLayout就无法消耗滑动事件,所以触发NestedScrollView自己的滑动
这里有坑,未独占,可能被拦截。
下滑的时候也一样,下滑触发CoordinatorLayout的onNestedPreScroll,分发给AppBarLayout,AppBarLayout如果可以滑,就由AppBarLayout消费掉,如果AppBarLayout不能滑,就NestedScrollView自己消费。

AppBarLayout的滑动,我们之前已经说过了。

子view与behavior

view behavior
AppBarLayout AppBarLayout.Behavior
NestedScrollView AppBarLayout.ScrollingViewBehavior
FloatingActionButton MyBehavior

上滑源码分析

嵌套滑动初始化

我这里默认大家对嵌套滑动的知识有所了解,不了解的可以参考
我们先来找嵌套滑动的链表,

NestedScrollView-> CoordinatorLayout

CoordinatorLayout实现了NestedScrollingParent,所以是嵌套滑动链上的NestedScrollView父节点,会收到NestedScrollView滑动的各种事件。
手指在屏幕上滑动,会触发NestedScrollView的move事件。

再来看真正建立嵌套滑动链表的代码,NestedScrollView调用startNestedScroll,会调用CoordinatorLayout的onStartNestedScroll
onStartNestedScroll内的逻辑是遍历子view,如果子view的behavior的onStartNestedScroll返回true,那么调用acceptNestedScroll把mDidAcceptNestedScroll置为true。这里涉及到了behavior的另一个函数onStartNestedScroll,这个函数有什么意义,CoordinatorLayout可以接收嵌套滑动事件并且交给他的子view去处理,onStartNestedScroll就代表了子view是否愿意接收嵌套滑动事件。

   public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        //遍历子节点
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                        nestedScrollAxes);
                handled |= accepted;

                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

     void acceptNestedScroll(boolean accept) {
            mDidAcceptNestedScroll = accept;
        }

而看看本文的3个子view的behavior,AppBarLayout.ScrollingViewBehavior和MyBehavior的onStartNestedScroll都是返回默认值false的,只有AppBarLayout.Behavior有可能返回true。看下边代码

     @Override
        public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
                View directTargetChild, View target, int nestedScrollAxes) {
            // Return true if we're nested scrolling vertically, and we have scrollable children
            // and the scrolling view is big enough to scroll
            final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
                    && child.hasScrollableChildren()
                    && parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
                ...
            return started;
        }

要想返回true,得满足3个条件,首先是纵向滑动,我们的NestedScrollView是纵向滑动的,满足。然后AppBarLayout有可滑动的子view,这里Toolbar内写有app:layout_scrollFlags=”scroll|..”,所以的确存在可滑动的子view,满足。
再看第三点CoordinatorLayout.getHeight() - NestedScrollView.getHeight() <= AppBarLayout.getHeight()

我们来看看这些高度分别是什么
这里我们曾经详细分析过CoordinatorLayout的measure和layout过程,
假设屏幕高度为H,navigatorbar高度为N,toolbar高度为T,状态栏高度为S,那么CoordinatorLayout的高度为H-N,AppBarLayout高度为T,NestedScrollView的高度是H-S-N(插一句如果toolbar不可滚,那么NestedScrollView的高度为H-S-N-T,原因可以参考HeaderScrollingViewBehavior#onMeasureChild),所以上边那个不等式就相当于T>=S,一般来说toolbar高度肯定大于statubar高度,所以条件3满足。

三个条件满足,返回true,建立嵌套滑动链,AppBarLayout的LayoutParams的mDidAcceptNestedScroll置为true。

开始上滑

上滑一开始是NestedScrollView的 dispatchNestedPreScroll,会发给上层CoordinatorLayout的onNestedPreScroll 代码如下,然后CoordinatorLayout发给子view。只有lp.mDidAcceptNestedScroll为true的才有资格接收嵌套事件,这里就是发给AppBarLayout.Behavior的onNestedPreScroll。

    //CoordinatorLayout
  public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            //等同于!lp.mDidAcceptNestedScroll
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }

            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);

                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);

                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }

        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

        if (accepted) {
            dispatchOnDependentViewChanged(true);
        }
    }

AppBarLayout.Behavior的onNestedPreScroll代码如下所示,上滑的时候走的是L13,获取min=-AppBarLayout.getUpNestedPreScrollRange(),getUpNestedPreScrollRange就是调用getTotalScrollRange,所以min是获取可滑动部分的高度的相反数(getTotalScrollRange的结果是toolbar的高度T)。
然后调用scroll进行滑动(其实就是调用offsetTopAndBottom),注意这里还有个返回值,返回的值就表示实际滑动了多少,因为有可能越界了,consumed[1]代表的是真正滑动的距离,会返回给NestedScrollView。如果没有完全消费掉这个move事件,那么NestedScrollView还会滑动。
再看传入scroll的min和max,这是范围约束,min为-T,max为0.

        //AppBarLayout.Behavior
       @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dx, int dy, int[] consumed) {
            if (dy != 0 && !mSkipNestedPreScroll) {
                int min, max;
                if (dy < 0) {
                    // We're scrolling down
                    min = -child.getTotalScrollRange();
                    max = min + child.getDownNestedPreScrollRange();
                } else {
                    // We're scrolling up
                    min = -child.getUpNestedPreScrollRange();
                    max = 0;
                }
                consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
            }
        }
   private int getUpNestedPreScrollRange() {
        return getTotalScrollRange();
    }

下滑源码分析

下滑的逻辑跟上滑类似,但有所不同。嵌套滑动初始化,建立嵌套滑动链表是一样的,重点看看下滑的代码。看下滑的代码,关注L7,L8.min为-T,getDownNestedPreScrollRange得到结果为T(why?),所以max为0。offset约束范围是 [-T,0]

   @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dx, int dy, int[] consumed) {
            if (dy != 0 && !mSkipNestedPreScroll) {
                int min, max;
                if (dy < 0) {
                    // We're scrolling down
                    min = -child.getTotalScrollRange();
                    max = min + child.getDownNestedPreScrollRange();
                } else {
                    // We're scrolling up
                    min = -child.getUpNestedPreScrollRange();
                    max = 0;
                }
                consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
            }
        }

再仔细看看getDownNestedPreScrollRange,为何返回T呢?

  private int getDownNestedPreScrollRange() {
        if (mDownPreScrollRange != INVALID_SCROLL_RANGE) {
            // If we already have a valid value, return it
            return mDownPreScrollRange;
        }

        int range = 0;
        for (int i = getChildCount() - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final int childHeight = child.getMeasuredHeight();
            final int flags = lp.mScrollFlags;

            if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
                // First take the margin into account
                range += lp.topMargin + lp.bottomMargin;
                // The view has the quick return flag combination...
                if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
                    // If they're set to enter collapsed, use the minimum height
                    range += ViewCompat.getMinimumHeight(child);
                } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
                    // Only enter by the amount of the collapsed height
                    range += childHeight - ViewCompat.getMinimumHeight(child);
                } else {
                    // Else use the full height
                    range += childHeight;
                }
            } else if (range > 0) {
                // If we've hit an non-quick return scrollable view, and we've already hit a
                // quick return view, return now
                break;
            }
        }
        return mDownPreScrollRange = Math.max(0, range - getTopInset());
    }

可以看出上述代码是,查找满足FLAG_QUICK_RETURN的view,把他的高度统计下来,FLAG_QUICK_RETURN是什么,看下边就是SCROLL_FLAG_SCROLL和SCROLL_FLAG_ENTER_ALWAYS,我们的toolbar满足条件,所以返回T
(如果for循环内存在多个满足FLAG_QUICK_RETURN的view,那第二个的margin会被计算进去,但是本身高度不会算到range里面)

static final int FLAG_QUICK_RETURN = SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS;

去掉enterAlways

此时,上滑的时候,先滑AppBarLayout,然后滑NestedScrollView,下滑的时候先滑NestedScrollView后滑AppBarLayout。
为何会这样呢?首先安装嵌套滑动的逻辑其实是NestedScrollView和CoordinatorLayout为嵌套父子view,此处CoordinatorLayout把滑动都交给了AppBarLayout。所以可以简单认为AppBarLayout和NestedScrollView为嵌套父子view,那无论上滑下滑的时候,都是在pre的时候由AppBarLayout滑动(step1),然后NestedScrollView自己滑(step2),然后再交由AppBarLayout滑动(step3)。怎么分配step1,step2,step3,AppBarlayout里有几个变量mTotalScrollRange,mDownPreScrollRange,mDownScrollRange就发挥作用了,下滑的时候,先执行step1,step1可以滑多远?mDownPreScrollRange。然后执行step2,step3,step3可以滑多远看mDownScrollRange。而上滑的时候,只有step1,step2,并没有step3(可以看AppBarLayout.Behavior#onNestedScroll内没有对上滑的处理)。

去掉enterAlways为何下滑的逻辑会变呢?
因为mDownPreScrollRange变了,getDownNestedPreScrollRange里面此时就不会把toolbar高度算进去,结果就是0.

   @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dx, int dy, int[] consumed) {
            if (dy != 0 && !mSkipNestedPreScroll) {
                int min, max;
                if (dy < 0) {
                    // We're scrolling down
                    min = -child.getTotalScrollRange();
                    max = min + child.getDownNestedPreScrollRange();
                } else {
                    // We're scrolling up
                    min = -child.getUpNestedPreScrollRange();
                    max = 0;
                }
                consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
            }
        }

下滑走L7,此时
minOffset==maxOffset,所以其实就禁止了preScroll,enterAlways阻止了下拉的时候的preScroll。

那为何NestedScrollView滑完后,会滑动AppBarLayout呢?
此时子view滑完,触发父view CoordinatorLayout的onNestedScroll,CoordinatorLayout把这个交给AppBarLayout的behavior,此时是下滑,所以dyUnconsumed<0,走到L8,开始滑动。

  @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
                View target, int dxConsumed, int dyConsumed,
                int dxUnconsumed, int dyUnconsumed) {
            if (dyUnconsumed < 0) {
                // If the scrolling view is scrolling down but not consuming, it's probably be at
                // the top of it's content
                scroll(coordinatorLayout, child, dyUnconsumed,
                        -child.getDownNestedScrollRange(), 0);
                // Set the expanding flag so that onNestedPreScroll doesn't handle any events
                mSkipNestedPreScroll = true;
            } else {
                // As we're no longer handling nested scrolls, reset the skip flag
                mSkipNestedPreScroll = false;
            }
        }

可以看出无enteralways的时候下滑有何不同,AppBarLayout滑动在NestedScrollView之后(onNestedScroll),而有enteralways的时候,AppBarLayout滑动在NestedScrollView之前(onNestedPreScroll)。这个时候可以简单的把AppBarLayout和NestedScrollView看做嵌套滑动里的父子,子view滑前会问父view是否滑(onNestedPreScroll),子view滑后,若未消耗完距离,会再问一遍父view是否滑(onNestedScroll)。这里的逻辑是一样的,AppBarLayout可以在NestedScrollView滑前滑,也可以在NestedScrollView滑完之后滑,还可以在NestedScrollView之前滑一部分,之后再滑剩下的。
而AppBarLayout内有三个变量来记录相关数据,mTotalScrollRange,mDownPreScrollRange,mDownScrollRange。

mDownPreScrollRange代表子view滑之前,我可滑多少
mDownScrollRange代表子view滑之后,我可以滑多少
这里 有enterAlways时候, mTotalScrollRange= mDownPreScrollRange=T
无enterAlways时候, mTotalScrollRange= mDownScrollRange =T

总结

1、CoordinatorLayout可以作为嵌套滑动的父view,但他和一般的父view不一样(一般的父view是传给父父view处理)他收到嵌套滑动事件之后,会发给子view处理。
2、上滑过程肯定是AppBarlayout优先滑
3、下滑过程为AppBarlayout先滑(step1),NestedScrollView再滑,AppBarlayout再滑,这个过程由mDownPreScrollRange和mDownScrollRange控制,受enterAlways影响

参考资料

http://android-developers.blogspot.com/2015/05/android-design-support-library.html
http://stackoverflow.com/questions/33984944/toolbar-overlaps-status-bar
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0717/3196.html
http://blog.csdn.net/qibin0506/article/details/50290421#reply
https://github.com/JakeWharton/DrawerBehavior
http://blog.csdn.net/eclipsexys/article/details/46349721
https://github.com/chrisbanes/cheesesquare (谷歌官方)

https://github.com/rufflez/SupportDesignLibrarySample
https://www.youtube.com/watch?v=Kz_s6DFjTcw
http://www.jianshu.com/p/7caa5f4f49bd
http://www.jianshu.com/p/360fd368936d

作者:litefish 发表于2016/9/19 20:37:38 原文链接
阅读:141 评论:0 查看评论

6AppBarLayout与scrollFlags

$
0
0

6AppBarLayout与scrollFlags

AppBarLayout分组

这里说过AppBarLayout可以分为可滑出和不可滑出上下2部分,其实细致一点可以分三部分,如下图所示,下滑最后出现(part 1),下滑立刻出现(part2),无法滑出(part3),其中part1和2合起来就是可以滑出的部分。

xml代码如下

   <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:theme="@style/AppTheme.AppBarOverlay"
        android:layout_height="wrap_content">


        <TextView
            android:gravity="center"
            app:layout_scrollFlags="scroll"
            android:textSize="20sp"
            android:text="下滑最后出现"
            android:background="#447700"
            android:layout_width="match_parent"
            android:layout_height="70dp" />


        <TextView
            android:gravity="center"
            app:layout_scrollFlags="scroll|enterAlways"
            android:textSize="20sp"
            android:text="下滑立刻出现"
            android:background="#004477"
            android:layout_width="match_parent"
            android:layout_height="100dp" />


        <TextView
            android:gravity="center"
            android:textSize="20sp"
            android:text="无法滑出去"
            android:background="#ff0000"
            android:layout_width="match_parent"
            android:layout_height="100dp" />

    </android.support.design.widget.AppBarLayout>

主要关注layout_scrollFlags,可以看到part3无scroll标志,代表无法滚出;part2是scroll|enterAlways代表下滑立刻出现;part1是scroll下滑的时候最后出现。
为什么会这样,主要和mDownPreScrollRange、mDownScrollRange有关,可以看下边代码。mDownPreScrollRange控制着嵌套滑动的父view的onNestedPreScroll部分可滑距离,mDownScrollRange控制着嵌套滑动的父view的onNestedScroll部分。

//AppBarLayout
    /**
     * Return the scroll range when scrolling down from a nested pre-scroll.
     */
    private int getDownNestedPreScrollRange() {
        if (mDownPreScrollRange != INVALID_SCROLL_RANGE) {
            // If we already have a valid value, return it
            return mDownPreScrollRange;
        }

        int range = 0;
        for (int i = getChildCount() - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final int childHeight = child.getMeasuredHeight();
            final int flags = lp.mScrollFlags;

            if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
                // First take the margin into account
                range += lp.topMargin + lp.bottomMargin;
                // The view has the quick return flag combination...
                if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
                    // If they're set to enter collapsed, use the minimum height
                    range += ViewCompat.getMinimumHeight(child);
                } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
                    // Only enter by the amount of the collapsed height
                    range += childHeight - ViewCompat.getMinimumHeight(child);
                } else {
                    // Else use the full height
                    range += childHeight;
                }
            } else if (range > 0) {
                // If we've hit an non-quick return scrollable view, and we've already hit a
                // quick return view, return now
                break;
            }
        }
        return mDownPreScrollRange = Math.max(0, range - getTopInset());
    }

    /**
     * Return the scroll range when scrolling down from a nested scroll.
     */
    private int getDownNestedScrollRange() {
        if (mDownScrollRange != INVALID_SCROLL_RANGE) {
            // If we already have a valid value, return it
            return mDownScrollRange;
        }

        int range = 0;
        for (int i = 0, z = getChildCount(); i < z; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            int childHeight = child.getMeasuredHeight();
            childHeight += lp.topMargin + lp.bottomMargin;

            final int flags = lp.mScrollFlags;

            if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
                // We're set to scroll so add the child's height
                range += childHeight;

                if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
                    // For a collapsing exit scroll, we to take the collapsed height into account.
                    // We also break the range straight away since later views can't scroll
                    // beneath us
                    range -= ViewCompat.getMinimumHeight(child) + getTopInset();
                    break;
                }
            } else {
                // As soon as a view doesn't have the scroll flag, we end the range calculation.
                // This is because views below can not scroll under a fixed view.
                break;
            }
        }
        return mDownScrollRange = Math.max(0, range);
    }

实际效果如下所示

scrollFlags

-scroll代表可滚动,被标注后算到mTotalScrollRange里,要写其他flag必须先写scroll才有效
-enterAlways下滑,这个view立刻跑出来,算在mDownPreScrollRange内
-enterAlwaysCollapsed下滑的时候在onNestedPreScroll阶段先滑出一个最小高度,这个参数我试了下都存在一定问题,没找到一个合适的场景。用enterAlwaysCollapsed必须先写 scroll和enterAlways
-exitUntilCollapsed 向上滚动直到折叠,往往用于CollapsingToolbarLayout内,后边会有介绍

作者:litefish 发表于2016/9/19 20:38:13 原文链接
阅读:142 评论:0 查看评论

8CollapsingToolbarLayout源码分析

$
0
0

8CollapsingToolbarLayout源码分析

纯色Toolbar滑动

最简单代码

先从最简单的看起

   <android.support.design.widget.AppBarLayout
        android:fitsSystemWindows="true"
        android:layout_width="match_parent"
        android:layout_height="256dp">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                android:minHeight="?attr/actionBarSize"
                app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

效果如下所示,toolbar可以伸展

AppBarLayout里有个接口,叫做OnOffsetChangedListener,如果AppBarLayout滑动了就会触发里面的回调onOffsetChanged

    /**
     * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical
     * offset changes.
     */
    public interface OnOffsetChangedListener {
        /**
         * Called when the {@link AppBarLayout}'s layout offset has been changed. This allows
         * child views to implement custom behavior based on the offset (for instance pinning a
         * view at a certain y value).
         *
         * @param appBarLayout the {@link AppBarLayout} which offset has changed
         * @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px
         */
        void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
    }

AppBarLayout滑动的时候会调用setHeaderTopBottomOffset,里面调用dispatchOffsetUpdates(appBarLayout),如下所示,会把移动的消息发给listeners

      private void dispatchOffsetUpdates(AppBarLayout layout) {
            final List<OnOffsetChangedListener> listeners = layout.mListeners;

            // Iterate backwards through the list so that most recently added listeners
            // get the first chance to decide
            for (int i = 0, z = listeners.size(); i < z; i++) {
                final OnOffsetChangedListener listener = listeners.get(i);
                if (listener != null) {
                    listener.onOffsetChanged(layout, getTopAndBottomOffset());
                }
            }
        }

而CollapsingToolbarLayout在onAttachedToWindow的时候加入
((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
其实就是注册了一个listener,AppBarLayout滑动了,CollapsingToolbarLayout 内的mOnOffsetChangedListener就会知道并作出相应动画,这里其实就是文字的缩小。主要代码在CollapsingTextHelper内,主要就是根据当前AppBarLayout的offset来修改mScale。

此时CollapsingToolbarLayout和AppBarLayout一样大小,包含statusbar 大小为256dp
mTotalScrollRange=range - getTopInset()=256dp-S=609
mDownPreScrollRange 0
mDownScrollRange =256dp=672
我曾经以为mTotalScrollRange= mDownPreScrollRange+ mDownScrollRange,这里不成立了。
我试着把mDownScrollRange强行改为609,滑动依然正常,因为下滑的时候offset是在变大的,所以不会到-672.

exitUntilCollapsed

再看设置了exitUntilCollapsed 之后,exitUntilCollapsed意思就是滑出直到折叠状态,即滑出的时候最多到折叠状态,无法完全滑出

exitUntilCollapsed会改变上滑的范围,上滑的范围就是mTotalScrollRange,

    private int getUpNestedPreScrollRange() {
        return getTotalScrollRange();
    }
   public final int getTotalScrollRange() {
        if (mTotalScrollRange != INVALID_SCROLL_RANGE) {
            return mTotalScrollRange;
        }

        int range = 0;
        for (int i = 0, z = getChildCount(); i < z; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final int childHeight = child.getMeasuredHeight();
            final int flags = lp.mScrollFlags;

            if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
                // We're set to scroll so add the child's height
                range += childHeight + lp.topMargin + lp.bottomMargin;

                if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
                    // For a collapsing scroll, we to take the collapsed height into account.
                    // We also break straight away since later views can't scroll beneath
                    // us
                    //减去标记了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED的child的最小高度
                    range -= ViewCompat.getMinimumHeight(child);
                    break;
                }
            } else {
                // As soon as a view doesn't have the scroll flag, we end the range calculation.
                // This is because views below can not scroll under a fixed view.
                break;
            }
        }
        return mTotalScrollRange = Math.max(0, range - getTopInset());
    }

由上可知,在算mTotalScrollRange的时候会减去标记了SCROLL_FLAG_EXIT_UNTIL_COLLAPSED的child的最小高度,这里就是减去CollapsingToolbarLayout的minHeight,但是又有个问题,CollapsingToolbarLayout我们并没有设置minHeight,我们只是在Toolbar里设置了minHeight。CollapsingToolbarLayout在onLayout的时候会调用setMinimumHeight(getHeightWithMargins(mToolbar));,这样CollapsingToolbarLayout就有了minHeight,这个值是toolbar的height加上下margin,跟Toolbar的minHeight没关系。试试看把Toolbar的minHeight去掉,毫不影响。所以此时mTotalScrollRange会减去CollapsingToolbarLayout的minHeight,这样上滑的时候就会留出一部分高度,不全部滑出,留出的高度就是CollapsingToolbarLayout的minHeight=toolbar高度+上下margin

定住toolbar

Toolbar设置app:layout_collapseMode=”pin”

这居然可以定住toolbar,和appbarlayout的设计又有点不符合,appbarlayout是认为底部可以存在不滑动的区域,但顶部不可以,那这里怎么做到的,实际上,他是随着appbarlayout往上offset了,然后他自己之后又offset了一次,使得toolbar相对屏幕的位置不变。实际上,假设appbarlayout往上滑了11,那么appbarlayout的offset是-11,此时我们又offset了一次,把toolbar相对CollapsingToolbarLayout的offset设置为11,这样toolbar相对屏幕就相当于没变化,核心代码在android.support.design.widget.CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged

//CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged
            for (int i = 0, z = getChildCount(); i < z; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);

                switch (lp.mCollapseMode) {
                    case LayoutParams.COLLAPSE_MODE_PIN:
                    //调整offset使得child看起来不动
                        if (getHeight() - insetTop + verticalOffset >= child.getHeight()) {
                            offsetHelper.setTopAndBottomOffset(-verticalOffset);
                        }
                        break;
                    case LayoutParams.COLLAPSE_MODE_PARALLAX:
                    //调整offset实现视差滑动
                        offsetHelper.setTopAndBottomOffset(
                                Math.round(-verticalOffset * lp.mParallaxMult));
                        break;
                }
            }

带背景图toolbar

对应case1

上滑的过程中,背景由图片变成纯色,状态栏也由透明变为纯色,这个变化是什么时候呢?这个临界点由getScrimTriggerOffset决定

        //CollapsingToolbarLayout.OffsetUpdateListener#onOffsetChanged
      // Show or hide the scrims if needed
            if (mContentScrim != null || mStatusBarScrim != null) {
                setScrimsShown(getHeight() + verticalOffset < getScrimTriggerOffset() + insetTop);
            }

    /**
     * The additional offset used to define when to trigger the scrim visibility change.
     */
    final int getScrimTriggerOffset() {
        return 2 * ViewCompat.getMinimumHeight(this);
    }

截了个图,大概是这个位置,图片可见部分的高度就是getScrimTriggerOffset的值,下一瞬间图片就会变成纯色。实际上就是在上面盖了个mContentScrim,mContentScrim就是一个ColorDrawable ,颜色为colorPrimary.由此可见修改CollapsingToolbarLayout的minHeight就可以修改变化瞬间的位置

变成纯色的同时,状态栏也从透明变为有颜色colorPrimaryDark。mScrimAlpha由1变为255,状态栏变为纯色,实际上是在状态栏的位置画了一个纯色的矩形,由mStatusBarScrim来实现,mStatusBarScrim的颜色也可以指定。

       if (mStatusBarScrim != null && mScrimAlpha > 0) {
            final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
            if (topInset > 0) {
                mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(),
                        topInset - mCurrentOffset);
                mStatusBarScrim.mutate().setAlpha(mScrimAlpha);
                mStatusBarScrim.draw(canvas);
            }
        }

初始态覆盖状态栏

对应case3
给ImageView加上fitSystemWindow,为什么就有效果,让初始态覆盖状态栏
不加的话,ImageView会被设置一个offset(insetTop),让他处于状态栏下边,如果加了,那就进不到L7,所以可以覆盖状态栏。

//android.support.design.widget.CollapsingToolbarLayout#onLayout
         if (mLastInsets != null && !ViewCompat.getFitsSystemWindows(child)) {
                final int insetTop = mLastInsets.getSystemWindowInsetTop();
                if (child.getTop() < insetTop) {
                    // If the child isn't set to fit system windows but is drawing within the inset
                    // offset it down
                    ViewCompat.offsetTopAndBottom(child, insetTop);
                }
            }

enterAlwaysCollapsed

再来看看enterAlwaysCollapsed有什么用
我拿CollapsImageActivity3试了一下,app:layout_scrollFlags=”scroll|enterAlways|enterAlwaysCollapsed” 发现有bug,下滑pre的时候显示如下,应该是下滑的范围(mDownPreScrollRange)少算了个statubar。暂时没有什么好的解决方案,看google后期会不会修复这个bug还是放弃enterAlwaysCollapsed。

作者:litefish 发表于2016/9/19 20:44:34 原文链接
阅读:147 评论:0 查看评论

7CollapsingToolbarLayout

$
0
0

7CollapsingToolbarLayout

CollapsingToolbarLayout是Toolbar的一个包装,可以做出很多很炫的折叠效果。

toolbar伸缩

toolbar伸展开加入图片背景,收缩时变会普通toolbar

Toolbar伸展

先从最简单的看起

   <android.support.design.widget.AppBarLayout
        android:fitsSystemWindows="true"
        android:layout_width="match_parent"
        android:layout_height="256dp">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_scrollFlags="scroll">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                android:minHeight="?attr/actionBarSize"
                app:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
                app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />

        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

效果如下所示,toolbar可以伸展

如果想要最后的时候toolbar也留着,只要在app:layout_scrollFlags里加一个exitUntilCollapsed,效果如下,exitUntilCollapsed意思就是滑出直到折叠状态,即滑出的时候最多到折叠状态,无法完全滑出

此时有个问题,右上角菜单不见了,实际上是滑出去了,我们要想保留,就得让toolbar不滑动,pin住就行了(Toolbar设置app:layout_collapseMode=”pin”),让toolbar不滑动。效果如下,此时效果已经不错了,上滑过程中,标题逐渐变小,然后跑到toolbar里去.此时如果要修改伸展状态下text的位置可以用app:expandedTitleMargin, app:expandedTitleMarginBottom, app:expandedTitleMarginEnd and app:expandedTitleMarginStart。

Toolbar背景图

case1

如何为Toolbar添加个背景图呢?
因为CollapsingToolbarLayout是FrameLayout所以,直接在里面加ImageView就可以了(加backgroud行不行?)
注意此时把toolbar背景去掉,删掉这句android:background=”?attr/colorPrimary”
然后CollapsingToolbarLayout内加入加入app:contentScrim=”?attr/colorPrimary”
这么做,伸展开的时候背景就是图片,收缩的时候背景就是纯色

case2

此时发现图片不会滑到状态栏上,那是因为状态栏没有设置透明,给activity换个style用AppTheme.NoActionBar之后,就可以滑到状态栏了。效果如下

case3

此时,初始态未覆盖状态栏,滑动的时候可以覆盖到状态栏,想要初始态就覆盖状态栏,怎么办?
给ImageView加上fitSystemWindow,效果如下

case4

app:layout_collapseMode=”parallax” 可以让图片和AppBarLayout一起滑动

最后代码如下

“` xml

enterAlways

enterAlways: 一旦下滑就会让view逐步显示出来. 当我们滑到一个list的底部的时候,想要下滑尽快看到toolbar的时候,用这个模式很有用。

一般情况下,我们想要toolbar显示出来,得先滑到list的顶部才行,如下所示

如果设置了enterAlways,就会如下所示,可以看到toolbar立刻就出来了

enterAlwaysCollapsed

但是如果使用enterAlwaysCollapsed,注意此时的layout_scrollFlags必须写了scroll、enterAlways、enterAlwaysCollapsed,
这样可以实现,一旦下滑就把我们的view拉出minimum height,然后list到顶的时候,可以把整个view拉出来。如下所示。(我试了下没做出这样的效果)

snap

snap的作用就是让CollapsingToolbarLayout要么完全展开,要么完全收缩,根据手指抬起的时候,滚动的距离有没有一半来判断

参考资料

http://blog.csdn.net/u010687392/article/details/46906657
https://guides.codepath.com/android/Handling-Scrolls-with-CoordinatorLayout
https://developer.android.com/reference/android/support/design/widget/CollapsingToolbarLayout.html

作者:litefish 发表于2016/9/19 20:45:24 原文链接
阅读:144 评论:0 查看评论

实现iOS图片等资源文件的热更新化(一): 从Images.xcassets导出合适的图片

$
0
0

本文会基于一个已有的脚本工具自动导出所有的图片;最终给出的是一个从 Images.xcassets 到基于文件夹的精简 合适 的图片资源集的完整过程.难点在于从完整图片集到精简图片集,肯定是基于一个定制化的脚本,自定义导出的.如果自己手动导出?那可有的忙喽~

Images.xcassets 与 Assets.car

Images.xcassets,是Xcode项目中的,用于存放资源文件.那么我们为什么不直接处理 Images.xcassets 呢?因为Images.xcassets中存放的图片名称可能与图片的资源名称不一致,最终决定图片资源名的是资源文件夹的名称;也有可能Images.xcassets存放的是pdf格式的图片,这样可以自动预编译对应尺寸的图片资源.

Images.xcassets 编译后,最终ipa包中,是以Assets.car包的形式出现的,内部是处理后的图片名.此处的文件名与我们代码中引用的图片资源名称是一致的.

也就是说: 直接基于Assets.car进行处理,可以使我们的使用图片处的代码变更尽可能少.

使用 cartool 从 Assets.car 导出图片

Assets.car 无法直接zip解压,需要借助专门的工具,此处推荐: cartool 使用方法,参见: iOS学习之解压Assets.car

如果你缺少足够复杂的Assets.car或者cartool用法有问题,可以直接使用我处理过的资源:https://github.com/ios122/ios_assets_hot_update/tree/master/res

针对文章github给定的目录, cartool的用法,可以简述为:
cd 到 res目录,然后

mkdir Assets
./cartool  ./Assets.car ./Assets

其实使用一张图片就可以额兼容iPhone/iPad

从 Assets.car 导出后的图片,大致有以下几种:

  • 只存在@1x的图: 如 2.png
  • 只存在@1x和@2x的图: 如 account.png 和 account@2x.png
  • 只存在@2x的图: 如add-1@2x.png
  • 只存在@2x与@3x的图片: 如 10@2x.png 和 10@3x.png
  • 同时存在三种尺寸的图片: 如 1.png 1@2x.png 和 1@3x.png
  • 区分iphone与ipad的图片,此类图一般由pdf自动在预编译时生成: 如bg_mypage_edit~ipad.png bg_mypage_edit~ipad@2x.png bg_mypage_edit~ipad@3x.png bg_mypage_edit~iphone.png bg_mypage_edit~iphone@2x.png bg_mypage_edit~iphone@3x.png
  • 汉语命名的图片: 如 提醒.png

以上图片的原因,很大一部分是由于App迭代引起的.对于一个图片,存在上述不同情况时,图片通常加载与当前屏幕比例(scale)最符合的图片,具体细节下一篇文章会更完整描述.

经过我自己的实验与网上各种资料的查询,使用 @3x 的图片是可以同时作为 iPhone和iPad的通用图标的.当然,这是需要自定义 imageNamed方法,也是下一篇文章的重点. 2套共5个图片,现在只需要1个图片,理论图片资源体积可以减小
((1 + 2 + 3 + 3 + 1.5) - 3) / (1 + 2 + 3 + 3 + 1.5) = 71.428571 % (信息量超大的速算法,看不懂就当是个冷笑话吧~(≧▽≦)/~)

自动归类脚本思路

我们想要获取的是 可用的@3x图片文件夹不包含@3x图片的有问题的资源列表. 对于不存在@3x副本的图片,很大可能这个资源已经被废弃了.这一块,暂定手动去排查与核实.如果一个图片仍在使用但是不存在@3x的副本,绝对是RD挖了一个坑,等你来填!

基本思路是:

  1. 去除 ~ipad 结尾的图片,如bg_mypage_edit~ipad.png;
  2. 去除 ~iphone 图片中的 ~iphone文字,如bg_mypage_edit~iphone@3x.png 重命名为 bg_mypage_edit@3x.png;
  3. 将含有@3x的图片组的@1x @2x @3x 的图片按顺序移动到单独文件夹 如 assets_3x,并都命名为@3x,此时原文件夹中即为有问题的资源,新文件夹中为有效的资源文件,且只保留了@3x;
  4. 将原资源文件夹命名为assets_error,以供以后使用;
  5. 人工确认非法图片是否具有存在意义,存在则寻找其@3x副本放到 assets_3x 文件夹;

自动归类脚本实现

除了以上的第五步以外,前四步都可以自动化运行:

#0. 需要先cd到解压后的Assets目录;
#1. 去除 ~ipad 结尾的图片,如bg_mypage_edit~ipad.png;
find . -iname "*~ipad*.png" -delete

#2. 去除 ~iphone 图片中的 ~iphone文字;
find . -name "*~iphone.png" -exec sh -c 'for i do mv -- "$i" "${i%~iphone.png}.png"; done' sh {} +

find . -name "*~iphone@2x.png" -exec sh -c 'for i do mv -- "$i" "${i%~iphone@2x.png}@2x.png"; done' sh {} +

find . -name "*~iphone@3x.png" -exec sh -c 'for i do mv -- "$i" "${i%~iphone@3x.png}@3x.png"; done' sh {} +

# 3.将含有@3x的图片组的@1x @2x @3x 的图片按顺序移动到单独文件夹 如 assets_3x,并都命名为@3x,此时原文件夹中即为有问题的资源,新文件夹中为有效的资源文件,且只保留了@3x;

mkdir ../assets_3x

find . -name "*@3x.png" -exec sh -c 'for i do mv -- "${i%@3x.png}.png" "../assets_3x/${i%@3x.png}@3x.png"; mv -- "${i%@3x.png}@2x.png" "../assets_3x/${i%@3x.png}@3x.png";mv -- "${i%@3x.png}@3x.png" "../assets_3x/${i%@3x.png}@3x.png";done' sh {} +

# 4.将原资源文件夹命名为assets_error,以供以后使用;
cd ..
mv Assets assets_error

最终得到的 assets_3x 即为可用资源,assets_error 即为需要手动确认可用性的资源.

收获与感悟:

  1. 项目中,图片这一块,的确有许多无用的或不合理的资源,需要及早解决;
  2. shell 脚本是基于路径进行复制,移动等操作的,如 find的结果,其实是一个文件路径,借助它,提出了一个简单的区分可用于不可用资源的方法;
  3. 写博客,确实可以使思路更清晰有序,坦白讲,这本来是一个我不敢碰的优化任务,一个一个比对,想想都头大.最终的处理结果,还是给出了一定数量的无用图片,但是我根据其名字就可以确定其位置,非常好处理了,已经省了不少功夫了;而且,要比我手动排查地可信多了.

系列专属github地址: https://github.com/ios122/ios_assets_hot_update

作者:sinat_30800357 发表于2016/9/19 21:16:11 原文链接
阅读:173 评论:0 查看评论

《React-Native系列》32、 基于Fetch封装HTTPUtil工具类

$
0
0

关于http请求的工具类,有很多,譬如:httpclient,okhttp。

那么关于RN的处理HTTP请求的工具类呢,目前还没有找到,所以自己简单封装了一个,避免代码里到处都是fetch方法。

好了,完整代码如下:

var HTTPUtil = {};

/**
 * 基于 fetch 封装的 GET请求
 * @param url
 * @param params {}
 * @param headers
 * @returns {Promise}
 */
HTTPUtil.get = function(url, params, headers) {
    if (params) {
        let paramsArray = [];
        //encodeURIComponent
        Object.keys(params).forEach(key => paramsArray.push(key + '=' + params[key]))
        if (url.search(/\?/) === -1) {
            url += '?' + paramsArray.join('&')
        } else {
            url += '&' + paramsArray.join('&')
        }
    }
    return new Promise(function (resolve, reject) {
      fetch(url, {
            method: 'GET',
            headers: headers,
          })
          .then((response) => {
              if (response.ok) {
                  return response.json();
              } else {
                  reject({status:response.status})
              }
          })
          .then((response) => {
              resolve(response);
          })
          .catch((err)=> {
            reject({status:-1});
          })
    })
}


/**
 * 基于 fetch 封装的 POST请求  FormData 表单数据
 * @param url
 * @param formData  
 * @param headers
 * @returns {Promise}
 */
HTTPUtil.post = function(url, formData, headers) {
    return new Promise(function (resolve, reject) {
      fetch(url, {
            method: 'POST',
            headers: headers,
            body:formData,
          })
          .then((response) => {
              if (response.ok) {
                  return response.json();
              } else {
                  reject({status:response.status})
              }
          })
          .then((response) => {
              resolve(response);
          })
          .catch((err)=> {
            reject({status:-1});
          })
    })
}

export default HTTPUtil;

怎么使用呢,举个简单的例子吧:

let formData = new FormData();
formData.append("id",1060);
      
HTTPUtil.post(url,formData,headers).then((json) => {
	//处理 请求success
   	if(json.code === 0 ){
            //我们假设业务定义code为0时,数据正常
        }else{
             //处理自定义异常
            this.doException(json);
        }
   },(json)=>{
     //TODO 处理请求fail
      
})



作者:hsbirenjie 发表于2016/9/19 21:41:15 原文链接
阅读:150 评论:0 查看评论

UGUI内核大探究(十八)Raycaster

$
0
0

射线其实是属于事件系统,它在EventSystem/Raycasters目录下,有BaseRaycaster、PhysicsRaycaster和Physics2DRaycaster三个类,命名空间也是UnityEngine.EventSystems。但是UI/Core目录下还有一个GraphicRaycaster文件,命名空间却是UnityEngine.UI。当我们在编辑器里新建(或间接新建)一个Canvas时,会为Canvas添加GraphicRaycaster组件,而PhysicsRaycaster和Physics2DRaycaster似乎在UGUI里没有被添加过。当然,我们在编辑器里可以添加这两个组件。本文就讨论一下这些射线照射器的原理。

按照惯例,附上UGUI源码下载地址

BaseRaycaster是其他Raycaster的基类,是一个抽象类。在它OnEnable(调用时机参考Untiy3D组件小贴士(一)OnEnabled与OnDisabled)里将自己注册到RaycasterManager,并在OnDisable的时候从后者移除。

RaycasterManager是一个静态类,维护了一个BaseRaycaster类型的List。EventSystem(参考UGUI内核大探究(一)EventSystem)里也通过这个类来管理所有的射线照射器。

PhysicsRaycaster(物理射线照射器)添加了特性

[RequireComponent(typeof(Camera))]

说明它依赖于Camera组件。它通过eventCamera属性来获取对象上的Camera组件。

Raycast方法重写了BaseRaycaster的同名抽象方法:

        public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
            if (eventCamera == null)
                return;

            var ray = eventCamera.ScreenPointToRay(eventData.position);
            float dist = eventCamera.farClipPlane - eventCamera.nearClipPlane;

            var hits = Physics.RaycastAll(ray, dist, finalEventMask);

            if (hits.Length > 1)
                System.Array.Sort(hits, (r1, r2) => r1.distance.CompareTo(r2.distance));

            if (hits.Length != 0)
            {
                for (int b = 0, bmax = hits.Length; b < bmax; ++b)
                {
                    var result = new RaycastResult
                    {
                        gameObject = hits[b].collider.gameObject,
                        module = this,
                        distance = hits[b].distance,
                        worldPosition = hits[b].point,
                        worldNormal = hits[b].normal,
                        screenPosition = eventData.position,
                        index = resultAppendList.Count,
                        sortingLayer = 0,
                        sortingOrder = 0
                    };
                    resultAppendList.Add(result);
                }
            }
        }

通过Physics.RaycastAll来获取所有被照射到的对象(finalEventMask是通过将Camera的cullingMask属性和编辑器设置中的EventMask属性做与运算获得的)。根据距离进行排序,然后包装成RaycastResult结构,加入到resultAppendList里面。EventSystem会将所有的Raycast的照射结果合在一起并排序,然后输入模块(参考UGUI内核大探究(三)输入模块)取到第一个结果的对象(距离最短)作为受输入事件影响的对象。

Physics2DRaycaster继承自PhysicsRaycaster,其他都一样,只重写了Raycast方法:

        public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
            if (eventCamera == null)
                return;

            var ray = eventCamera.ScreenPointToRay(eventData.position);

            float dist = eventCamera.farClipPlane - eventCamera.nearClipPlane;

            var hits = Physics2D.RaycastAll(ray.origin, ray.direction, dist, finalEventMask);

            if (hits.Length != 0)
            {
                for (int b = 0, bmax = hits.Length; b < bmax; ++b)
                {
                    var sr = hits[b].collider.gameObject.GetComponent<SpriteRenderer>();

                    var result = new RaycastResult
                    {
                        gameObject = hits[b].collider.gameObject,
                        module = this,
                        distance = Vector3.Distance(eventCamera.transform.position, hits[b].transform.position),
                        worldPosition = hits[b].point,
                        worldNormal = hits[b].normal,
                        screenPosition = eventData.position,
                        index = resultAppendList.Count,
                        sortingLayer =  sr != null ? sr.sortingLayerID : 0,
                        sortingOrder = sr != null ? sr.sortingOrder : 0
                    };
                    resultAppendList.Add(result);
                }
            }
        }

改为用Physics2D.RaycastAll来照射对象,并且根据SpriteRenderer组件设置结果变量(在EventSystem里会作为排序依据,毕竟是2D对象)。

GraphicRaycaster继承自BaseRaycaster,它添加了特性:

[RequireComponent(typeof(Canvas))]

表示它依赖于Canvas组件(通过canvas属性来获取)。

它重写了三个属性sortOrderPriority、renderOrderPriority(获取Canvas的sortingOrder和renderOrder,这在EventSystem里会作为排序依据,呃……毕竟是UI)和eventCamera(获取canvas.worldCamera,为null则返回Camera.main)。

Raycast方法:

        [NonSerialized] private List<Graphic> m_RaycastResults = new List<Graphic>();
        public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
            if (canvas == null)
                return;

            // Convert to view space
            Vector2 pos;
            if (eventCamera == null)
                pos = new Vector2(eventData.position.x / Screen.width, eventData.position.y / Screen.height);
            else
                pos = eventCamera.ScreenToViewportPoint(eventData.position);

            // If it's outside the camera's viewport, do nothing
            if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
                return;

            float hitDistance = float.MaxValue;

            Ray ray = new Ray();

            if (eventCamera != null)
                ray = eventCamera.ScreenPointToRay(eventData.position);

            if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
            {
                float dist = 100.0f;

                if (eventCamera != null)
                    dist = eventCamera.farClipPlane - eventCamera.nearClipPlane;

                if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)
                {
                    RaycastHit hit;
                    if (Physics.Raycast(ray, out hit, dist, m_BlockingMask))
                    {
                        hitDistance = hit.distance;
                    }
                }

                if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All)
                {
                    RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction, dist, m_BlockingMask);

                    if (hit.collider != null)
                    {
                        hitDistance = hit.fraction * dist;
                    }
                }
            }

            m_RaycastResults.Clear();
            Raycast(canvas, eventCamera, eventData.position, m_RaycastResults);

            for (var index = 0; index < m_RaycastResults.Count; index++)
            {
                var go = m_RaycastResults[index].gameObject;
                bool appendGraphic = true;

                if (ignoreReversedGraphics)
                {
                    if (eventCamera == null)
                    {
                        // If we dont have a camera we know that we should always be facing forward
                        var dir = go.transform.rotation * Vector3.forward;
                        appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
                    }
                    else
                    {
                        // If we have a camera compare the direction against the cameras forward.
                        var cameraFoward = eventCamera.transform.rotation * Vector3.forward;
                        var dir = go.transform.rotation * Vector3.forward;
                        appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
                    }
                }

                if (appendGraphic)
                {
                    float distance = 0;

                    if (eventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
                        distance = 0;
                    else
                    {
                        Transform trans = go.transform;
                        Vector3 transForward = trans.forward;
                        // http://geomalgorithms.com/a06-_intersect-2.html
                        distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));

                        // Check to see if the go is behind the camera.
                        if (distance < 0)
                            continue;
                    }

                    if (distance >= hitDistance)
                        continue;

                    var castResult = new RaycastResult
                    {
                        gameObject = go,
                        module = this,
                        distance = distance,
                        screenPosition = eventData.position,
                        index = resultAppendList.Count,
                        depth = m_RaycastResults[index].depth,
                        sortingLayer =  canvas.sortingLayerID,
                        sortingOrder = canvas.sortingOrder
                    };
                    resultAppendList.Add(castResult);
                }
            }
        }
首先将屏幕点转换为Camera的视窗坐标,用于判断是否在视窗外。然后根据blockingObjects来选择Physics或RaycastHit2D,不过这里只是用来计算距离hitDistance。然后调用静态方法Raycast获取屏幕点在其区域内的Graphic的列表m_RaycastResults(会调用Graphic的Raycast方法,参考UGUI内核大探究(七)Graphic)。接着遍历m_RaycastResults,判断Graphic的方向向量和Camera的方向向量是否相交,然后判断Graphic是否在Camera的前面,并且距离小于等于hitDistance,满足了这些条件,才会把它打包成RaycastResult添加到resultAppendList里。

由此可见GraphicRaycaster与其他射线照射器的区别就在于,它把照射对象限定为了Graphic,这也是UGUI里的常规用法。

作者:ecidevilin 发表于2016/9/19 22:02:35 原文链接
阅读:132 评论:0 查看评论

Android 5.0+ 之Notification

$
0
0
Notification在日常开发中是会经常遇到的,而在5.0之后,又发生了一些微妙的变化:在设置小icon后发现通知栏的icon并不是我们设置的icon,而是一个纯白色的图标。本文将带你介绍Notification的使用方法。

官网截图

        当新的通知被发布或删除,或它们的次序改变的时候,从系统接收呼叫的服务。大概可以这样翻译,很难理解是吧,那我们不需要按照它的意思来,我们就把它翻译为一个通知,就可以了。下拉状态栏后会显示完整的信息。当用户点击这个 notification 时,系统就会处理创建 notification 是传入的 Intent(通常是启动一个 Activity).你也可以给你的notification添加声音、震动、闪光灯功能。当后台服务需要提示用户来响应某个事件时,应该使用状态栏通知。后台服务不应该自己去启动一个 activity 来与用户互动,它应该创建一个状态栏通知,当用户选择这个通知时再去启动 activity。继承关系如图:

关系图

感兴趣的同学可以http://www.android-doc.com/reference/android/app/Notification.html自己研究。

一般Notification分为三类,普通、折叠、悬挂三种:
普通:在状态栏显示一个图标,下拉通知栏,会看到一个图标和详情,如图:

普通

折叠:在状态栏显示一个图标,并且有两种视图,一种是普通视图,另一种可以展开视图。如图:

展开时的样式
合起来的样式

悬挂:当前操作不会打断,焦点不变,不需要下拉通知栏就直接显示出来,过几秒就会消失。 只不过Activity上像鼻涕一样流下来一个悬挂在顶部的通知,我们常用的微信就包含了这种通知。如图:

这里写图片描述

按照我写的Demo,先来看下视图文件:
![这里写图片描述](http://img.blog.csdn.net/20160920112206624)
视图很简单,就三个按钮,点击会出现对应的通知,由于布局太简单了,不上代码了,可以底部下载Demo查看。
要创建一个Notification很简单,稍看一下文档就知道,用一个Builder就可以构造出一个通知,我们在onCreate之前初始化:
    private NotificationManager notificationManager;

    private Notification.Builder builder;
    private Intent mIntent;
    private PendingIntent pendingIntent;
在实现各种创建的时候,偷了一个懒:
//公共的属性都写到了这里避免代码重复
        builder = new Notification.Builder(this);//创建builder对象
        //指定点击通知后的动作,此处跳到我的博客
        mIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://blog.csdn.net/u012552275"));
        pendingIntent = PendingIntent.getActivity(this, 0, mIntent, 0);
        builder.setContentIntent(pendingIntent); //跳转
        builder.setSmallIcon(R.mipmap.ic_launcher);//小图标
        builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
        builder.setAutoCancel(true); //顾名思义,左右滑动可删除通知
        notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
之后在给builder设置各种参数,下面就是普通的通知,要调用notify。
builder.setContentTitle("普通通知");
        builder.setSubText("这是一个普通通知");
        builder.setContentText("点击上我");
        notificationManager.notify(0, builder.build());
折叠:给了一个视图,这个视图是可以自定义的,比如我们常用的酷狗的下拉菜单,里面可以有各种功能实现。
builder.setContentTitle("折叠通知(我可以被拉大哦)");
        builder.setSubText("这个折叠的通知,可以被删除");
        builder.setContentText("点击上我");
        //用RemoteViews来创建自定义Notification视图
        RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.view_fold_notification);
        Notification notification = builder.build();
        //指定展开时的视图
        notification.bigContentView = remoteViews;
        notificationManager.notify(1, notification);
悬挂:
builder.setFullScreenIntent(pendingIntent, true);


其他备注:
Notification分成了三个等级:
VISIBILITY_PRIVATE——表明只有当没有锁屏的时候会显示
VISIBILITY_PUBLIC——标明在任何情况下都会显示
VISIBILITY_SECRET——表明在pin、password等安全锁和没有锁的情况下才能够显示
在使用的时候:
builder.setVisibility(Notification.VISIBILITY_PUBLIC);

builder.setTicker("Ticker...");// 通知首次出现在通知栏时显示的内容,带动画效果
builder.setDefaults(Notification.DEFAULT_ALL);// 通知的声音,闪光和振动效果为当前用户的默认设置

    builder.setCategory(Notification.CATEGORY_EMAIL);//设置Notification显示的位置
    builder.setColor(Color.RED);//设置Notification背景颜色


另外,由于版本之间有差异性,我们需要使用:
boolean isAboveLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
builder.setSmallIcon(isAboveLollipop ? R.mipmap.ic_launcher : R.mipmap.ic_launcher);
来对版本做出不同的设置,至于5.0之后系统代码发生了哪些变化,感兴趣的可以阅读这个文章:
http://www.cnblogs.com/avenwu/p/4180193.html

本文Demo下载地址:
http://download.csdn.net/detail/u012552275/9634661

放上官网的说明:
https://developer.android.com/training/notify-user/index.html

作者:u012552275 发表于2016/9/20 12:20:30 原文链接
阅读:144 评论:0 查看评论

Activity详解一 配置、启动和关闭activity

$
0
0

先看效果图

Android为我们提供了四种应组件,分别为Activity、Service、Broadcast receivers和Content providers,这些组建也就是我们开发一个Android应用程序的基石。系统可以通过不同组建提供的切入点进入到开发的应用程序中。对用户来说不是所有的组建都是实际的切入点,但是他们之间都是相互依赖的,它们每一个作为存在的实体,扮演着特定的角色,作为独一无二的基石帮助开发者定义Android应用的行为。下面我将整理自己的Activity学习点滴:

        一个Acitvity作为一个显示在屏幕上的用户交互界面,比如在电子邮件应用中,一个用来显示收件列表的Activity,一个用来写邮件的Activity,一个阅读邮件内容的Activity,等等。Activity用来提供用户体验,许多不同体验的Activity聚集在一起即可以形成一个Android应用程序的用户体验,每一Activity都是相互独立的。应用除了可以访问自己的Activity,也可以访问其他APP的Acitivity(需要被APP允许)。

1.如何创建一个Activity?

    必须创建一个Activity的 子类,在子类中需要实现Activity状态在生命周期中切换时系统回调的函数(onCreate、onStart、onResume、onPause、onStop、onDestroy),当然并非所有的函数都需要重新实现。其中两个比较重要的函数为onCreate和onPause:

          onCreate(),此方法必须要重写。系统调用此方法创建activity,实现该方法是你初始化你所创建Activity的重要步骤。其中最重要的就是调用 setContentView() 去定义你的要展现的用户界面的布局。

          onPause(),当系统任务用户离开此界面时会调用此方法,此时并非销毁一个Activity。通常在这里就要处理一些持久超越用户会话的变化,比如:数据的保存。

         为了保证流畅的用户体验和处理,你可以调用其他的回调函数来使你的Atctivity停止或销毁。在onStop()方法中,一般做一些大资源货对象的释放,如:网络或者数据库连接。可以在onResume时再加载所需要资源。

2创建Activity  

public class MainActivity extends Activity { 
 
    //必须重写的方法 
 
    @Override 
 
    protected void onCreate(Bundle savedInstanceState) { 
 
        super.onCreate(savedInstanceState); 
 
        setContentView(R.layout.activity_main);//activity的布局 
 
    } 
 
} 

 

 

       2.一个Activity创建完成后,为了它可以访问系统必须要声明注册它到应用的AndroidManifest.xml文件中:

<activity 
 
    android:name="com.zy.demo.activity.MainActivity" 
 
    android:label="@string/app_name" > 
 
    <intent-filter> 
 
        <action android:name="android.intent.action.MAIN" /> 
 
        <category android:name="android.intent.category.LAUNCHER" /> 
 
    </intent-filter> 
 
</activity> 


      <activity>有很多属性供开发者定义不同特色的Activity,比如lable、icon或者theme、style等。其中android:name是必须的属性,用来定义activity的名字,当应用发布后不能改变。

        <activity>还提供各种intent-filter,使用<intent-filter>来声明其它应用组件如何激活(启动)Activity,<intent-filter>有包含<action>和<category>两个元素。如上例中<action android:name="android.intent.action.MAIN" />用来表示此Activity需要响应android.intent.action.MAIN(表明为应用程序的主要入口),<category android:name="android.intent.category.LAUNCHER" />表示Activity为LAUNCHER类别,即应用程序会列在Launcher中,允许用户直接启动。以上也是一个应用的主activity所必须的声明方法:一个MAIN action,和一个LAUNCHER category。如果要Activity响应其他应用的隐式的intent,则需要为Activity声明对应action,还可以添加categor和data。

3.Activity的启动

3.1 startActivity

        通过调用startActivity(intent)启动Activity,intent用来准确的描述你要启动的Activity,或者你要进行的action,intent也可以用来携带小数据给被启动Acitivity。

           当在同一个应用中间需要简单启动另一个Activity,intent明确的定义你要启动Activity类即可:

//定义一个intent,指名要启动的activity:ToStartActivity 
 
Intent intent =  new Intent(MainActivity.this,ToStartActivity.class); 
 
//使用startActivity(),启动activity 
 
startActivity(intent); 

 

           在你的应用程序需要执行一些自身没有Activity可以执行的行为时,我们可以使用手机上的其他应用程序的Activity来代替执行。比如发送一个mail、查看一张图片、搜索一个单词等等。这个里也就是Intent的重要指出,你可以定义一个intent描述你想要做的行为,等你发送给系统后,系统会启动合适的Acitivty帮你执行,如果有多个应用的Activity都可以处理此行为时,系统会让用户去选择一个。当此Activity执行完毕后,原来的Activity将比

              

//跨应用从google界面搜索 
 
              Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); 
 
              intent.putExtra(SearchManager.QUERY, "zy"); 
 
              startActivity(intent); 

 

     当跨应用启动Activity时,在定义intent时必须要为他指定具体的acitvity,前提是此activity必须暴露在自己应用程序之外(android:exported="true"):

Intent intent = new Intent(); 
 
//指定要启动组建完整的包名,对象名 
 
ComponentName cn = new ComponentName("com.android.settings", 
 
        "com.android.settings.RunningServices"); 
 
intent.setComponent(cn); 
 
// 使用context.startActivity()时需要新启一个任务 
 
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
 
startActivity(intent); 

 

3.2 startActivityForResult

       通过调用 startActivityForResult(intent),来接收到启动的Acitivity反馈的结果。为了接收接下来启动的Activity的结果,需要重写onActivityResult()这个回调函数。当调用的activity完成后,它将返回一个含有结果的intent给onActivityResult()处理。比如,在应用程序的Activity中,需要用户选择联系人中的一个,Activity需要得到联系人的部分信息:

         

Intent intent = new Intent(Intent.ACTION_PICK, 
 
                        Contacts.People.CONTENT_URI); 
 
                //启动一个带有选择联系人返回结果的activity 
 
                startActivityForResult(intent, PICK_CONTACT_REQUEST); 
 
          这里的PICK_CONTACT_REQUEST为自定义的int型请求反馈结果代码。
 
//重新onActivityResult()用来处理接收到的返回结果 
 
@Override 
 
protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
 
    // 如果请求requestCode成功,且请求返回的结果resultCode是我们要的PICK_CONTACT_REQUEST 
 
    if (resultCode == Activity.RESULT_OK 
 
            && requestCode == PICK_CONTACT_REQUEST) {            
 
        // 处理Intent返回的数据,在联系人数据库中查找联系人的名字 
 
        Cursor cursor = getContentResolver().query(data.getData(), 
 
                new String[] { Contacts.People.NAME }, null, null, null); 
 
           
 
        if (cursor.moveToFirst()) { // 如果cursor不为空,就查找出联系人的名字 
 
            int columnIndex = cursor.getColumnIndex(Contacts.People.NAME); 
 
            String name = cursor.getString(columnIndex); 
 
            //添加其他功能 
 
        } 
 
    } 
 
}    

 

        这里在要说明是onActivityResult()使用来处理返回结果的,首先要检查的是请求是否成功,然后是否有返回结果,结果是否是startActivityForResult()中所要的,如果满足,则处理通过Intent返回的数据。

4.关闭Activity

1  Activity可以调用finish()方法关闭自己,也可以通过调用finishActivity()的方法关闭一个独立的之前启动的Activity。

2 调用finishActivity()的方法关闭一个独立的之前启动的Activity

 //此方法用在关闭使用startActivityForResult(requestCode)启用的Activity  

 this.finishActivity(requestCode);  

           关于何时关闭一个Activity,一般由系统直接为我们管理。但是当你确认用户不用返回到此Activity时,我们调用以上方法关闭对应的Activity。

 

5 Demo代码:

package mm.shandong.com.testusea;
 
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
 
public class TestUseAActivity extends AppCompatActivity {
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_use_a);
    }
   //启动第一个activity
    public void startFirstActivity(View view) {
        Intent intent = new Intent(this, TestUseAActivity2.class);
        startActivity(intent);
    }
    //启动第二个activity
    public void startSecondActivity(View view) {
        Intent intent = new Intent(this, TestUseAActivity3.class);
        startActivity(intent);
    }
    //启动第三个activity,这个activity 4秒钟后被关闭
    public void startThirdActivity(View view) {
        Intent intent = new Intent(this, TestUseAActivity4.class);
        startActivityForResult(intent, 1);
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(4000);
                    finishActivity(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();
 
    }
}


 Demo下载 
最后,以上例子都来源与安卓无忧,请去应用宝或者豌豆荚下载:http://android.myapp.com/myapp/detail.htm?apkName=com.shandong.mm.androidstudy,源码例子文档一网打尽。



作者:androidWuYou 发表于2016/9/20 12:55:24 原文链接
阅读:192 评论:0 查看评论

手把手教你做音乐播放器(一)功能规划

$
0
0

前言

学习完“计算器” “视频播放器” “蓝牙聊天”以后,对安卓应用的开发我们基本上就入门70%了。

现在,我们将在之前学习的基础上,进一步完善我们要掌握的安卓开发技术,开发一个“音乐播放器”。

当完成这个“音乐播放器”应用后,我们对安卓的应用开发就完全的入门,能和大部分安卓开发者侃侃而谈了,当然更重要的是能够开发更多功能全面、复杂的应用程序了。

本文针对的读者是:

  1. 对安卓开发有了初步认识,但还没有什么经验的新人;
  2. 对已有的安卓开发经验没有系统化整理的开发者;

在开始以前,假设各位已经做好了如下准备:

  • 一台开发用笔记本电脑,并搭建好了开发环境;
  • 一部安卓系统设备(手机或平板电脑);
  • 设备上存放了可以播放的音乐文件;
  • 一根连接电脑和安卓设备的数据线(通常是micro usb数据线);
  • 一到两天时间;
  • 耐心与求知欲;

本文的代码,可以从安豆网示例代码中下载。

第1节 功能规划

音乐是我们日常必备的精神粮食,每个人的手机里面一定会有一个音乐播放器。在坐公交时,它会陪伴着我们;在运动的时候,它会陪伴着我们;睡觉前,它同样会陪伴着我们。

与之前规划产品的思路一样,我们先要做加法,尽可能的把音乐播放器可以拥有的功能挖掘出来;然后再做减法,把不实用、或者投入性价比不高的功能放一放;最后,再根据用户的反馈、加上上一版产品留下的遗憾,进行产品的升级。

让我们把有限的精力集中到最为重要的功能上面去。

1.1 可能的功能点

音乐播放器我们已经见过很多了,它们的功能越来越高级,最早只能播放本地设备上存储的音乐,现在进化的还能够播放网络端端音乐了。

我们就先来为自己的音乐播放器来做个头脑风暴吧,看看它可以拥有哪些功能:

  1. 展示出本地的音乐文件,并显示该文件的相关信息,例如歌曲名、演唱者、长度、所属唱片、歌曲的封面图片;
  2. 音乐可以按照演唱者、专辑名称、男女歌手、歌曲类型等信息进行分类,进入每个分类后,按照字母顺序展现歌曲列表;
  3. 删除选中的本地音乐;
  4. 播放本地音乐,在播放的过程中,可以停止播放、回复播放、播放下一首、播放上一首、拖动音乐进度条到音乐任意时间点播放;
  5. 播放时,能显示当前播放音乐的进度,指示播放的时间;
  6. 播放时,能实时的显示当前歌词;
  7. 能添加选定的歌曲到播放列表,让播放器播放列表中的歌曲,播放的顺序可以设置顺序播放、单曲循环播放、随机播放等等;
  8. 音乐播放器等界面退出以后,音乐仍然能在后台播放;再次打开播放器,能显示当前播放的实时信息,例如播放进度条;
  9. 记录每首音乐的播放历史,下一次播放音乐就从以前播放到的地方继续播放;
  10. 可以连接到网络,播放网络上的音乐;
  11. 下载网络上的音乐;
  12. 让用户标注喜欢的音乐,允许用户给音乐写评论;
  13. 根据用户的播放历史,为他推荐可能会喜欢的音乐;
  14. 用户通过音乐来交友
    ……

可以添加的功能实在是太多了,这个清单实在是不能包含其万一。

1.2 功能的筛选

从上面列出的明细可以看出,能够赋予这个聊天应用的功能实在是太多了,因此我们必须根据我们的能力和精力来进行筛选,做功能的减法。

大体上看,上面的功能清单将功能分成了两块,

  1. 不需要网络支持的本地播放;
  2. 需要网络支持的在线播放和网络社交;

加入网络方面的功能是一个很好的想法,不过就目前来说,我们啥也没有,还是先把更为基础简单的功能实现了吧。因此,我们决定先做好本地播放器的功能,其它的以后再说。

在实现本地音乐播放器的过程中,我们也选择避繁就简的原则,对那些不是原则上重要的功能能省就省,做到尽量简单。

1.3 现阶段的功能

根据上面设计的原则,我们来确定音乐播放器的具体功能:

  1. 展示出本地的音乐文件,并显示该歌曲的封面图片、歌曲名、长度;
  2. 播放本地音乐,在播放的过程中,可以停止播放、回复播放、播放下一首、播放上一首、拖动音乐进度条到音乐任意时间点播放;
  3. 播放时,能显示当前播放音乐的进度,指示播放的时间;
  4. 点击音乐列表中的单个音乐,会将它添加到播放列表中,并立即播放该音乐;
  5. 长按音乐列表中的单个音乐,进入多选模式,将多选中的音乐作为新的播放列表替换以前的播放列表,并从头开始播放;
  6. 音乐播放器等界面退出以后,音乐仍然能在后台播放;再次打开播放器,能显示当前播放的实时信息,例如播放进度条;
  7. 每次启动音乐播放器,播放器装载之前的播放列表,并把列表中的第一首音乐作为默认的首播曲;
  8. 假如音乐正在播放,启动音乐播放器后,播放器装载之前的播放列表,并把正在播放的音乐作为列表中默认的首播曲;

这里我们再增加一个福利,为音乐播放器增加一个桌面小工具。当它放到桌面上以后,用户能很方便的控制音乐的播放。

在桌面小工具上,可以显示音乐的封面,歌曲的名字,以及控制它的播放、暂停、上一首、下一首。

因此,对视频播放器的界面进行了如下的设计:

整个音乐播放的流程应该是,

  1. 用户在MusicListActivity通过长按,开始选择多首音乐,组成一个播放列表;
  2. 这个播放列表被传递给MusicServiceMusicService操作PlayListContentProvider清空原有的播放列表,然后将这份新的列表存储到PlayListContentProvider当中;
  3. 用户点击MusicListActivity上的播放按钮时,MusicService开始从播放列表获取第一首曲子开始播放;
  4. 播放的过程中,MusicService将当前播放的进度实时更新到PlayListContentProvider中;
  5. 当播放的音乐有变化(播放完成、切换歌曲、播放进度每秒的变化),MusicService都将通知给MusicListActivity,让其能够同步的改变界面显示;
  6. 播放过程中,MusicListActivity能通过调用MusicService提供的接口控制音乐的暂停、继续、播放上一首、下一首;
  7. 如果用户单独点击音乐列表中的音乐,该音乐将被添加到播放列表中的第一位,并开始播放;
  8. 当播放到以前播放过的歌曲,将从它曾经停止播放的位置继续播放,如果之前该乐曲被播放完毕,这次播放则从头开始播放;

关于播放的规则是我们自己定义的,如果你有自己的想法,可以在完成音乐播放器的开发后,按照自己的想法修改,做到融会贯通。

1.4 功能条件的假设

我们确定了音乐播放器应该具备的功能,还需要给出实现这些功能的一些基本假设。

  1. 设备上可被播放的音频文件很多很多,可能是通话录音,可能是语音记事本,为了简便操作,我们将认为音乐文件都放在包含了music关键字的路径当中;

  2. 设备上已经准备好自带封面的音乐。比如说一首MP3格式的音乐,它所包含的内容并不只是音乐本省,还包括了很多元信息,例如歌曲的作者,歌曲的所属专辑,甚至歌曲的专辑封面图片等很多丰富的信息。只不过我们通常只注意到了MP3格式的音乐本身的音乐内容和文件名字,没有直观的看到其它这些信息。元信息是可选的内容,所以有的音乐文件也没有把对应的信息填充到里面去。不过正版版权到音乐供应商都会注意到这些细节,把与这首音乐更多的信息都提供给大家。

    我们这里使用的测试音乐都是通过网易云音乐下载的正版歌曲(你也可以去别的渠道下载正版歌曲,作为调试程序时使用的素材),所以基本上会看到歌曲的封面图片。如果没有封面图片,我们会让应用显示默认的封面图片。

1.5 关于遗憾

对于那些没有在这个阶段加入的功能,期待以后加入吧。

对于那些为了简化开发难度、减少开发时间而采用的简单设计,期待在下一版程序中优化和完善吧。

前言

学习完“计算器” “视频播放器” “蓝牙聊天”以后,对安卓应用的开发我们基本上就入门70%了。

现在,我们将在之前学习的基础上,进一步完善我们要掌握的安卓开发技术,开发一个“音乐播放器”。

当完成这个“音乐播放器”应用后,我们对安卓的应用开发就完全的入门,能和大部分安卓开发者侃侃而谈了,当然更重要的是能够开发更多功能全面、复杂的应用程序了。

本文针对的读者是:

  1. 对安卓开发有了初步认识,但还没有什么经验的新人;
  2. 对已有的安卓开发经验没有系统化整理的开发者;

在开始以前,假设各位已经做好了如下准备:

  • 一台开发用笔记本电脑,并搭建好了开发环境;
  • 一部安卓系统设备(手机或平板电脑);
  • 设备上存放了可以播放的音乐文件;
  • 一根连接电脑和安卓设备的数据线(通常是micro usb数据线);
  • 一到两天时间;
  • 耐心与求知欲;

本文的代码,可以从安豆网示例代码中下载。

第1节 功能规划

音乐是我们日常必备的精神粮食,每个人的手机里面一定会有一个音乐播放器。在坐公交时,它会陪伴着我们;在运动的时候,它会陪伴着我们;睡觉前,它同样会陪伴着我们。

与之前规划产品的思路一样,我们先要做加法,尽可能的把音乐播放器可以拥有的功能挖掘出来;然后再做减法,把不实用、或者投入性价比不高的功能放一放;最后,再根据用户的反馈、加上上一版产品留下的遗憾,进行产品的升级。

让我们把有限的精力集中到最为重要的功能上面去。

1.1 可能的功能点

音乐播放器我们已经见过很多了,它们的功能越来越高级,最早只能播放本地设备上存储的音乐,现在进化的还能够播放网络端端音乐了。

我们就先来为自己的音乐播放器来做个头脑风暴吧,看看它可以拥有哪些功能:

  1. 展示出本地的音乐文件,并显示该文件的相关信息,例如歌曲名、演唱者、长度、所属唱片、歌曲的封面图片;
  2. 音乐可以按照演唱者、专辑名称、男女歌手、歌曲类型等信息进行分类,进入每个分类后,按照字母顺序展现歌曲列表;
  3. 删除选中的本地音乐;
  4. 播放本地音乐,在播放的过程中,可以停止播放、回复播放、播放下一首、播放上一首、拖动音乐进度条到音乐任意时间点播放;
  5. 播放时,能显示当前播放音乐的进度,指示播放的时间;
  6. 播放时,能实时的显示当前歌词;
  7. 能添加选定的歌曲到播放列表,让播放器播放列表中的歌曲,播放的顺序可以设置顺序播放、单曲循环播放、随机播放等等;
  8. 音乐播放器等界面退出以后,音乐仍然能在后台播放;再次打开播放器,能显示当前播放的实时信息,例如播放进度条;
  9. 记录每首音乐的播放历史,下一次播放音乐就从以前播放到的地方继续播放;
  10. 可以连接到网络,播放网络上的音乐;
  11. 下载网络上的音乐;
  12. 让用户标注喜欢的音乐,允许用户给音乐写评论;
  13. 根据用户的播放历史,为他推荐可能会喜欢的音乐;
  14. 用户通过音乐来交友
    ……

可以添加的功能实在是太多了,这个清单实在是不能包含其万一。

1.2 功能的筛选

从上面列出的明细可以看出,能够赋予这个聊天应用的功能实在是太多了,因此我们必须根据我们的能力和精力来进行筛选,做功能的减法。

大体上看,上面的功能清单将功能分成了两块,

  1. 不需要网络支持的本地播放;
  2. 需要网络支持的在线播放和网络社交;

加入网络方面的功能是一个很好的想法,不过就目前来说,我们啥也没有,还是先把更为基础简单的功能实现了吧。因此,我们决定先做好本地播放器的功能,其它的以后再说。

在实现本地音乐播放器的过程中,我们也选择避繁就简的原则,对那些不是原则上重要的功能能省就省,做到尽量简单。

1.3 现阶段的功能

根据上面设计的原则,我们来确定音乐播放器的具体功能:

  1. 展示出本地的音乐文件,并显示该歌曲的封面图片、歌曲名、长度;
  2. 播放本地音乐,在播放的过程中,可以停止播放、回复播放、播放下一首、播放上一首、拖动音乐进度条到音乐任意时间点播放;
  3. 播放时,能显示当前播放音乐的进度,指示播放的时间;
  4. 点击音乐列表中的单个音乐,会将它添加到播放列表中,并立即播放该音乐;
  5. 长按音乐列表中的单个音乐,进入多选模式,将多选中的音乐作为新的播放列表替换以前的播放列表,并从头开始播放;
  6. 音乐播放器等界面退出以后,音乐仍然能在后台播放;再次打开播放器,能显示当前播放的实时信息,例如播放进度条;
  7. 每次启动音乐播放器,播放器装载之前的播放列表,并把列表中的第一首音乐作为默认的首播曲;
  8. 假如音乐正在播放,启动音乐播放器后,播放器装载之前的播放列表,并把正在播放的音乐作为列表中默认的首播曲;

这里我们再增加一个福利,为音乐播放器增加一个桌面小工具。当它放到桌面上以后,用户能很方便的控制音乐的播放。

在桌面小工具上,可以显示音乐的封面,歌曲的名字,以及控制它的播放、暂停、上一首、下一首。

因此,对视频播放器的界面进行了如下的设计:

整个音乐播放的流程应该是,

  1. 用户在MusicListActivity通过长按,开始选择多首音乐,组成一个播放列表;
  2. 这个播放列表被传递给MusicServiceMusicService操作PlayListContentProvider清空原有的播放列表,然后将这份新的列表存储到PlayListContentProvider当中;
  3. 用户点击MusicListActivity上的播放按钮时,MusicService开始从播放列表获取第一首曲子开始播放;
  4. 播放的过程中,MusicService将当前播放的进度实时更新到PlayListContentProvider中;
  5. 当播放的音乐有变化(播放完成、切换歌曲、播放进度每秒的变化),MusicService都将通知给MusicListActivity,让其能够同步的改变界面显示;
  6. 播放过程中,MusicListActivity能通过调用MusicService提供的接口控制音乐的暂停、继续、播放上一首、下一首;
  7. 如果用户单独点击音乐列表中的音乐,该音乐将被添加到播放列表中的第一位,并开始播放;
  8. 当播放到以前播放过的歌曲,将从它曾经停止播放的位置继续播放,如果之前该乐曲被播放完毕,这次播放则从头开始播放;

关于播放的规则是我们自己定义的,如果你有自己的想法,可以在完成音乐播放器的开发后,按照自己的想法修改,做到融会贯通。

1.4 功能条件的假设

我们确定了音乐播放器应该具备的功能,还需要给出实现这些功能的一些基本假设。

  1. 设备上可被播放的音频文件很多很多,可能是通话录音,可能是语音记事本,为了简便操作,我们将认为音乐文件都放在包含了music关键字的路径当中;

  2. 设备上已经准备好自带封面的音乐。比如说一首MP3格式的音乐,它所包含的内容并不只是音乐本省,还包括了很多元信息,例如歌曲的作者,歌曲的所属专辑,甚至歌曲的专辑封面图片等很多丰富的信息。只不过我们通常只注意到了MP3格式的音乐本身的音乐内容和文件名字,没有直观的看到其它这些信息。元信息是可选的内容,所以有的音乐文件也没有把对应的信息填充到里面去。不过正版版权到音乐供应商都会注意到这些细节,把与这首音乐更多的信息都提供给大家。

    我们这里使用的测试音乐都是通过网易云音乐下载的正版歌曲(你也可以去别的渠道下载正版歌曲,作为调试程序时使用的素材),所以基本上会看到歌曲的封面图片。如果没有封面图片,我们会让应用显示默认的封面图片。

1.5 关于遗憾

对于那些没有在这个阶段加入的功能,期待以后加入吧。

对于那些为了简化开发难度、减少开发时间而采用的简单设计,期待在下一版程序中优化和完善吧。


/*******************************************************************/
* 版权声明
* 本教程只在CSDN安豆网发布,其他网站出现本教程均属侵权。
/*******************************************************************/

作者:anddlecn 发表于2016/9/20 13:25:56 原文链接
阅读:144 评论:0 查看评论

一步一步实现直播和弹幕

$
0
0

序言

最近在研究直播的弹幕,东西有点多,准备记录一下免得自己忘了又要重新研究,也帮助有这方面需要的同学少走点弯路。关于直播的技术细节其实就是两个方面一个是推流一个是拉流,而弹幕的实现核心在即时聊天,使用聊天室的就能实现,只是消息的展示方式不同而已。在大多数的项目中还是使用第三方的直播平台实现推流功能,因此关于直播平台的选择也是至关重要。下面由我娓娓道来。

效果

为了演示方便我把屏幕录像上传到优酷了,这是视频地址

这里写图片描述

功能

1.缓冲进度

这里写图片描述

2.弹幕

这里写图片描述

3.横竖屏切换

这里写图片描述

实现

1.直播SDK的选择

提供直播功能的厂商有很多,比如七牛云,乐视,百度云,腾讯云,金山云,等等。功能也大同小异,常见的缩略图,视频录制,转码,都可以实现。但是对于SDK的易用程度还是不敢恭维的。下面我说说我遇到的一些问题。

1.乐视

乐视云 移动直播

优点:
乐视直播的注册流程还是很方便的,选择个人开发者,然后验证身份信息就可以使用了,每人每月免费10GB的流量。

缺点

最大的缺点就是稳定性,至少在我测试的时候也就是2016年9月份稳定性很差,不是说视频的稳定性,而是推流的稳定性,我有一台在同样的网络下我的ViVO X7能推流,但是魅蓝NOTE2不能推流。然而ViVO X7推出去的流在电脑上用VLC能播放,在其他手机上显示黑屏,既不报错也没画面。随后使用同样的网络,同样的魅蓝NOTE2,百度的SDK就能推流。看来乐视的直播技术方面还有待改进,直接pass。

这里写图片描述

2.七牛云

七牛云官网

优点
态度好,服务周到,其他方面的不能再评价了,因为没有真正使用过,这的确很尴尬,不过态度的确很好,会有客服打电话过来询问需求,会有技术支持人员主动沟通,这是很值得肯定的。

缺点
倒不能算是缺点,可能算特点吧,七牛云需要使用域名别名解析来做RTMP直播流域名,也就是说你要使用七牛云必须要有一个备案过的域名,由于我司的域名我不能轻易去改,而且我也没有备案过的域名,所以不能测试。

这里写图片描述

3.腾讯云

还没有通过审核,效率太低。

4.阿里云

也需要域名,跳过。

5.百度云

百度音视频直播 LSS

优点

审核速度挺快的,实名认证大概15分钟搞定(这是我的速度,仅供参考),不需要域名,为个人开发者免费提供10G流量测试,这点很良心。而且功能很全面,推流很简单。下面是价格表:

这里写图片描述

缺点

企业用户需要认证,否则单月最大流量为1TB,个人用户总流量限制在1000GB。

经过以上对比最终选择了百度云来实现直播。

2.及时聊天SDK的选择

这里边倒没有太多的考虑,环信,融云,LeanCloud都可以,但是长期使用leancloud发现其文档质量很高,SDK简单易用。所以使用了LeanCloud来实现即时通讯。

LearnCloud Android 实时通信开发指南

3.弹幕实现

弹幕说白了就是聊天室,只是聊天室的消息需要在视频节目上显示而已,所以首先要实现一个聊天室,此处使用LeanCloud实现。

第一步:初始化

这里写图片描述

第二步:登录

package com.zgh.livedemo;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;

import com.avos.avoscloud.im.v2.AVIMClient;
import com.avos.avoscloud.im.v2.AVIMException;
import com.avos.avoscloud.im.v2.callback.AVIMClientCallback;

public class LoginActivity extends AppCompatActivity {
    EditText et_name;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        et_name = (EditText) findViewById(R.id.et_name);
        findViewById(R.id.btn_login).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String name = et_name.getText().toString();
                if (TextUtils.isEmpty(name)) {
                    Toast.makeText(LoginActivity.this, "登录名不能为空", Toast.LENGTH_SHORT).show();
                    return;
                }
                login(name);
            }
        });
    }


    public void login(String name) {
          //使用name作为cliendID
        AVIMClient jerry = AVIMClient.getInstance(name);
        jerry.open(new AVIMClientCallback() {

            @Override
            public void done(AVIMClient client, AVIMException e) {
                if (e == null) {
                    Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
                    //保存client
                    MyApp.mClient = client;
                    startActivity(new Intent(LoginActivity.this, MainActivity.class));
                } else {
                    Toast.makeText(LoginActivity.this, "登录失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
                }
            }
        });
    }


}

第三步,进入聊天室

在进入直播界面的时候调用此方法,进入聊天室。conversationId应该从服务器获取,此处用于测试使用了一个固定的ID。


    private void join() {
        MyApp.mClient.open(new AVIMClientCallback() {

            @Override
            public void done(AVIMClient client, AVIMException e) {
                if (e == null) {
                    //登录成功
                    conv = client.getConversation("57d8b2445bbb50005e420535");
                    conv.join(new AVIMConversationCallback() {
                        @Override
                        public void done(AVIMException e) {
                            if (e == null) {
                                //加入成功
                                Toast.makeText(MainActivity.this, "加入聊天室成功", Toast.LENGTH_SHORT).show();
                                et_send.setEnabled(true);
                            } else {
                                Toast.makeText(MainActivity.this, "加入聊天室失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
                                et_send.setEnabled(false);
                                android.util.Log.i("zzz", "加入聊天室失败 :" + e.getMessage());
                            }
                        }
                    });
                }
            }
        });
    }

登录成功以后,在onResum的时候将此Activity注册为消息处理者,在onPause的时候取消注册。而在application的onCreate的时候注册一个默认的处理器,也就是说当APP在后头运行的时候,通过默认处理器处理消息,即弹出状态栏弹出通知,而在聊天界面由当前界面处理消息。


    @Override
    protected void onResume() {
        super.onResume();
        AVIMMessageManager.registerMessageHandler(AVIMTextMessage.class, roomMessageHandler);
    }

    @Override
    protected void onPause() {
        super.onPause();
        AVIMMessageManager.unregisterMessageHandler(AVIMTextMessage.class, roomMessageHandler);
    }

在接收到消息以后把消息显示在弹幕控件上。


    public class RoomMessageHandler extends AVIMMessageHandler {
        //接收到消息后的处理逻辑
        @Override
        public void onMessage(AVIMMessage message, AVIMConversation conversation, AVIMClient client) {
            if (message instanceof AVIMTextMessage) {
                String info = ((AVIMTextMessage) message).getText();
                //添加消息到屏幕
                addMsg(info);
            }
        }

    }

    private void addMsg(String msg) {
        TextView textView = new TextView(MainActivity.this);
        textView.setText(msg);
        ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        params.setMargins(5, 10, 5, 10);
        textView.setLayoutParams(params);
        ll_room.addView(textView, 0);
        barrageView.addMessage(msg);
    }

弹幕的控件

package com.zgh.livedemo.view;

import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.RelativeLayout;
import android.widget.TextView;

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

/**
 * Created by lixueyong on 16/2/19.
 */
public class BarrageView extends RelativeLayout {
    private Context mContext;
    private BarrageHandler mHandler = new BarrageHandler();
    private Random random = new Random(System.currentTimeMillis());
    private static final long BARRAGE_GAP_MIN_DURATION = 1000;//两个弹幕的最小间隔时间
    private static final long BARRAGE_GAP_MAX_DURATION = 2000;//两个弹幕的最大间隔时间
    private int maxSpeed = 10000;//速度,ms
    private int minSpeed = 5000;//速度,ms
    private int maxSize = 30;//文字大小,dp
    private int minSize = 15;//文字大小,dp

    private int totalHeight = 0;
    private int lineHeight = 0;//每一行弹幕的高度
    private int totalLine = 0;//弹幕的行数
    private List<String> messageList = new ArrayList<>();
    // private String[] itemText = {"是否需要帮忙", "what are you 弄啥来", "哈哈哈哈哈哈哈", "抢占沙发。。。。。。", "************", "是否需要帮忙",
    //        "我不会轻易的狗带", "嘿嘿", "这是我见过的最长长长长长长长长长长长的评论"};
    private int textCount;
//    private List<BarrageItem> itemList = new ArrayList<BarrageItem>();

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

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

    public BarrageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        init();
    }

    private void init() {
        // textCount = itemText.length;

        int duration = (int) ((BARRAGE_GAP_MAX_DURATION - BARRAGE_GAP_MIN_DURATION) * Math.random());
        mHandler.sendEmptyMessageDelayed(0, duration);
    }

    public void addMessage(String message) {
        messageList.add(message);
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        totalHeight = getMeasuredHeight();
        lineHeight = getLineHeight();
        totalLine = totalHeight / lineHeight;
    }

    private void generateItem() {
        if (messageList.size() > 0) {
            BarrageItem item = new BarrageItem();
            String tx = messageList.remove(0);
            int sz = (int) (minSize + (maxSize - minSize) * Math.random());
            item.textView = new TextView(mContext);
            item.textView.setText(tx);
            item.textView.setTextSize(sz);
            item.textView.setTextColor(Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256)));
            item.textMeasuredWidth = (int) getTextWidth(item, tx, sz);
            item.moveSpeed = (int) (minSpeed + (maxSpeed - minSpeed) * Math.random());
            if (totalLine == 0) {
                totalHeight = getMeasuredHeight();
                lineHeight = getLineHeight();
                totalLine = totalHeight / lineHeight;
            }
            item.verticalPos = random.nextInt(totalLine) * lineHeight;
//        itemList.add(item);
            showBarrageItem(item);
        }
    }

    private void showBarrageItem(final BarrageItem item) {

        int leftMargin = this.getRight() - this.getLeft() - this.getPaddingLeft();

        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
        params.topMargin = item.verticalPos;
        this.addView(item.textView, params);
        Animation anim = generateTranslateAnim(item, leftMargin);
        anim.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {

            }

            @Override
            public void onAnimationEnd(Animation animation) {
                item.textView.clearAnimation();
                BarrageView.this.removeView(item.textView);
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
        item.textView.startAnimation(anim);
    }

    private TranslateAnimation generateTranslateAnim(BarrageItem item, int leftMargin) {
        TranslateAnimation anim = new TranslateAnimation(leftMargin, -item.textMeasuredWidth, 0, 0);
        anim.setDuration(item.moveSpeed);
        anim.setInterpolator(new AccelerateDecelerateInterpolator());
        anim.setFillAfter(true);
        return anim;
    }

    /**
     * 计算TextView中字符串的长度
     *
     * @param text 要计算的字符串
     * @param Size 字体大小
     * @return TextView中字符串的长度
     */
    public float getTextWidth(BarrageItem item, String text, float Size) {
        Rect bounds = new Rect();
        TextPaint paint;
        paint = item.textView.getPaint();
        paint.getTextBounds(text, 0, text.length(), bounds);
        return bounds.width();
    }

    /**
     * 获得每一行弹幕的最大高度
     *
     * @return
     */
    private int getLineHeight() {
      /*  BarrageItem item = new BarrageItem();
        String tx = itemText[0];
        item.textView = new TextView(mContext);
        item.textView.setText(tx);
        item.textView.setTextSize(maxSize);

        Rect bounds = new Rect();
        TextPaint paint;
        paint = item.textView.getPaint();
        paint.getTextBounds(tx, 0, tx.length(), bounds);
        return bounds.height();*/
        return 50;
    }

    class BarrageHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            generateItem();
            //每个弹幕产生的间隔时间随机
            int duration = (int) ((BARRAGE_GAP_MAX_DURATION - BARRAGE_GAP_MIN_DURATION) * Math.random());
            this.sendEmptyMessageDelayed(0, duration);
        }
    }

}

剩下的细节看demo吧。

4.视频播放

视频的播放使用的是vitamio框架关于具体的API请参考这里这里

这里写图片描述

需要注意的是在状态的获取,通过设置不同的监听来实现的。

     mVideoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {
            public boolean onInfo(MediaPlayer mp, int what, int extra) {
                //缓冲开始
                if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
                    layout_loading.setVisibility(View.VISIBLE);
                    android.util.Log.i("zzz", "onStart");
                //缓冲结束
                } else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
                    //此接口每次回调完START就回调END,若不加上判断就会出现缓冲图标一闪一闪的卡顿现象
                    android.util.Log.i("zzz", "onEnd");
                    layout_loading.setVisibility(View.GONE);
                 //   mp.start();
                    mVideoView.start();
                }
                return true;
            }
        });
        //获取缓存百分比
        mVideoView.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
            @Override
            public void onBufferingUpdate(MediaPlayer mp, int percent) {
                if(!mp.isPlaying()) {
                    layout_loading.setVisibility(View.VISIBLE);
                    tv_present.setText("正在缓冲" + percent + "%");
                }else{
                    layout_loading.setVisibility(View.GONE);
                }

            }
        });

        mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mediaPlayer) {
                mediaPlayer.setPlaybackSpeed(1.0f);
            }
        });
        //出错处理
        mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {

                tv_present.setText("加载失败");
                return true;
            }
        });

还有就是MediaController的使用,可以参考农民伯伯的vitamio中文API

需要注意的是在xml中使用MediaController时需要这样使用位置为VideoView之上,高度为需要显示的控制条的高度,内部需要包括控制控件,id必须为指定的ID,布局可以参考源码中这个文件
这里写图片描述

  <io.vov.vitamio.widget.MediaController
            android:id="@+id/mediacontroller"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_alignParentBottom="true"
            android:background="#ff0000">

            <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

                <ImageButton
                    android:id="@+id/mediacontroller_play_pause"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginLeft="5dp"
                    android:background="@drawable/mediacontroller_button"
                    android:contentDescription="@string/mediacontroller_play_pause"
                    android:src="@drawable/mediacontroller_pause" />
            </RelativeLayout>
        </io.vov.vitamio.widget.MediaController>

5.视频的全屏模式

其核心的逻辑是点击按钮,改变屏幕方向,在改变方向的时候隐藏聊天室,输入框等。同时改变控件的大小。要让Activity在屏幕切换的时候不重新创建需要添加这个选项。

  android:configChanges="keyboardHidden|orientation|screenSize"

核心代码

 private void fullScreen() {
        if (isScreenOriatationPortrait(this)) {// 当屏幕是竖屏时
            full(true);
            // 点击后变横屏
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            // 设置当前activity为横屏
            // 当横屏时 把除了视频以外的都隐藏
            //隐藏其他组件的代码
            ll_room.setVisibility(View.GONE);
            et_send.setVisibility(View.GONE);
            int width=getResources().getDisplayMetrics().widthPixels;
            int height=getResources().getDisplayMetrics().heightPixels;
            layout_video.setLayoutParams(new LinearLayout.LayoutParams(height, width));
            mVideoView.setLayoutParams(new RelativeLayout.LayoutParams(height,width));


        } else {
            full(false);
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);// 设置当前activity为竖屏
            //显示其他组件
            ll_room.setVisibility(View.VISIBLE);
            et_send.setVisibility(View.VISIBLE);
            int width=getResources().getDisplayMetrics().heightPixels;
            int height= (int) (width*9.0/16);
            layout_video.setLayoutParams(new LinearLayout.LayoutParams(width, height));
            mVideoView.setLayoutParams(new RelativeLayout.LayoutParams(width,height));

        }
    }

    //动态隐藏状态栏
    private void full(boolean enable) {
        if (enable) {
            WindowManager.LayoutParams lp = getWindow().getAttributes();
            lp.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
            getWindow().setAttributes(lp);
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
        } else {
            WindowManager.LayoutParams attr = getWindow().getAttributes();
            attr.flags &= (~WindowManager.LayoutParams.FLAG_FULLSCREEN);
            getWindow().setAttributes(attr);
            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
        }
    }

Demo

关于demo中的配置信息,我抽取到相关的config接口中了,大家只需要配置好就行了

下载地址

package com.zgh.livedemo;

/**
 * Created by zhuguohui on 2016/9/20.
 */
public interface Config {
    /**
     * learnCloud APP_ID
     */
    String APP_ID = "";
    /**
     * learnCloud APP_KEY
     */
    String APP_KEY = "";
    /**
     * learnCloud 聊天室ID
     */
    String CONVERSATION_ID = "";
    /**
     * rtmp 视频地址
     */
    String VIDEO_URL = "";
}

关于推流用的是百度直播SDK的官方的Demo

这里写图片描述

作者:qq_22706515 发表于2016/9/20 13:26:52 原文链接
阅读:249 评论:1 查看评论

android之ExpandableListView

$
0
0

ExpandableListView从字面意思来说就是对listview的扩展。只要我们掌握listview的用法。ExpandableListView就很容易。listview只是展示一级列表,而ExpandableListView展示的是二级列表。就像qq联系人这块。就是个二级列表。哈哈。。接下来我们就学习使用ExpandableListView这个控件。

我们可以去看看官网对它的介绍

这里写图片描述

上面的第一段大概意思是说:这个view可以垂直展示两个列表不同于listview。父列表可以展示子列表。这个数据的来源可以来自ExpandableListAdapter这个适配器来关联这个view。。。。
哈哈哈。。。我这绝对是我自己概述的。我绝对没用翻译软件。 不一定理解对哈。。。。讲代码之前先看看效果再来写。。。

这里写图片描述

这效果是不是像qq联系人这样的效果。不啰嗦,直接放代码哈。。嘻嘻

ContentUtil.java代码如下:

 public static int mImgs[] = new int[]{
            R.mipmap.ic_launcher,
            R.mipmap.ic_launcher,
            R.mipmap.ic_launcher,
            R.mipmap.ic_launcher,
            R.mipmap.ic_launcher,
            R.mipmap.ic_launcher
    };
    public static String names[] = new String[]{
            "我的朋友",
            "我的好友",
            "我的家人",
            "我的同学",
            "我的兄弟",
            "聊得来"
    };
    public static String counts[] = new String[]{
            "12/23",
            "12/22",
            "14/22",
            "1/3",
            "4/5",
            "4/7"
    };
    public static int child_img[][] = new int[][]{
            {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher},
            {R.mipmap.ic_launcher, R.mipmap.ic_launcher},
            {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher},
            {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher},
            {R.mipmap.ic_launcher},
            {R.mipmap.ic_launcher, R.mipmap.ic_launcher, R.mipmap.ic_launcher}
    };
    public static String child_names[][]=new String[][]{
            {"小菜","小宋","小镇","小红"},
            {"张三","李四"},
            {"王五","赵六","王二"},
            {"小看","小龙","小林","小鸟"},
            {"笑哭"},
            {"小搜","小课","小店"}
    };
    public static String child_contents[][]=new String[][]{
            {"我不是程序员","我不是程序员","我不是程序员","我不是程序员"},
            {"我不是程序员","我不是程序员"},
            {"我不是程序员","我不是程序员","我不是程序员"},
            {"我不是程序员","我不是程序员","我不是程序员","我不是程序员"},
            {"我不是程序员"},
            {"我不是程序员","我不是程序员","我不是程序员"}
    };

这些数据。我是把它写在一个类中了,这样对于我们初学来说方便理解。你也可以写在其它地方。 嘻嘻。。还有我的成员变量用static修饰了,这样做的原因只是可以用类名。来调用。不要static的话你就必须new一个对象出来调用它。。

acitivity_main.xml代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.edu.expandablelistviewsimple.MainActivity">

  <ExpandableListView
      android:groupIndicator="@null"
      android:id="@+id/elv"
      android:layout_width="match_parent"
      android:layout_height="match_parent">
  </ExpandableListView>
</RelativeLayout>

很简单。就是一个ExpandableListView控件。

group_item.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"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/group_img"
        android:src="@mipmap/ic_launcher"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:gravity="center_horizontal"
        android:layout_weight="1"
        android:id="@+id/group_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="我的好友"
        />
    <TextView
        android:id="@+id/group_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1/23"
        />
</LinearLayout>

我们看效果图就知道,父列表就是左边有个图片,中间的文本,右边也是文本。所以我们父列表的item文件就这样定义。。也不难。哈哈。。

child_item.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"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/child_img"
        android:src="@mipmap/ic_launcher"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
    <TextView
        android:layout_marginTop="5dp"
        android:gravity="center_horizontal"
        android:id="@+id/child_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我的好友"
        />
    <TextView
        android:id="@+id/child_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="1/23"
        />
    </LinearLayout>
</LinearLayout>

子列表的item是不是也很简单。一个图片,两个文本。。。

其实这些布局文件都简单。。哈哈。。接下来就是我们怎样把数据绑定到ExpandableListView上中,就需要我们重写BaseExpandableListAdapter适配器。如果不懂可以看看我上次写的适配器的使用。仅需四步就可以教你使用适配器。哈哈
http://blog.csdn.net/song_shui_lin/article/details/52579246

MyAdapter.java代码如下:

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;

/**
 * Created by Administrator on 2016/9/19.
 */
public class MyAdapter extends BaseExpandableListAdapter{

    private Context context;
    private LayoutInflater inflater;

    public MyAdapter(Context context) {
        this.context = context;
        inflater = LayoutInflater.from(context);
    }


    @Override
    public int getGroupCount() {
        return ContentUtil.mImgs.length;
    }

    @Override
    public int getChildrenCount(int groupPosition) {
        return ContentUtil.child_img[groupPosition].length;
    }

    @Override
    public Object getGroup(int groupPosition) {
        return ContentUtil.mImgs[groupPosition];
    }

    @Override
    public Object getChild(int groupPosition, int childPosition) {
        return ContentUtil.child_img[groupPosition][childPosition];
    }

    @Override
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        GroupViewHodler holder = null;
        View view = null;
        if (convertView != null) {
            view = convertView;
            holder = (GroupViewHodler) view.getTag();
        } else {
            holder = new GroupViewHodler();
            view = inflater.inflate(R.layout.group_item, null);
            view.setTag(holder);
            holder.group_img = (ImageView) view.findViewById(R.id.group_img);
            holder.group_name = (TextView) view.findViewById(R.id.group_name);
            holder.group_count = (TextView) view.findViewById(R.id.group_content);
        }
        holder.group_img.setImageResource(ContentUtil.mImgs[groupPosition]);
        holder.group_name.setText(ContentUtil.names[groupPosition]);
        holder.group_count.setText(ContentUtil.counts[groupPosition]);
        return view;
    }

    @Override
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
        ChildViewHodler hodler = null;
        View view = null;
        if (convertView != null) {
            view = convertView;
            hodler = (ChildViewHodler) view.getTag();
        } else {
            hodler = new ChildViewHodler();
            view = inflater.inflate(R.layout.child_item, null);
            view.setTag(hodler);
            hodler.child_img = (ImageView) view.findViewById(R.id.child_img);
            hodler.child_name = (TextView) view.findViewById(R.id.child_name);
            hodler.child_content = (TextView) view.findViewById(R.id.child_content);
        }
        hodler.child_img.setImageResource(ContentUtil.child_img[groupPosition][childPosition]);
        hodler.child_name.setText(ContentUtil.child_names[groupPosition][childPosition]);
        hodler.child_content.setText(ContentUtil.child_contents[groupPosition][childPosition]);
        return view;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }

}

class GroupViewHodler {
    ImageView group_img;
    TextView group_name, group_count;

}
class ChildViewHodler {
    ImageView child_img;
    TextView child_name, child_content;
}

上面的方法,我就是一个方法不太懂,其他的方法基本上可以做到看其名知其意。
1,hasStableIds()

这里写图片描述

然后我又去看官网api对它的解释:

这里写图片描述

还 是官网的解释准确。哈哈。。写完了适配器后,基本上我们的基本的差不多做完了。只剩下最后一步了。

MainActivity.java

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.ExpandableListView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
 private ExpandableListView  mElv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initWidgets();
        MyAdapter adapter=new MyAdapter(MainActivity.this);
        mElv.setAdapter(adapter);
        //为子列表添加点击事件
   mElv.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
       @Override
       public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {

           Toast.makeText(MainActivity.this,ContentUtil.child_names[groupPosition][childPosition],Toast.LENGTH_SHORT).show();
           return true;
       }
   });
    }
   //初始化控件
    private void initWidgets() {
        mElv= (ExpandableListView) findViewById(R.id.elv);
    }
}

总结:

其实我跟大家一样,好水。写博客只是记录下自己平常学的点点滴滴。加油!!!还有不懂的看官网的解释是最好的。

源码下载:

https://github.com/songshuilin/AndroidForBlog/tree/master/expandablelistviewsimple

作者:song_shui_lin 发表于2016/9/20 13:34:32 原文链接
阅读:217 评论:0 查看评论

到处都在说直播连麦技术,它们真的能连吗?

$
0
0

直播火了。连麦直播在火的路上。

那么,这些连麦技术方案,真的能连吗?本文将常见的,不常见的直播技术方案进行了比较,各位同学自己甄别。

首先,基础知识普及,技术上直播的流程是什么?

一、直播的流程

这里写图片描述

正如上图所示,整个直播流程分为以下几个关键步骤:
1、主播客户端,将本地采集的视频推送到CDN;
2、CDN对视频流进行缓存以及转发;
3、观众客户端,拉取CDN中缓存视频流进行播放;

可以看到CDN在这里起到了关键的作用,2016也是一个CDN崛起的年代,网宿、快网、七牛、高升、蓝汛、观止云、腾讯云、百度云、阿里云等CDN纷纷表示对直播进行了支持,直播也逐渐成为了CDN的标配。

那么接下来了解一下CDN的技术原理。

二、CDN技术原理

CDN的全称为Content Delivery Network,即内容分发网络,是一个策略性部署的整体系统,主要用来解决由于网络带宽小、用户访问量大、网点分布不均匀等导致用户访问网站速度慢的问题。

这里写图片描述

CDN的技术原理见上图,具体实现是通过在现有的网络中,增加一层新的网络架构,将网站的内容发布到离用户最近的网络节点上,这样用户可以就近获取所需的内容,解决之前网络拥塞、访问延迟高的问题,提高用户体验。

对于直播来说,则将Web服务器换作主播客户端,如下图所示。
这里写图片描述
由于视频占用带宽较大,与普通的Web服务差别较大,这样CDN的优势更能体现出来:网络拥塞减少,访问延迟降低,带宽得到良好的控制等等。

另外,CDN直播中常用的流媒体协议包括RTMP,HLS,HTTP FLV等。

  • RTMP(Real Time Messaging Protocol)是基于TCP的,由Adobe公司为Flash播放器和服务器之间音频、视频传输开发的开放协议。
  • HLS(HTTP Live Streaming)是基于HTTP的,是Apple公司开放的音视频传输协议。
  • HTTP FLV则是将RTMP封装在HTTP协议之上的,可以更好的穿透防火墙等。

三、CDN的常用架构

CDN架构设计比较复杂。不同的CDN厂商,也在对其架构进行不断的优化,所以架构不能统一而论。这里只是对一些基本的架构进行简单的剖析。

CDN主要包含:源站、缓存服务器、智能DNS、客户端等几个主要组成部分。

源站:是指发布内容的原始站点。添加、删除和更改网站的文件,都是在源站上进行的;另外缓存服务器所抓取的对象也全部来自于源站。对于直播来说,源站为主播客户端。

缓存服务器:是直接提供给用户访问的站点资源,由一台或数台服务器组成;当用户发起访问时,他的访问请求被智能DNS定位到离他较近的缓存服务器。如果用户所请求的内容刚好在缓存里面,则直接把内容返还给用户;如果访问所需的内容没有被缓存,则缓存服务器向邻近的缓存服务器或直接向源站抓取内容,然后再返还给用户。

智能DNS:是整个CDN技术的核心,它主要根据用户的来源,以及当前缓存服务器的负载情况等,将其访问请求指向离用户比较近且负载较小的缓存服务器。通过智能DNS解析,让用户访问同服务商下、负载较小的服务器,可以消除网络访问慢的问题,达到加速作用。

客户端:即发起访问的普通用户。对于直播来说,就是观众客户端。

对于直播来说,CDN整体架构如下图:
这里写图片描述

主要流程为:

  1. 主播开始进行直播,向智能DNS发送解析请求;
  2. 智能DNS返回最优CDN节点IP地址;
  3. 主播端采集音视频数据,发送给CDN节点,CDN节点进行缓存等处理;
  4. 观众端要观看此主播的视频,向智能DNS发送解析请求;
  5. 智能DNS返回最优CDN节点IP地址;
  6. 观众端向CDN节点请求音视频数据;
  7. CDN节点同步其他节点的音视频数据;
  8. CDN节点将音视频数据发送给观众端;

四、CDN的短板

大概了解了CDN的技术原理后,我们在做直播选型时,还需要了解一个方案优缺点。接下来,我们来分析一下CDN的短板。

4.1 短板:播放延时

连麦直播的难题主要是播放延时!播放延时从何而来?

4.1.1 网络延时

网络延时这里指的是从主播端采集,到观众端播放,这之间的时间差。这里不考虑主播段采集对视频进行编码的时间,以及观众端观看对视频进行解码的时间,仅考虑网络传输中的延时。例如说下图中的网络延时:
这里写图片描述

另外,数据传输过程中还涉及到逻辑上的交互,例如包的重传以及确认,以及缓存上的一些逻辑等,会在这个基础上又增加很多。

那么来简单估算一下大概的网络延时。众所周知,光在真空中的速度约为300,000km/s,而在其他介质中光 速会大大降低,所以在普通光纤中,工程上一般认为传输速度是200,000km/s。从现实上来说,可以参考如下:
这里写图片描述

所以说,在节点较少、网络情况较好的情况下,那么网络延时对应也是最小,加上一定的缓存,可以控制延时在1s~2s左右。但是节点多、网络差的情况下,网络延时会对应增大,经验来说延时可以达到15s以上。

4.1.2 网络抖动

网络抖动,是指数据包的到达顺序、间隔和发出时不一致。比如说,发送100个数据包,每个包间隔1s发出。结果第27个包在传输过程中遇到网络拥塞,造成包27不是紧跟着26到达的,而是延迟到87后面才达。在直播中,这种抖动的效果实际上跟丢包是一样的。因为你不能依照接收顺序把内容播放出来,否则会造成失真。

网络抖动,会造成播放延时对应增大。如果网络中抖动较大,会造成播放卡顿等现象。

这里写图片描述

如上图所示,主播端t3和t5发出的包,分别在t3’和t5’到达,但是中间延时增大,即发生了网络抖动。这样造成观众端观看视频的延时会不断增大。

4.1.3 网络丢包

CDN直播中用到的RTMP、HLS、HTTP FLV等协议都是在TCP的基础之上。TCP一个很重要的特性是可靠性,即不会发生数据丢失的问题。为了保证可靠性,TCP在传输过程中有3次握手,见下图。首先客户端会向服务端发送连接请求,服务端同意后,客户端会确认这次连接。这就是3次握手。接着,客户端就开始发送数据,每次发送一批数据,得到服务端的“收到“确认后,继续发送下一批。TCP为了保证传到,会有自动重传机制。如果传输中发生了丢包,没有收到对端发出的“收到”信号,那么就会自动重传丢失的包,一直到超时。

这里写图片描述

由于互联网的网络状况是变化的,以及主播端的网络状况是无法控制的。所以当网络中丢包率开始升高时,重传会导致延时会不断增大,甚至导致不断尝试重连等情况,这样不能有效的缓存,严重情况下会导致观众端视频无法观看。

4.2 短板:连麦

直播中,主播如果要与用户交互,常见有两种方式:

第一种方式:文字,这种比较常见,实现也比较简单,这里不再进行分析;
第二种方式:连麦,这样主播可以面对面与观众进行交互,增加了互动性;
由于连麦方式比较复杂,这里进行详细分析。

4.2.1 多路RTMP流实现

前面提到,RTMP是目前主播中最常用的协议,使用RTMP协议,可以实现最简单的一种连麦方式,如下图。

这里写图片描述

当有连麦者时,则主播端和连麦者端,都分别推一路RTMP流到CDN,CDN再将这两路RTMP流发送给观众端,观众端将两路RTMP流合成为一个画面。这种方式的优缺点如下:

  • 优点
    • 实现简单;
  • 缺点
    • 主播与连麦者如果要进行交互,考虑到上面分析的延时问题,在这里延时需要至少加大一倍。这样对于实时交互来说,完全无法接受;
    • 主播与连麦者交互时,声音会产生干扰,形成回音;
    • 观众端要接收两条视频流,带宽、流量消耗过大,并且两路视频流解码播放,耗费CPU等资源也非常多;
    • 这样看来,这种方式弊大于利,基本不可取。

4.2.2 主播端与连麦者P2P

第二种方式,是主播端与连麦者之间使用P2P方式进行交互,然后主播端将自己和连麦者的视频进行合并,再推到CDN上,CDN再发送给观众端,如下图:
这里写图片描述

这种方式的优缺点如下:

  • 优点
    • 主播和连麦者之间使用P2P,网络质量较好,延迟较小,保证了两者之间交互不会有非常大的延时;
    • 解决声音的干扰问题,消除回声;
  • 缺点
    • P2P在某些网络下无法穿透,有些观众根本无法与主播端进行交互;
    • 主播端需要上传两路视频:一路P2P与连麦者进行交互,一路使用RTMP推到CDN。还要下载一路视频:连麦者P2P发送过来的交互数据。所以主播端要求带宽需要较高,网络较差时无法进行主播
    • 主播端要进行多路视频的编码、解码,要求主播端设备配置比较高,较差的设备也无法进行主播;
    • 只能支持一个连麦者,不能支持多个连麦者;
    • 由于主播端和连麦者经过CDN合并成一路,因此,不能实现主播端和连麦者视频大小窗口切换。

综合来说,P2P方式在一定程度上可以解决连麦的问题。

4.2.3 服务器端合图

另外一种方式,是主播和连麦者都将视频推送到CDN中,然后CDN内部对这几路视频进行合图,再将其发送给观众端。如下图:

这里写图片描述

这种方式的优缺点如下:

  • 优点
    • 主播和连麦者各路视频都使用RTMP推送到CDN,可以保证延时较小;
    • 由于CDN进行视频合图和发送,所以主播不需要很高的带宽;
    • 由于CDN进行视频合图,所以主播的设备不需要配置非常高;
    • 没有声音干扰问题;
    • 可以支持多个连麦者连麦;
  • 缺点
    • CDN需要进行视频的合图,需要额外开发工作,并且逻辑比较复杂;
    • CDN需要进行视频的合图,需要消耗较高服务器资源;
    • CDN合图后的布局难控制;
    • 据目前所知,还没有CDN支持这种方案;

五、基于SD-RTN的解决方案

声网Agora.io,在开发互动直播解决方案时,抛弃传统的基于TCP协议的CDN方案,从底层协议和布网上开始,创建了基于UDP协议的SD-RTN方案。

(一)什么是SD-RTN

SD-RTN(Software-Defined Real Time Net work),软件定义实时传输网络,是一种新型的专为内容实时传输而设计的网络架构。通过在互联网上不同地区的数据中心放置软件组网单元,相互连接互相调度,在现有的公共互联网基础上构建一层新的虚拟网络。SD-RTN系统能够实时根据各节点的连接和传输状况、负载状况以及到用户的距离和响应时间,自动分配最优、最通畅的传输路径,达到实时传输需要的质量保障级别。

(二)SD-RTN与CDN有何不同

  • 基本原理不同。CDN是存储转发结构,设计目的是在各个边缘节点缓存待分发内容,结构上从源站到观众是伞状多级缓存放大方式。SD-RTN本质上一个实时传输网络,用户的数据在网络单元内部和传输线路上都以实时交换方式传送,从而能够保证最低延迟。

  • 底层协议不同。SD-RTN采用了专为实时传输设计的UDP协议,避免了采用TCP的延时不可控缺点。能够大大缩短交互延时,延时可从CDN方案的数秒,降低到数百毫秒。

  • 内容分发机制不同。SD-RTN是基于自定义路由,选择最优传输路径,直接将内容端到端传输,数据在网络单元中从不缓存,从而最大可能的降低延迟,同时内容安全性也更好。CDN是将内容缓存于缓存服务器中,再将内容就近下发。

  • 使用场景不同。SD-RTN适用于要求极低时延的实时互动场景,例如网络电话、视频会议、有主播与观众交互需求的互动直播等。CDN适用于对时延要求不高的场景,例如对延时要求不高、类似电视的单点直播、网站加速等。若硬要将CDN改造用于互动直播,那么其结构上对降低延迟的不适应性,始终会成为质量改进需求的瓶颈。

(三)SD-RTN相较CDN,有何优点

1、时延大大缩短。

直播延时可从CDN方案的数秒,降低到数百毫秒。这一延迟范围,属于实时通信或准实时通信延迟的范畴。在这一级别上,主播和观众可以基本重现在现场活动中的交互体验,从而大大释放了内容制作者的潜力,也为业务运营者创造新业务形式打开了无限的空间和可能。

比如,在这一延迟下,主播和观众可以不光通过文字交互,也可以通过音频实时交互,而不会感到延迟过大而不自然。这种交互体验,在手机上也更自然,比打字更符合人的自然习惯。业务运营方当然可以把这一功能当作比文字互动更高级别的特权能力,仅仅对于付费或是一定级别、身份的用户才可以直接和主播语音互动。业务运营者也可以利用此类功能创造类似课堂,或小剧场的现场互动氛围,让主播可以听得到观众的发问,或是掌声、叹息,甚至嘘声,实现自然的台上台下交互和有沉浸感的互动直播体验。加上辅助功能,体验上可以任意规定谁可以发声,谁不可以,这中间的可能性是无限的。

更重要的是,即便在一般的连麦直播场景,这样的体验也可以帮助这类低延迟观众(我们称为“近场观众”)在上麦互动的时候实现平滑体验,不用每次切换就黑屏一次,好像节目中断一样。

对于近场观众,即便是在网络较差的时候,基本上能够保证延迟不超过1秒,极少数观众延迟不超过2秒。相对于CDN,即便在网络质量无问题时,也有3秒以上延迟。实测网络丢包仅仅10%,就可以让延迟拉大到10秒。这样的丢包率,在手机的无线信号下可是经常出现的。

所有这些,都要归公于声网SD-RTN的实时传输保障能力。UDP实现的传输协议,不会因为前一个包的丢失或延迟导致下后续包的延迟送达,而丢包可以用对延迟更友好的方式修复或补偿出来。不采用这个机制是无法达到这样的延迟保障效果的。

2、抗丢包能力强。

使用声网的技术,30%丢包时,依然能够进行正常直播。而基于TCP的CDN直播方案在丢包2%时就明显卡顿,达到30%经常已断开连接。

(三)基于SD-RTN的直播架构与特性

下图是声网Agora.io互动直播的架构图
这里写图片描述

客户端均通过UDP连接SD-RTN(Agora Global Network),通过SD-RTN的就近接入策略,让使用者就近接入质量最好的数据节点,通过Agora Global Network的软件定义优化路由,经过传输延迟和质量优化的最优路径,自动避免网络拥塞,并规避骨干网络故障的影响。

若有常规的长延迟旁路直播需求,则可以将主播与连麦者合成一路直播流,通过RTMP推到CDN,进行下发。连接这一路的观众,不能参与连麦互动(称为“远场观众”)。

主要特点如下:

1、可以支持更多的主播交互,目前支持7人视频交互,100人语音交互。
2、当有观众连麦时,其他观众端收到的多路视频,观众端可以动态选择布局;
3、声网Agora.io会将直播视频推送到CDN,其他观众(网页端等)可以直接观看;
4、当有观众连麦时,声网Agora.io会将视频合图后推送到CDN,其他观众(网页端等)可以观看到连麦者与主播的互动;
5、在经过RTMP推流前的观众端,可以进行大小流切换,自主选择视频大小窗口的切换。

【本文作者】

单辉 声网Agora.io 高级开发工程师

作者:agora_cloud 发表于2016/9/20 14:31:11 原文链接
阅读:208 评论:0 查看评论

Android App架构设计

$
0
0

前言

Web的架构经过多年的发展已经非常成熟了,我们常用的SSM,SSH等等,架构都非常标准。个人认为,Web服务逻辑比较清晰,目的明确,流程也相对固定,从服务器收到请求开始,经过一系列的的拦截器,过滤器->被转发到控制器手中->控制器再调用服务->服务再调用DAO获取想要的数据->最后把数据返回给web层。哪怕中间增加一些东西,如缓存什么的。他的模型依然是以用户请求的线程为生命周期,经过一个个切面(层)的结构,感觉类似于流水线的结构吧。
这里写图片描述
而Android App则有所不同,他没有像用户请求这样一个统一的出发点,最接近的可能是来自于UI的事件,然而远不仅仅于此。根据app不同的需求,其结构也会千差万别,所以很难有较为统一的架构。
但是客户端类app确实是较为常见的App类型,其结构还是有迹可循的。

常见的架构

一.MVC
mvc现在是用的人最多,同时也是Android官方的设计模式,可以说Android App原本就是MVC的,View对应布局文件xml,Controller对应Activity,Model对应数据模型。
这里写图片描述
这类App一般会定义一个BaseActivity,BaseActivity内部实现了网络的异步请求,本地数据的存储加,数据库访问载等复用性较强的逻辑。逻辑控制则在对应的Activity中实现。
MVC的缺点:Activity过于臃肿,往往一个Activity几百上千行代码。View层的XML控制力其实非常弱,众多的View处理还是要放在Activity进行,这样的话,Activity就既包含了View又包含了Controller,耦合高,不利于测试扩展,可读性也变差。


二.MVVM
用过VS开发过.net的人肯定知道MVVM的强大之处,仅需要点点鼠标,数据库里的信息和View的控件显示就被简单的绑定了。
这里写图片描述
而Android的数据绑定个人认为还是不够成熟的,用法长这样android:text=”@{user.username}”/>
在xml里面配置数据模型。一是控制力不够,二是部分逻辑需要放到数据Model里处理。


三.MVP
最近在Android上应用比较火的模式。相较于MVC,MVP将Activity中的业务逻辑抽取出来,将Activity,Fragment等作为View层,专职与界面交互。而Presenter则负责数据Model的填充和View层的显示。View不直接与Model交互,解耦了Actiity。
这里写图片描述
这样可以做到逻辑和界面交互的完全分离,方便测试,界面升级等。代码的可读性也大大增加。

个人的设计

对于我自己,在我自己架构项目的时候确实遇到了一些困难,也有选择障碍,经过一番思考。我有了自己的见解,总体还是偏向于MVP,但又有些不同。可能是MVP+MVVM(伪)吧。

首先是包结构
这里写图片描述
1.View层
view层按照Android组件的分类,可以使用接口通信,也可以使用类似EventBus的事件框架进行通信
Action包就是事件实体,这里使用的是我自己实现的事件框架。
这里写图片描述


2.Model层
model层包含数据模型,实体类,以及dao,http等数据获取得代码。回掉的话可以使用接口,也可以使用事件框架。另外缓存也放在这里。
这里写图片描述


3.Presenter
包含Base(自己实现的Presenter框架,其实就是将Activity抽取了一层),Service包是Android组件Service
Impl是业务的实现,下面一堆I开头的是业务接口。
这里写图片描述
这里讲一下Base,Base相当于控制器,拿登陆来举例,一个登陆操作可能涉及多个界面,多个业务逻辑单元。比如登陆,首先是请求网络的逻辑,除此之外,登陆成功后,需要对会话,用户基础信息等进行持久化;还有控制器需要控制各个界面的刷新。这些都是在控制器Base中完成的,他是一组逻辑的控制单元,负责一个典型的业务,比如说登陆。


下面的重点是业务接口,例如登陆ILogin,如果自己实现,你需要写一大堆的东西,网络请求,异步处理,Handler,异常处理,JSON解析,显示,洋洋洒洒至少上百行了。如果你的项目需要快速上线怎么办?同时你又想保持项目的逻辑结构,以后做更细致的改进,这时候就体现了接口的重要性。这里安利我一个比较快速的实现链接也是我实现的一个小框架。
用起来画风是这样的。

public class LoginPresenter extends Presenter implements ActivityOnCreatedListener,ICallBack<User,Throwable>{

    private ILogin ILogin;

    @Override
    protected void onContextChanged(ContextChangeEvent event) {

    }

    @Override
    public void OnPresentInited(Context context) {
        ILogin = HttpProxyFactory.With(ILogin.class).setCallBack(this).setViewContent(getActivityRaw()).establish();
        getActivityInter().setOnCreateListener(this);
    }

    @Override
    public void ActivityOnCreated(Bundle savedInstanceState, final Activity activity) {
        getActivityInter().getView(R.id.btn_login)
                          .setOnClickListener(new View.OnClickListener() {
                              @Override
                              public void onClick(View v) {
                                  LoginActivity ac = (LoginActivity) activity;
                                  ac.progressDialog.show();
                                  getActivityInter().getView(R.id.btn_login).setClickable(false);
                                  EditText name = getActivityInter().getView(R.id.login_name);
                                  EditText pass = getActivityInter().getView(R.id.login_pass);
                                  ILogin.login(name.getText().toString(),pass.getText().toString());
                              }
                          });
    }


    @Override
    public void onSuccess(User user) {
        Log.e("gy",user.toString());
        getActivityRaw().finish();
        navTo(HomeActivity.class);
    }

    @Override
    public void onFailed(Throwable throwable) {
        ILoginCallBack callBack = (ILoginCallBack) getContext();
        callBack.onLogFailed(throwable.getMessage());
    }
}

你可以发现ILogin = HttpProxyFactory.With(ILogin.class).setCallBack(this).setViewContent(getActivityRaw()).establish();
这么简单,你的ILogin业务接口就被框架实现了,简单的说就是用了动态代理,框架根据你在接口上绑定的注解信息,帮你动态代理处一个业务实现对象。帮你包办了网络请求,异步处理,异步回掉,异常处理,JSON解析,显示等一大堆操作。
如何绑定你的需求?

ILogin接口张这样的

public interface ILogin {
    @HttpSrcMethod(url = "/store/login",session = Global.SKEY_UNLOGIN,filters = ResultFilter.class)
    public User login(@Param("tel")String name,@Param("password")String passwd);
}

返回值模型Model User比较简单,我们换一个比较典型的

@JsonOrm
public class ResultArea implements IHandler{

    @JsonString("name")
    private String name;
    @JsonString("id")
    private String id;
    @BindListView(CityPickerActivity.ListViewId)
    @JsonSet(name = "areas",clazz = Area.class)
    private List<Area> child;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public List<Area> getChild() {
        return child;
    }

    public void setChild(List<Area> child) {
        this.child = child;
    }

    @Override
    public void handler() throws Exception {
        if (child == null)
            child = new ArrayList<>();
        child.add(0,new Area(name,id));
    }
}

注解几乎映射了你所有的业务接口协议,包括请求参数,URL,头,返回值的json映射,对应View层的视图显示等等。

使用这种临时解决方案之后,后期如有需求,自己再选用其它框架,或者自己实现业务接口即可,根本不需要动其他模块,从而保证了可扩展性。

作者:ganyao939543405 发表于2016/9/20 14:35:17 原文链接
阅读:251 评论:0 查看评论

Qt之QScintilla(源代码编辑器)

$
0
0

简述

QScintilla 是 Scintilla 在 Qt 上的移植,Scintilla 是一个免费的源代码编辑控件。它完全开放源代码,功能强大,包括:代码高亮、代码补全、代码折叠、自动缩进、代码提示等。支持非常多的语言,可以轻松实现显示断点,显示运行行等,定义各种样式都很轻松方便。著名的开源编辑器 SciTE 就是 Scintilla 开发者开发的。

下载

进入 QScintilla Download ,你会发现 Windows、Linux / OS X 的源码包:

这里写图片描述

下载对应的源码包,我选择的是:QScintilla_gpl-2.9.3.zip(Windows source)

构建和安装

命令行

解压缩之后,在 /doc/html-Qt4Qt5/index.html 中可以找到安装指南,根据提示安装即可。

要构建和安装 QScintilla,运行:

cd Qt4Qt5
qmake qscintilla.pro
make
make install

如果你安装了多个版本的 Qt,确保使用 qmake 的正确版本。

  • 在Windows上安装

在编译 QScintilla之前,应该删除任何以前安装包含 QScintilla 头文件的 Qsci 目录,这是因为 qmake 生成的 Makefile 文件会发现这些旧的头文件,而不是新的。

根据使用的不同编译器,可能需要使用 nmake 来代替 make。

如果你建立了一个 Windows DLL,那么你可能还需要运行:

copy %QTDIR%\lib\qscintilla2.dll %QTDIR%\bin

Creator

环境:Qt 5.5.1 + MSVC 2013

当然,如果不想用命令行,也可以直接打开 qscintilla.pro 编译。编译完成之后会生成 qscintilla2.dll 和 qscintilla2.lib。

这里写图片描述

使用

如果要测试,我们直接可以打开 /example-Qt4Qt5 中的示例。

其中,最主要的类是 QsciScintilla。使用时,需要在 .pro 文件中添加:

ROOT = E:/GitHub/QScintilla_gpl-2.9.3

LIBS += -L$${ROOT}/lib -lqscintilla2
INCLUDEPATH += $${ROOT}/Qt4Qt5

如果你要开发一款 IDE,不妨试试 TA O(∩_∩)O哈哈~

更多参考

作者:u011012932 发表于2016/9/20 14:41:42 原文链接
阅读:202 评论:0 查看评论

百万并发量苹果官网准备好了吗?——一分钟学会服务器压力测试

$
0
0

作者:Oliver,腾讯服务器性能测试团队产品经理
商业转载请联系腾讯WeTest授权,非商业转载请注明出处。

目前腾讯WeTest服务器性能测试已经正式对外开放,点击链接:http://wetest.qq.com/gaps/立即体验!

WeTest导读

企业需要良好的网站性能。网站的访问速度和顺利的体验是企业必须要做好的事情。本文从苹果官网两年来每次预购都出现的服务器宕机情况,揭示服务器性能测试的重要性,手把手指导Web压测的高效方法。

北京时间9月8日凌晨1点,苹果正式举办2016年秋季新品发布会,iPhone 7终于千呼万唤始出来,简单总结它的新特点如下:
1、 更快
A10处理器比A9快40%,GPU性能提升50%。
2、 更炫
手机颜色为金色、银色、玫瑰金、新增亚光黑和亮光黑。
3、 更清晰
前置摄像头升级为700万像素,后置1200万像素,支持自动防抖。4个闪光灯。新的视网膜显示屏,亮度提高25%。
4、 更洒脱
取消3.5mm耳机插孔,进入无线耳机时代,加入IP67防水,用起来更放心。
5、 更大
放弃16G,直接从32G开卖,拥有更大容量

不过随着智能手机的竞争进入了成熟期,相比于之前发布会的火爆场面,iPhone 7/ 7plus的发布已经弱了很多,三星、索尼、华为、魅族等安卓手机的大力推广已经抢占了许多市场,不过话说回来,苹果永远是苹果,苹果一发布新品,大家不管买不买,总是要看看的。。。
这里写图片描述

看看不要紧,可是当几千万的用户同时都这么想的时候,问题就没那么简单了。。。
来看看2014年的iPhone 6预购的情况:
这里写图片描述

2014年9月12日下午三点,香港各个公司的办公平台都在不断的刷新苹果官网,当天苹果官网无法承载用户压力导致无法访问,网页通过多国文字显示“我们将很快恢复服务”。

时过一年,同样是9月12日,距离iPhone 6s开始预约不到两小时,尝试打开苹果官网浏览,结果显示无法访问。不光是苹果中国官网,美国以及中国香港、中国台湾等地均出现了类似故障。
这里写图片描述
这里写图片描述

让人觉得有趣的是,每次人们在看低苹果新品前景的时候,苹果都会通过这样的方式让人意识到苹果依然如此受到万众瞩目。不过,有趣归有趣,出现这样的问题是一定会影响到苹果的市场发展和后市股价的,那么苹果是如何部署他的官网服务器的呢?

苹果采用的方案是与全球首屈一指的CDN服务商Akamai进行合作,什么是CDN呢?就是内容分发网络的意思(Content Delivery Network),在数据传输的过程中尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。当我们在不同的时区和地区打开同样域名的网站时,我们所调用的并不会是同一个服务器,而是优化最好的一个,通常会是离我们最近的一个。所幸的是,苹果官网及时修复了问题,在预定开放之前重新开放了网站。

不过我们可以从中发现,企业需要良好的网站性能。网站的访问速度是企业必须要做好的事情。谷歌和一些网站的研究表明,用户们只愿意访问那些打开速度最快、性能最好的网站。一个网站每慢一秒钟,就会丢失许多访客,甚至其中很多访客永远不会再次光顾这个网站,在这里访问速度完全可能成为木桶理论中最短的那一块。对于移动访问和APP应用来说,也是同理。

对于众多企业来说,像苹果官网这样正式上线之后来一次两小时“过山车”般的宕机体验实在过于刺激,为了不让企业出现损失,一定要在上线之前对自己的网站承载能力进行一个测试。如果自己没有服务器,没有人力,没有钱,都没有关系。。。
这里写图片描述

腾讯提供了一个可以自主进行服务器性能测试的环境,用户只需要填写域名和简单的几个参数就可以获知自己的服务器性能情况。那么具体如何使用呢?
1、 进入腾讯WeTest官网,http://wetest.qq.com/
2、 在“产品——性能测试——服务器性能测试”找到“服务器性能测试”
这里写图片描述

3、 进入页面后,如果第一次使用,点击“创建新产品”,填写项目信息,点击“提交”后,项目生成成功!
这里写图片描述

4、 点击开始测试,进入项目
这里写图片描述

5、 首先点击压测产品首页中的快捷入口:HTTP直压。模式选择简单模式,名称和描述可以自己填写。(图中示例起始人数5人,每隔30秒增加5人,加到10人为上限)
这里写图片描述
这里写图片描述

6、新建一个客户端请求,方法选择GET,填写想要测试的URL,URL变量和Header变量这里可以暂且不填。(注:填写Header信息或修改参数化变量可以满足更高要求的测试场景,具体可以查看更一步的帮助,在本篇中不作展开)
这里写图片描述
这里写图片描述

7、 编辑一下测试模型,增加一个场景名,本篇暂时只介绍一个首页场景,所以暂时把所有100%的压力都放在该场景上。
这里写图片描述
这里写图片描述

8、 如果测试的不是自己的服务器,那就无法去服务器上部署性能观测工具监测CPU,内存等性能情况,就可以不用填。
这里写图片描述

9、 可以选择“保存设置”,您也可以选择‘立即执行’这个测试,测试会马上进入排队系统,如果压力源系统内有空闲资源将马上为您执行测试
这里写图片描述
这里写图片描述

10、 随着时间的移动,测试报告数据会发生变化,用户可以看到网站数据的实时变化
这里写图片描述

腾讯WeTest正是运用了沉淀十多年的内部实践经验总结,通过基于真实业务场景和用户行为进行压力测试,帮助游戏开发者发现服务器端的性能瓶颈,进行针对性的性能调优,降低服务器采购和维护成本,提高用户留存和转化率。

目前腾讯WeTest服务器性能测试已经正式对外开放:

体验地址:http://wetest.qq.com/gaps/

如何使用简单模式:http://wetest.qq.com/help/documentation/10094.html

如何分析报告:http://wetest.qq.com/help/documentation/10099.html

常用测试指标:http://wetest.qq.com/help/documentation/10098.html

最后,祝愿所以企业的官网都可以用最好的用户体验出现在人们面前。

这里写图片描述

参考文章:
人民网,http://finance.people.com.cn/n/2014/0914/c1004-25657728.html
TechWeb,http://mi.techweb.com.cn/tmt/2015-09-12/2201254.shtml
从苹果官网瘫痪一事浅谈CDN,百度百家,http://itobserve.baijia.baidu.com/article/164999
CDN,百度百科,
http://baike.baidu.com/link?url=eMIttmYqJ065Nsh8bbb0txxkvqTqIvGcd0xBIvvnWczQ6xwjE3Aokl5MrB8KbLq0P5ZHWhJIV7PViJMcMGmgL_

作者:wetest_tencent 发表于2016/9/20 15:19:40 原文链接
阅读:249 评论:0 查看评论

IPC之AIDL(2)in out inout

$
0
0

内容大纲:

1.在AIDL的时候正确使用in out inout

上一篇我们用AIDL简单实现了一个IPC,其中我们谈到在定义aidl接口中的除基本类型和AIDL接口外的参数要调价修饰符in out 或 inout中的一种,本文将帮助大家理解in out inout,并让读者可以正确的使用in out inout。
在介绍in out inout的区别之前我们先明确两个基本概念:起点 和 终点,起点指调用方,终点指响应方,比如我在客户端调用aidl接口那么客户端就是起点 服务端就是重点,在一次调用中服务端如果要调用一个aidl接口回调给客户端,那么服务端就是起点,客户端就是重点。
然后我们来定义in out inout:
in : 将对象从起点传递给终点,在终点部分中对对象的修改不会反映到起点,即只输入
out : 对象中的值不会传递给终点,但是在终点部分对对象的修改会反映到起点,即只输出
intout : 将对象从起点传递给终点,在终点部分的修改会反应到起点,即输入输出都有影响

我们再用一个具体的例子来看一下这个区分:
我们来看下客户端的代码:

        mServiceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                mBookManager = IBookManager.Stub.asInterface(service);
                try {
                    mBookManager.registerListener(new IBookListener.Stub() {
                        @Override
                        public void onBookAdd(Book book) throws RemoteException {
                            Log.i("wlh " , "addBook callback : " + book.name);
                        }
                    });
                    Book book = new Book();
                    book.name = "TestBook";
                    mBookManager.addBook(book);
                    Log.i("wlh",  book.name);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                unbindService(mServiceConnection);
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
        };

我们输出了callback 调用addBook 和调用后book对象的名称。我们再来看下服务端的代码:


    private IBinder mBinder = new IBookManager.Stub() {
        @Override
        public List getBookList() throws RemoteException {
            Log.i("wlh", "getBookList");
            return null;
        }



        @Override
        public void addBook(Book book) throws RemoteException {
            Log.i("wlh", "addBook : " + book.name);
            int N = mCallbacks.beginBroadcast();
            book.name += " : server";
            for (int i = 0; i 《 N; i++ ) {
                mCallbacks.getBroadcastItem(i).onBookAdd(book);
            }
            mCallbacks.finishBroadcast();

        }


        @Override
        public void registerListener(IBookListener listener) throws RemoteException {
            mCallbacks.register(listener);
        }

        @Override
        public void unRegisterListener(IBookListener listener) throws RemoteException {
            mCallbacks.unregister(listener);
        }
    };
    

观察addBook方法 我们在客户端传过来的book中修改了name字段 追加了一个:server,然后我们输出了客户端传递过来的book名称。现在我们来看在book的修饰符分别为in out 和inout时候的系统输出。(我们假设BookListener中的参数都是in,其实BookListener中的in out inout修饰就是起点 和 终点的转换,这个时候起点是调用方服务端 终点是响应方客户端,具体的读者可自行分析)

in:
我们可以先猜测一下输出,in表示数据会传递到终点,那么服务端会输出TestBook,然后由于对对象的修改不会反映到起点,所以对象不会反映到客户端,客户端依然输出的是TestBook,而callback中的回调是TestBook : server,我们来看下实际效果:

out:
out不会将数据传递到终点在这里也就是服务端,那么服务端会输出null, 然后由于对象修改会反应到起点也就是客户端,所以客户端会输出 null : server,我们来看下实际效果:

inout:
inout即会把数据传递到终点,起点也会响应终点的变化,那么服务端会输出 TestBook
然后客户端输出 TestBook : server, 我们来看下实际效果:

欢迎关注公众号:CoderHouse
这里写图片描述

作者:wei7017406 发表于2016/9/20 17:08:41 原文链接
阅读:154 评论:0 查看评论

Android Studio 2.2 来啦

$
0
0

今年的 I/O 2016 Google 放出了 Android Studio 2.2 的预览版,透露改进了多项功能,只不过为了保证公司项目不受影响,我一般都不安装预览版的,因为预览版意味着不稳定,可能遇到各种意想不到的坑,昨天,Google 终于发布了 Android Studio 2.2 的正式版,于是赶紧第一时间体验了下,按照 Google 的说法本次更新包含了三个方面:speed, smarts, and Android platform support,言外之意就是更快、更智能,而且增加了很多有用的功能,我们来一个个看下。

Layout Editor

本次更新带来了全新的布局编辑器,我们以后调 UI 将更方便。打开一个 XML
文件,默认的 Design 模式如下图所示,主要包含 Palette、Component Tree、Toolbar、Design Editor、Properties 五部分,直接可视化的操作使布局更加方便易操作。

当然对于习惯写 XML 代码的同学来说可以点击左下角的 Text 切换到代码格式,但是右边依然可以实时预览。Text 模式下的截图如下:

这里有个小技巧,可以操作快捷键 Control+Shift+Right/Left 来进行左右切换。

然后我们可以通过 Toolbar 那一栏来配置我们预览的主题外观

评:改进的更方便了,以后可以教你们的设计师帮你们调 UI 了。

Constraint Layout

Constraint Layout 翻译过来我把它叫约束布局,它也是今年 Google 全新推出的一种布局,它更强大,简单来说,用 Constraint Layout 可以实现之前需要各种嵌套才能实现的效果,我们知道过多的布局嵌套对性能影响是很大的,因为 Constraint Layout 更强大,所以属性也就特别多,不过 Google 完全提供了一种可视化的操作,一张动图你们感受下:

关于 Constraint Layout 的详细用法介绍这里就不多说了,Google 官方有个教程,想学习的可以见这里:

Using ConstraintLayout to design your views

友情提示,上面链接需要科学上网,英文阅读有困难的不妨看下这篇博客,我觉得写的还算不错:

Android ConstraintLayout详解

以上 Google 对 UI 布局的改进可以看出,Google 的想法是想让布局更智能更可视化,对于一些刚接触 Android 的同学无意大大降低了门槛,只不过对于一些老一辈的程序员,比如我,还是习惯直接写代码调 UI 来的直接。

评:这个布局很强大,但是宝宝不喜欢拖来拖去,感觉设计师可以开始学 Android 了。

Samples Browser

不知道大家知不知道 GitHub 上 Google 有个叫 Google Samples 的组织,这里罗列了 Google 的上百个关于一些代码的示例,而这其中大部分都是 Android 相关的,比如 NavigationDrawer 不会用了,google 有个 android-NavigationDrawer 的示例。而这次 Google 直接把他关联到 Android Stduio 了,你可以在 Android Studio 选中一个类直接右键点击 Find Sample Code ,神奇的事情发生了:

上图可以看到以选中 PackageManager 为例,下面直接出现了一些 Google Sample 相关的代码,方便你快速查找该用法,而且还有个链接直接指向到 Android Developer 官网该类的详细介绍,简直不要太方便,我喜欢这功能!

评:这功能很实用。

Instant Run Improvements

Instant Run 的推出确实很不错,但是妈蛋第一次编译也太慢了吧,就是因为编译太慢我一般都是把该功能禁用的。我们先来看下 Google 官方的更新说明:

In this release, we have made many stability and reliability improvements to Instant Run. If you have previously disabled Instant Run, we encourage you to re-enable it and let us know if you come across further issues.

卧槽,看完我笑死了,原来 Google 早知道我们会把 Instant Run 功能禁用啊,按照 Google 的说法这次更新做了改进,更稳定,更快了。鼓励我们把 Instant Run 功能打开,好吧,我尝试了一把,确实速度上比之前快不少,大家可以重新打开体验了。打开方法见下图:

评:这次我终于把 Instant Run 功能打开了。

Build cache (Experimental)

其实刚升级 AS 就强烈提示我升级 Gradle 到 2.14 版本,只需要把 Android Gradle plugin 的版本升级到 2.2.0 就好了。

classpath 'com.android.tools.build:gradle:2.2.0'

为了加快 Gradle 的编译速度,Google 新增了一个编译缓存的功能,不过目前还是实验性的,具体用法就是在你的 gradle.properties 文件里加上这么一行代码:

android.enableBuildCache=true

总体来说升级了 Gradle,加上这么一句代码,确实感觉编译快了些,大家可以自行感受下。

对了,每次编译生成的缓存在 ~/users/.android/build-cache 目录下,如果缓存过多可以手动删除该目录进行清除。

评:编译确实快了,不知道是不是错觉。

APK Analyzer

Google 推出了一个 APK
分析器,现在可以很方便的使用 Android Studio 进行 APK 分析。具体用法点击 Build -> Analyze APK 然后选择你要分析的 APK 文件就可以了。

  • 可以方便的查看全部文件和大小

  • 可以直接查看 AndroidManifest.xml 文件

  • 可以直接查看资源文件

查看图片

查看 xml 资源文件

  • 可以直接查看 dex 文件

  • 还可以对两个 apk 进行比较

评:这个功能堪称神器啊,以后人人都会逆向 APK 了。

Virtual Sensors in the Android Emulator

Google 这次同样改进了模拟器,这次让模拟器支持虚拟传感器,你们感受下。

评:对于我这种从不用模拟器的人没啥用。

Espresso Test Recorder (Beta)

Google 为测试新增了一个功能,就是我们可以对操作进行录像,然后根据我们的操作生成一些测试脚本,而且配合 Firebase 将更方便。

评:理论上来说此功能很不错,可以解放了测试人员的双手,只不过该功能还是测试,应该很不稳定,而且国内行情结合 Firebase 很困难,对开发意义不大,可以持续关注。

总结

除以上之外,此次更新还包括对 Java 8 的支持,Jack 编译器的改进,可以调试 GPU,改进了对 C++ 的支持等,总体来说此次更新推出了不少提升 Android 开发效率的工具,性能上也做了优化,值得大家更新!

官方更新说明:

Android Studio 2.2

本文原创发布于微信公众号 AndroidDeveloper,转载请务必注明出处!

作者:googdev 发表于2016/9/20 18:55:00 原文链接
阅读:278 评论:1 查看评论

Github项目解析(十二)-->一个简单的多行文本显示控件

$
0
0

转载请标明出处:一片枫叶的专栏

上一篇文章中我们讲解了一个简单,强大的广告活动弹窗控件。不少App在打开的时候需要弹出一个广告活动弹窗,点击广告活动弹窗中的图片就会跳转到一个H5页面,加载显示具体的活动内容等,为了方便大家的操作,我将其做成了一个标准控件:android-adDialog。需要说明的是,虽然其名称为android-adDialog,并且表现形式也和Dialog类似,但是这里并不是通过Dialog实现的,而是自定义的View。

本文我们将讲解一个使用的多行文本显示控件,在实际开发过程中我们时常会遇到这种需求:有两个TextView控件分行显示,当第一个TextView的内容过多一行显示不下时,我们需要将第二个TextView在第一个TextView的第二行末尾显示,当第二个TextView第二行也显示不下时,第一个TextView的第二行结尾以“…”结束,第二个TextView显示在第二行的最后段,而本文介绍的就是一个实现这种需求的自定义控件。

本项目的github地址:android-titleView,欢迎star和follow。

在介绍具体的使用说明之前,我们先看一下简单的实现效果:
这里写图片描述

使用说明

  • 当大title文案显示为一行的时候,大title和小title分行显示;

  • 当大title文案显示为两行且不满两行的时候,小title在大title第二行显示内容之后显示;

  • 当大title文案显示满两行的时候,小title在第二行末尾显示,大title第二行不占满全行,切末尾以…结束;

  • 支持对大title文案内容,字体颜色,字体大小的设定,支持对小title文案内容,字体颜色,字体大小的设定;

自定义属性说明:

<!-- titleView自定义属性 -->
    <declare-styleable name="titleview" >
        <!-- 用于设定大title的文案内容 -->
        <attr name="title_content" format="string"/>
        <!-- 用于设定小title的文案内容 -->
        <attr name="count_content" format="string"/>
        <!-- 用于设定大title文案文字大小 -->
        <attr name="title_text_size" format="dimension"/>
        <!-- 用于设定大title文案文字颜色 -->
        <attr name="title_text_color" format="color"/>
        <!-- 用于设定小title文案文字大小 -->
        <attr name="count_text_size" format="dimension"/>
        <!-- 用于设定小title文案文字颜色 -->
        <attr name="count_text_color" format="color"/>
    </declare-styleable>

使用方式:

  • 在module的build.gradle中执行compile操作
compile 'cn.yipianfengye.android:mich-titleview:1.0'
  • 在Layout布局文件中定义组件
<com.mich.titleview.TitleView
        android:id="@+id/title_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:padding="10dp"
        app:title_content="人鱼线,狗公腰,刚果队长真是太帅了"
        app:count_content="15.3万次播放"
        app:title_text_size="18dp"
        app:title_text_color="#334455"
        app:count_text_size="12dp"
        app:count_text_color="#666666"
        />
  • 在代码中管理自定义组件
/**
     * 初始化组件
     */
    public void initView() {
        titleView = (TitleView) findViewById(R.id.title_view);
        btnAdd = (Button) findViewById(R.id.btn_add);
        btnDel = (Button) findViewById(R.id.btn_del);

        btnAdd.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                titleView.setTitleContent(titleView.getTitleContent() + "人鱼线,狗公腰,刚过队长太帅了");
            }
        });

        btnDel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (titleView.getTitleContent().length() > 15) {
                    titleView.setTitleContent(titleView.getTitleContent().substring(0, titleView.getTitleContent().length() - 15));
                }
            }
        });
    }

怎么样是不是很简单?下面我们可以来看一下具体API。

实现原理:

下面是自定义组件的布局实现部分:

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

    <TextView
        android:id="@+id/first_textview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="18dp"
        android:textColor="#334455"
        android:maxLines="1"
        />

    <LinearLayout
        android:id="@+id/linear_inner"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="8dp"
        >

        <TextView
            android:id="@+id/second_textview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="18dp"
            android:textColor="#334455"
            android:maxLines="1"
            android:layout_gravity="center_vertical"
            android:ellipsize="end"
            />

        <TextView
            android:id="@+id/count_textview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="15.2万次播放"
            android:textSize="12dp"
            android:maxLines="1"
            android:ellipsize="end"
            android:layout_gravity="center_vertical"
            />
    </LinearLayout>

</marge>

下面是自定义组件的核心代码部分:

/**
 * Created by aaron on 16/9/19.
 * 自定义实现TitleView组件
 */
public class TitleView extends LinearLayout{

    /**
     * title TextView组件
     */
    private TextView firstTitle = null;
    /**
     * title TextView组件
     */
    private TextView secondTitle = null;
    /**
     * 播放次数TextView组件
     */
    private TextView countView = null;

    /**
     * title组件
     */
    private String titleContent = "";
    private String countContent = "";

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

        init(context, null);
    }

    public TitleView(Context context, AttributeSet attrs) {
        super(context, attrs);

        init(context, attrs);
    }

    /**
     * 执行初始化操作
     * @param context
     */
    private void init(Context context, AttributeSet attrs) {
        if (context == null) {
            return;
        }

        View rootView = LayoutInflater.from(context).inflate(R.layout.view_titleview, null);
        ViewGroup.LayoutParams lps = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        this.addView(rootView, lps);

        /**
         * 初始化组件
         */
        initView(rootView);

        /**
         * 初始化自定义属性
         */
        if (attrs != null) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.titleview);
            titleContent = ta.getString(R.styleable.titleview_title_content);
            countContent = ta.getString(R.styleable.titleview_count_content);
            firstTitle.setText(titleContent);
            countView.setText(countContent);

            float titleTextSize = ta.getDimension(R.styleable.titleview_title_text_size, firstTitle.getTextSize());
            firstTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleTextSize);
            secondTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleTextSize);

            int titleTextColor = ta.getColor(R.styleable.titleview_title_text_color, firstTitle.getCurrentTextColor());
            firstTitle.setTextColor(titleTextColor);
            secondTitle.setTextColor(titleTextColor);

            float countTextSize = ta.getDimension(R.styleable.titleview_count_text_size, countView.getTextSize());
            countView.setTextSize(TypedValue.COMPLEX_UNIT_PX, countTextSize);

            int countTextColor = ta.getColor(R.styleable.titleview_count_text_color, countView.getCurrentTextColor());
            countView.setTextColor(countTextColor);
        }

    }


    /**
     * 执行组件的初始化操作
     * @param rootView
     */
    private void initView(View rootView) {
        firstTitle = (TextView) rootView.findViewById(R.id.first_textview);
        secondTitle = (TextView) rootView.findViewById(R.id.second_textview);
        countView = (TextView) rootView.findViewById(R.id.count_textview);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        doUpdateUI();
    }

    /**
     * 执行更新UI的操作
     */
    private void doUpdateUI() {
        if (firstTitle != null) {
            firstTitle.post(new Runnable() {
                @Override
                public void run() {
                    /**
                     * 当title只有一行时,不做任何操作,默认即可
                     */
                    Paint paint = firstTitle.getPaint();
                    if (paint.measureText(titleContent) <= firstTitle.getWidth()) {
                        secondTitle.setVisibility(View.GONE);
                    }
                    /**
                     * 执行title显示为两行的处理逻辑
                     */
                    else {
                        secondTitle.setVisibility(View.VISIBLE);
                        /**
                         * 获取第一行显示内容,第二行显示内容
                         */
                        int count = 1;
                        StringBuffer sb = new StringBuffer(titleContent.substring(0, 1));
                        while (paint.measureText(sb.toString()) < firstTitle.getWidth()) {
                            if (count >= titleContent.length()) {
                                break;
                            }
                            sb.append(titleContent.substring(count, count + 1));
                            count ++;
                        }
                        String firstLineContent = sb.toString();
                        String secondLineContent = titleContent.substring(count);

                        firstTitle.setText(firstLineContent);
                        secondTitle.setText(secondLineContent);

                        /**
                         * 获取相关组件的宽高
                         */
                        int titleWidth = firstTitle.getWidth();
                        int countWidth = countView.getWidth();

                        /**
                         * 获取第二行文字的长度
                         */
                        float secondLineWidth = secondTitle.getPaint().measureText(secondLineContent);

                        /**
                         * 判断第二行文字长度是否大于组件的长度-播放次数组件的长度
                         */
                        if (secondLineWidth > titleWidth - countWidth) {
                            secondTitle.getLayoutParams().width = titleWidth - countWidth;
                        } else {
                            secondTitle.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
                        }

                        secondTitle.setText(secondLineContent);
                    }
                }
            });
        }
    }


    // ###################### 组件的set get方法 #########################

    public String getTitleContent() {
        return new StringBuffer(firstTitle.getText().toString()).append(secondTitle.getText().toString()).toString();
    }

    public void setTitleContent(String titleContent) {
        this.titleContent = titleContent;

        firstTitle.setText(titleContent);
        doUpdateUI();
    }

    public String getCountContent() {
        return countView.getText().toString();
    }

    public void setCountContent(String countContent) {
        this.countContent = countContent;
        countView.setText(countContent);
        doUpdateUI();
    }
}

可以看到注释的挺详细的,其主要的实现思路是:通过三个TextView实现的:

  • 首先判断大title的内容是否占满一行,若没有的话,则大小title控件分行显示;

  • 其次截取大title第一行的显示本文显示在第一行的控件上,然后获取剩余的显示文本显示在第二个控件上,若第二个控件也将占满一行则重设其宽度,预留出可以显示小title控件的位置;

当然更具体的关于控件的实现可以下载源码参考。

总结:

以上就是我实现的这个简单的多行文本显示控件。当然现在还很不完善,以后其还可以添加一些其他的API、自定义属性等,欢迎提出,对于源码有兴趣的同学可以到github上看一下具体实现。项目地址:android-titleview


另外对github项目,开源项目解析感兴趣的同学可以参考我的:
Github项目解析(一)–>上传Android项目至github
Github项目解析(二)–>将Android项目发布至JCenter代码库
Github项目解析(三)–>Android内存泄露监测之leakcanary
Github项目解析(四)–>动态更改TextView的字体大小
Github项目解析(五)–>Android日志框架
Github项目解析(六)–>自定义实现ButterKnife框架
Github项目解析(七)–>防止按钮重复点击
Github项目解析(八)–>Activity启动过程中获取组件宽高的五种方式
Github项目解析(九)–>实现Activity跳转动画的五种方式
Github项目解析(十)–>几行代码快速集成二维码扫描库
Github项目解析(十一)–>一个简单,强大的自定义广告活动弹窗

作者:qq_23547831 发表于2016/9/20 19:10:48 原文链接
阅读:120 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>