2个方法都是刷新父窗口,但是其中还是有奥妙的哦。
window.opener.location.reload(); 这个方法在强迫父窗口的时候,在有些IE浏览器(比如安全设置高)的情况下,会弹出一个确认对话框,提示是不是要重新再刷新一次页面,这可是比较郁闷的事情哦,我后来把这个方法替换成了window.opener.location.href=/blog_article/window.opener.location.href;_br/index.html>就不会出现那样的问题了。
window.opener其实是指本窗口的父窗口,比如,one.jsp 通过popupwindow打开了two.jsp,哪么在two.jsp里面的window.opener就是指one.jsp,所以在two.jsp里面完全可以用window.opener调用任何一个one.jsp里面的方法,实现one.jsp和two.jsp的交互。
注意:window.opener.location.href只是一个链接,如果想实现父窗口的提交就要调用window.opener.action="" 和window.opener.submit(); 方法,但是不幸的是这段代码在firefox下不能运行,解决的办法为在父窗口中写一个提交的function在子窗口中通过window.opener.functionname()调用。
通常在使用window.opener的时候要去判断父窗口的状态,如果父窗口被关闭或者更新,就会出错,解决办法是加上如下的验证if(window.opener && !window.opener.closed)
当我们使用像Skype、QQ这样的工具和朋友流畅地进行语音视频聊天时,我们可曾想过其背后有哪些强大的技术在支撑?本文将对网络语音通话所使用到的技术做一些简单的介绍,算是管中窥豹吧。
一.概念模型网络语音通话通常是双向的,就模型层面来说,这个双向是对称的。为了简单起见,我们讨论一个方向的通道就可以了。一方说话,另一方则听到声音。看似简单而迅捷,但是其背后的流程却是相当复杂的。我们将其经过的各个主要环节简化成下图所示的概念模型:
这是一个最基础的模型,由五个重要的环节构成:采集、编码、传送、解码、播放。
1.语音采集语音采集指的是从麦克风采集音频数据,即声音样本转换成数字信号。其涉及到几个重要的参数:采样频率、采样位数、声道数。
简单的来说:采样频率,就是在1秒内进行采集动作的次数;采样位数,就是每次采集动作得到的数据长度。
而一个音频帧的大小就等于:(采样频率×采样位数×声道数×时间)/8。
通常一个采样帧的时长为10ms,即每10ms的数据构成一个音频帧。假设:采样率16k、采样位数16bit、声道数1,那么一个10ms的音频帧的大小为:(16000*16*1*0.01)/8 = 320 字节。计算式中的0.01为秒,即10ms。
2.编码假设我们将采集到的音频帧不经过编码,而直接发送,那么我们可以计算其所需要的带宽要求,仍以上例:320*100 =32KBytes/s,如果换算为bits/s,则为256kb/s。这是个很大的带宽占用。而通过网络流量监控工具,我们可以发现采用类似QQ等IM软件进行语音通话时,流量为3-5KB/s,这比原始流量小了一个数量级。而这主要得益于音频编码技术。
所以,在实际的语音通话应用中,编码这个环节是不可缺少的。目前有很多常用的语音编码技术,像G.729、iLBC、AAC、SPEEX等等。
3.网络传送当一个音频帧完成编码后,即可通过网络发送给通话的对方。对于语音对话这样Realtime应用,低延迟和平稳是非常重要的,这就要求我们的网络传送非常顺畅。
4.解码当对方接收到编码帧后,会对其进行解码,以恢复成为可供声卡直接播放的数据。
5.语音播放完成解码后,即可将得到的音频帧提交给声卡进行播放。
二.实际应用中的难点及如果仅仅依靠上述的技术就能实现一个效果良好的应用于广域网上的语音对话系统,那就没什么太大的必要来撰写此文了。正是有很多现实的因素为上述的概念模型引入了众多挑战,使得网络语音系统的实现不是那么简单,其涉及到很多专业技术。当然,这些挑战大多已经有了成熟的。首先,我们要为“效果良好”的语音对话系统下个定义,我觉得应该达到如下几点:
(1)低延迟。只有低延迟,才能让通话的双方有很强的Realtime的感觉。当然,这个主要取决于网络的速度和通话双方的物理位置的距离,就单纯软件的角度,优化的可能性很小。
(2)背景噪音小。
(3)声音流畅、没有卡、停顿的感觉。
(4)没有回音。
下面我们就逐个说说实际网络语音对话系统中额外用到的技术。
1.回音消除 AEC现在大家几乎都已经都习惯了在语音聊天时,直接用PC或笔记本的声音外放功能。殊不知,这个小小的习惯曾为语音技术提出了多大的挑战。当使用外放功能时,扬声器播放的声音会被麦克风再次采集,传回给对方,这样对方就听到了自己的回音。所以,实际应用中,回音消除的功能是必需的。
在得到采集的音频帧后,在编码之前的这个间隙,是回音消除模块工作的时机。
其原理简单地来说就是,回音消除模块依据刚播放的音频帧,在采集的音频帧中做一些类似抵消的运算,从而将回声从采集帧中清除掉。这个过程是相当复杂的,而且其还与你聊天时所处的房间的大小、以及你在房间中的位置有关,因为这些信息决定了声波反射的时长。 智能的回音消除模块,能动态调整内部参数,以最佳适应当前的环境。
2.噪声抑制 DENOISE噪声抑制又称为降噪处理,是根据语音数据的特点,将属于背景噪音的部分识别出来,并从音频帧中过滤掉。有很多编码器都内置了该功能。
3.抖动缓冲区 JitterBuffer抖动缓冲区用于解决网络抖动的问题。所谓网络抖动,就是网络延迟一会大一会小,在这种情况下,即使发送方是定时发送数据包的(比如每100ms发送一个包),而接收方的接收就无法同样定时了,有时一个周期内一个包都接收不到,有时一个周期内接收到好几个包。如此,导致接收方听到的声音就是一卡一卡的。
JitterBuffer工作于解码器之后,语音播放之前的环节。即语音解码完成后,将解码帧放入JitterBuffer,声卡的播放回调到来时,从JitterBuffer中取出最老的一帧进行播放。
JitterBuffer的缓冲深度取决于网络抖动的程度,网络抖动越大,缓冲深度越大,播放音频的延迟就越大。所以,JitterBuffer是利用了较高的延迟来换取声音的流畅播放的,因为相比声音一卡一卡来说,稍大一点的延迟但更流畅的效果,其主观体验要更好。
当然,JitterBuffer的缓冲深度不是一直不变的,而是根据网络抖动程度的变化而动态调整的。当网络恢复到非常平稳通畅时,缓冲深度会非常小,这样因为JitterBuffer而增加的播放延迟就可以忽略不计了。
4.静音检测 VAD在语音对话中,要是当一方没有说话时,就不会产生流量就好了。静音检测就是用于这个目的的。静音检测通常也集成在编码模块中。静音检测算法结合前面的噪声抑制算法,可以识别出当前是否有语音输入,如果没有语音输入,就可以编码输出一个特殊的的编码帧(比如长度为0)。
特别是在多人视频会议中,通常只有一个人在发言,这种情况下,利用静音检测技术而节省带宽还是非常可观的。
5.混音算法在多人语音聊天时,我们需要同时播放来自于多个人的语音数据,而声卡播放的缓冲区只有一个,所以,需要将多路语音混合成一路,这就是混音算法要做的事情。即使,你可以想办法绕开混音而让多路声音同时播放,那么对于回音消除的目的而言,也必需混音成一路播放,否则,回音消除最多就只能消除多路声音中的某一路。
混音可以在客户端进行,也可以在服务端进行(可节省下行的带宽)。如果使用了P2P通道,那么混音就只能在客户端进行了。如果是在客户端混音,通常,混音是播放之前的最后一个环节。
综合上面的概念模型以及现实中用到的网络语音技术,下面我们给出一个完整的模型图:
本文是我们在实现OMCS语音部分功能的一个粗略的经验总结。在这里,我们只是对图中各个环节做了一个最简单的说明,而任何一块深入下去,都可以写成一篇长篇论文甚至是一本书。所以,本文就算是为那些刚刚接触网络语音系统开发的人提供一个入门的地图,给出一些线索。
至此,在网络语音技术基础上再引出视频技术,可以借鉴一些SDK资料,
商用或学习都可以使用的,下载SDK可到CSDN上:
http://dldx.csdn.net/fd.php?i=447645132231776&s=ef991809163c5ef73ba9560d77bc6294
看到小米手机和其他一些图片显示软件,gridview显示图片时候,长按某个item,可以进入编辑模式,试着也做了个。
在Android拍照、预览、上传综合的基础上改进,实现效果,长按gridview一个item图片显示,进入编辑模式,可以批量删除图片。两个需要注意的地方:
1、选中图片的边框效果。
实现方法:设置背景图片为将要显示的图片,然后设置图片源setImageResource为带2像素边框的透明小图片。
2、图片右上角给个小对号图片,标示选中。
实现方法:使用相对布局是这个小图片放在右上角。
总结:网上也没有找到一个得劲的实现方式,我做这个后来发现有些瑕疵,本来可以做的更好,如:需要在java代码中控制gridview的全屏以及上下隔出间距显示按钮等其他元素(因为我使用的是Framelayout,各个元素是重叠的,设置显示为gone也没用);如果使用Linearlayout应该可以做到不用java代码设置间距。
效果图如下:
这个没有demo,我把需要的图片,样式给放出来,关键地方有注释说明,源码如下:
【1】、图片展示类gridview
/**
* 图片预览 * @author: aokunsang * @date: 2012-8-1 */ public class PictureScanAct extends Activity { private GridView gridView; private ImageAdapter imgAdapter; private TextView seclectNumView; private Button deleteButton; private List<LoadImage> fileNameList = new ArrayList<LoadImage>(); //保存Adapter中显示的图片详情(要跟adapter里面的List要对应) private List<LoadImage> selectFileLs = new ArrayList<LoadImage>(); //保存选中的图片信息 private boolean isDbClick = false; //是否正在长按状态 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.picturescan); gridView = (GridView)findViewById(R.id.picture_grid); seclectNumView = (TextView)findViewById(R.id.pic_seclet_num); deleteButton = (Button)findViewById(R.id.pic_delete); deleteButton.setOnClickListener(delClickListener); imgAdapter = new ImageAdapter(this); gridView.setAdapter(imgAdapter); gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { LoadImage loadimg = fileNameList.get(position); ViewHolder holder = (ViewHolder)view.getTag(); if(isDbClick){ if(selectFileLs.contains(loadimg)){ holder.image1.setImageDrawable(null); holder.image2.setVisibility(View.GONE); imgAdapter.delNumber(position+""); selectFileLs.remove(loadimg); }else{ holder.image1.setImageResource(R.drawable.border); //添加图片(带边框的透明图片)[主要目的就是让该图片带边框] holder.image2.setVisibility(View.VISIBLE); //设置图片右上角的对号显示 imgAdapter.addNumber(position+""); //把该图片添加到adapter的选中状态,防止滚动后就没有在选中状态了。 selectFileLs.add(loadimg); } seclectNumView.setText("选中"+selectFileLs.size()+"张图片"); }else{ startActivity(new Intent(PictureScanAct.this, PictureViewAct.class).putExtra("flag","upload").putExtra("imagePath",loadimg.getFileName())); } } }); gridView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { LoadImage loadimg = fileNameList.get(position); ViewHolder holder = (ViewHolder)view.getTag(); if(!isDbClick){ isDbClick = true; gridView.setPadding(0, 50, 0, 50); //长按后,让gridview上下都分出点空间,显示删除按钮之类的。看效果图就知道了。 seclectNumView.setVisibility(View.VISIBLE); deleteButton.setVisibility(View.VISIBLE); holder.image1.setImageResource(R.drawable.border); holder.image2.setVisibility(View.VISIBLE); imgAdapter.addNumber(position+""); selectFileLs.add(loadimg); seclectNumView.setText("选中1张图片"); return true; } return false; } }); Toast.makeText(this, "加载图片中....", Toast.LENGTH_SHORT).show(); new AsyncLoadedImage().execute(); } /** * 删除监听器事件 */ private android.view.View.OnClickListener delClickListener = new View.OnClickListener() { @Override public void onClick(View v) { if(selectFileLs.isEmpty()) { Toast.makeText(PictureScanAct.this, "请选择图片", Toast.LENGTH_SHORT).show(); return ; } for(LoadImage loadimg : selectFileLs){ File file = new File(loadimg.getFileName()); boolean isTrue = file.delete(); Log.i("----------------------删除图片------", isTrue+"---------------"); } imgAdapter.deletePhoto(selectFileLs); seclectNumView.setText("选中0张图片"); } }; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if(isDbClick && keyCode == KeyEvent.KEYCODE_BACK){ //点击返回按键 isDbClick = false; gridView.setPadding(0, 0, 0, 0); //退出编辑转台时候,使gridview全屏显示 seclectNumView.setVisibility(View.GONE); deleteButton.setVisibility(View.GONE); selectFileLs.clear(); imgAdapter.clear(); return false; } return super.onKeyDown(keyCode, event); } /** * 异步加载图片展示 * @author: aokunsang * @date: 2012-8-1 */ class AsyncLoadedImage extends AsyncTask<Object, LoadImage, Boolean> { @Override protected Boolean doInBackground(Object... params) { File fileDir = new File(Const.imgPath); File[] files = fileDir.listFiles(); boolean result = false; if(files!=null){ for(File file:files){ String fileName = file.getName(); if (fileName.lastIndexOf(".") > 0 && fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length()).equals("jpg")){ Bitmap bitmap; Bitmap newBitmap; try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 10; bitmap = BitmapFactory.decodeFile(file.getPath(), options); newBitmap = ThumbnailUtils.extractThumbnail(bitmap, 67, 70); bitmap.recycle(); if (newBitmap != null) { LoadImage loadImage = new LoadImage(file.getPath(),newBitmap); fileNameList.add(loadImage); publishProgress(loadImage); result = true; } } catch (Exception e) { e.printStackTrace(); } } } } return result; } @Override public void onProgressUpdate(LoadImage... value) { for(LoadImage loadImage:value){ imgAdapter.addPhoto(loadImage); } } @Override protected void onPostExecute(Boolean result) { if(!result){ showDialog(1); } } } @Override protected Dialog onCreateDialog(int id) { AlertDialog dialog = new AlertDialog.Builder(PictureScanAct.this).setTitle("温馨提示").setMessage("暂时还没有照片,请先采集照片!") .setPositiveButton("确定", new DialogInterface.OnClickListener(){ @Override public void onClick(DialogInterface dialog, int which) { startActivity(new Intent(PictureScanAct.this,TakePhotoAct.class)); } }).setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { finish(); } }).show(); return dialog; } /** * 图片详细信息bean * @author: aokunsang * @date: 2012-8-31 */ public class LoadImage { private String fileName; private Bitmap bitmap; public LoadImage() { super(); // TODO Auto-generated constructor stub } public LoadImage(String fileName, Bitmap bitmap) { super(); this.fileName = fileName; this.bitmap = bitmap; } /** * @return the fileName */ public String getFileName() { return fileName; } /** * @param fileName the fileName to set */ public void setFileName(String fileName) { this.fileName = fileName; } /** * @return the bitmap */ public Bitmap getBitmap() { return bitmap; } /** * @param bitmap the bitmap to set */ public void setBitmap(Bitmap bitmap) { this.bitmap = bitmap; } @Override public int hashCode() { return this.getFileName().hashCode(); } @Override public boolean equals(Object o) { LoadImage loadImg = (LoadImage)o; return this.getFileName().equals(loadImg.getFileName()); } }
【2】、图片展示布局
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <GridView android:id="@+id/picture_grid" android:layout_width="match_parent" android:layout_height="match_parent" android:numColumns="auto_fit" android:stretchMode="columnWidth" android:columnWidth="70dip" android:gravity="center" /> <TextView android:id="@+id/pic_seclet_num" android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@drawable/btn" android:textSize="20sp" android:gravity="center" android:visibility="gone" /> <Button android:id="@+id/pic_delete" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/deletecontact" android:layout_gravity="bottom" android:textSize="20sp" android:visibility="gone" /> </FrameLayout>
【3】、适配器类
/**
* 图片适配器 * @author aokunsang * @Date 2011-12-6 */ public class ImageAdapter extends BaseAdapter { private List<LoadImage> picList = new ArrayList<LoadImage>(); //图片集合 private List<String> picNumber = new ArrayList<String>(); //选中图片的位置集合 private LayoutInflater inflater; public ImageAdapter(Context mContext){ inflater = LayoutInflater.from(mContext); } @Override public int getCount() { return picList.size(); } @Override public Object getItem(int position) { return picList.get(position); } /** * 添加选中状态的图片位置 * @param position */ public void addNumber(String position){ picNumber.add(position); } /** * 去除已选中状态的图片位置 * @param position */ public void delNumber(String position){ picNumber.remove(position); } /** * 清空已选中的图片状态 */ public void clear(){ picNumber.clear(); notifyDataSetChanged(); } /** * 添加图片 * @param bitmap */ public void addPhoto(LoadImage loadImage){ picList.add(loadImage); notifyDataSetChanged(); } /** * 删除图片 * @param loadimgLs */ public void deletePhoto(List<LoadImage> loadimgLs){ for(LoadImage img:loadimgLs){ if(picList.contains(img)){ picList.remove(img); } } picNumber.clear(); notifyDataSetChanged(); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder = null; if(holder==null){ holder = new ViewHolder(); convertView = inflater.inflate(R.layout.picturescan_item, null); holder.image1 = (ImageView)convertView.findViewById(R.id.scan_img); holder.image2 = (ImageView)convertView.findViewById(R.id.scan_select); convertView.setTag(holder); }else{ holder = (ViewHolder)convertView.getTag(); } Drawable bit = new BitmapDrawable(picList.get(position).getBitmap()); holder.image1.setBackgroundDrawable(bit); if(picNumber.contains(""+position)){ //如果该图片在选中状态,使其右上角的小对号图片显示,并且添加边框。 holder.image2.setVisibility(View.VISIBLE); holder.image1.setImageResource(R.drawable.border); }else{ holder.image2.setVisibility(View.GONE); } return convertView; } public static class ViewHolder{ public ImageView image1; //要显示的图片 public ImageView image2; //图片右上角的小对号图片(标示选中状态的玩意) } }
【4】、gridview的item布局
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingTop="3dip" android:paddingRight="3dip" > <ImageView android:id="@+id/scan_img" android:scaleType="centerCrop" android:layout_height="75dip" android:layout_width="90dip" /> <ImageView android:id="@+id/scan_select" android:layout_height="wrap_content" android:layout_width="wrap_content" android:src="/blog_article/@drawable/selected/index.html" android:layout_alignRight="@id/scan_img" android:layout_alignParentTop="true" android:padding="4dip" /> </RelativeLayout>